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

Method flipCursorSessionProtocolToAcp

hub/src/sync/syncEngine.ts:448–528  ·  view source on GitHub ↗

* Apply the post-migration metadata flip in hapi.db: * - metadata.cursorSessionProtocol = 'acp' * - session.model = lastUsedModel (if provided) * * Returns 'success' on a clean write, 'version-mismatch' if the metadata * version moved underneath us (caller retries) or 'n

(
        sessionId: string,
        namespace: string,
        lastUsedModel: string | null
    )

Source from the content-addressed store, hash-verified

446 * writes funnel through the existing cache-refresh path.
447 */
448 flipCursorSessionProtocolToAcp(
449 sessionId: string,
450 namespace: string,
451 lastUsedModel: string | null
452 ): { result: 'success' | 'version-mismatch' | 'not-found' | 'session-active' } {
453 for (let attempt = 0; attempt < 2; attempt += 1) {
454 const latest = this.sessionCache.getSessionByNamespace(sessionId, namespace)
455 ?? this.sessionCache.refreshSession(sessionId)
456 if (!latest?.metadata) {
457 return { result: 'not-found' }
458 }
459 // Combined SSE-event payload contract (UX A++): clear the
460 // `cursorMigrationState='in_progress'` flag in the SAME metadata
461 // write that flips `cursorSessionProtocol` to 'acp'. The web
462 // banner keys off `cursorMigrationState`, so a single SSE
463 // session-updated event swaps both atomically — banner gone,
464 // protocol flipped — preventing a flicker window where the
465 // banner has already disappeared but the chat hasn't re-rendered
466 // to the ACP transport yet.
467 const carriedMigrationState = latest.metadata.cursorMigrationState
468 // Atomic active-check inside the same synchronous flip op so
469 // that a resume cannot land between the migrator's recheck
470 // and the actual DB update. Bun is single-threaded — once
471 // we've read `latest` and the row is inactive, no other JS
472 // can mutate active=true until this method returns. Codex
473 // review #34 P1 v2: the migrator's recheck is best-effort;
474 // this is the authoritative gate.
475 //
476 // Codex review #34 P2 v5: only block on `active === true`,
477 // NOT on lifecycleState === 'running'. After a force-archive
478 // flow archiveSession() synchronously sets active=false but
479 // the cleanup metadata write that flips lifecycleState
480 // 'running' → 'archived' may still be in-flight, and that
481 // is OUR archive completing, not a resume race. The active
482 // flag is the authoritative live-runner signal.
483 if (latest.active === true) {
484 return { result: 'session-active' }
485 }
486 // Codex review #34 P2 v7: ALSO clear a stale lifecycleState
487 // value if it still says 'running'. The migrator now skips
488 // archiveSession() for stale-running rows (active=false but
489 // lifecycle=running with --force-archive-running) because
490 // there's no live runner to archive. Without this fixup,
491 // successfully migrated stale rows would retain lifecycle=
492 // running forever and any downstream code that filters by
493 // lifecycleState (not the cache active flag) would keep
494 // treating archived ACP sessions as live.
495 const oldLifecycle = typeof latest.metadata.lifecycleState === 'string' ? latest.metadata.lifecycleState : undefined
496 const nextMetadata: typeof latest.metadata = {
497 ...latest.metadata,
498 cursorSessionProtocol: 'acp' as const,
499 ...(oldLifecycle === 'running' ? { lifecycleState: 'archived' as const } : {})
500 }
501 // Drop the migration-in-progress flag in the same write (see
502 // header comment). Safe whether or not it was set.
503 if (carriedMigrationState !== undefined) {
504 delete nextMetadata.cursorMigrationState
505 }

Callers 2

Calls 4

refreshSessionMethod · 0.80
updateSessionMetadataMethod · 0.80
setSessionModelMethod · 0.80
getSessionByNamespaceMethod · 0.45

Tested by

no test coverage detected