MCPcopy Create free account
hub / github.com/tiann/hapi / maybeAutoMigrateLegacyCursorSession

Method maybeAutoMigrateLegacyCursorSession

hub/src/sync/syncEngine.ts:825–951  ·  view source on GitHub ↗

* tiann/hapi#824 — sync-on-open auto-migration. Returns the (possibly * refreshed-from-cache) session. If the session is a legacy stream-json * Cursor session AND the env flag is on, attempts a transplant migration * synchronously before the caller spawns the runner. * * The

(session: Session, namespace: string)

Source from the content-addressed store, hash-verified

823 * caller proceeds with the legacy launcher.
824 */
825 private async maybeAutoMigrateLegacyCursorSession(session: Session, namespace: string): Promise<Session> {
826 const md = session.metadata
827 const flagRaw = process.env.HAPI_CURSOR_LEGACY_AUTO_MIGRATE?.trim().toLowerCase() ?? ''
828 console.info('[auto-migrate] considering', {
829 sessionId: session.id,
830 flavor: md?.flavor ?? null,
831 proto: md?.cursorSessionProtocol ?? null,
832 hasCursorId: typeof md?.cursorSessionId === 'string' && md.cursorSessionId.length > 0,
833 envFlag: flagRaw === '' ? '(unset; default on)' : flagRaw
834 })
835
836 if (flagRaw === '0' || flagRaw === 'false' || flagRaw === 'no' || flagRaw === 'off') {
837 console.info('[auto-migrate] skipped: env flag disabled', { sessionId: session.id })
838 return session
839 }
840 if (!md || md.flavor !== 'cursor') {
841 console.info('[auto-migrate] skipped: not a cursor session', { sessionId: session.id, flavor: md?.flavor ?? null })
842 return session
843 }
844 if (md.cursorSessionProtocol === 'acp') {
845 console.info('[auto-migrate] skipped: already ACP', { sessionId: session.id })
846 return session
847 }
848 if (typeof md.cursorSessionId !== 'string' || md.cursorSessionId.length === 0) {
849 console.info('[auto-migrate] skipped: no cursorSessionId', { sessionId: session.id })
850 return session
851 }
852
853 console.info('[auto-migrate] starting transplant', { sessionId: session.id, cursorSessionId: md.cursorSessionId })
854 // UX A++: surface the migration to the user via a banner in the
855 // web UI. We set `cursorMigrationState='in_progress'` on the row
856 // BEFORE the long-running transplant; the sessionCache.refresh()
857 // call emits a `session-updated` SSE event the web client uses to
858 // render the banner. The flag is cleared on success by the same
859 // metadata write that flips cursorSessionProtocol to 'acp' (see
860 // flipCursorSessionProtocolToAcp) so the banner disappears in the
861 // same render tick the chat re-renders as ACP — no flicker. On
862 // failure we clear the flag explicitly in the catch path below.
863 const flagSet = this.setCursorMigrationStateInProgress(session.id, namespace)
864 let bannerCleanupNeeded = flagSet
865 try {
866 const migrator = this.buildMigratorForRequest({})
867 // Codex #34 P2 (round 13): for inactive rows whose metadata
868 // still reads `lifecycleState === 'running'` (e.g. orphaned by
869 // a hub crash where the lifecycle transition didn't land), the
870 // migrator's preflight refuses with `running_refused` unless
871 // `forceArchiveRunning` is true. resumeSession's caller-side
872 // guard already ensured `session.active === false` (see the
873 // early-return above), so we know there's no runner to yank;
874 // the `running` lifecycle is stale metadata, not a live agent.
875 // This is exactly the stale-row case the sync-on-open path is
876 // meant to clean up — refusing here would defeat the whole
877 // point and silently fall back to the legacy launcher forever.
878 const outcome = await migrator.migrateOne(session, { forceArchiveRunning: true })
879 if (outcome.ok) {
880 console.info('[auto-migrate] success', {
881 sessionId: session.id,
882 cursorSessionId: md.cursorSessionId,

Callers 2

resumeSessionMethod · 0.95
callHelperFunction · 0.80

Calls 8

infoMethod · 0.80
migrateOneMethod · 0.80
warnMethod · 0.80
getSessionByNamespaceMethod · 0.45

Tested by 1

callHelperFunction · 0.64