Skip to content

fix(runtime-host): lazily adopt legacy Session Connection identity - #4627

Open
bug-superman wants to merge 1 commit into
apache:mainfrom
bug-superman:codex/issue-3860-delegated
Open

fix(runtime-host): lazily adopt legacy Session Connection identity#4627
bug-superman wants to merge 1 commit into
apache:mainfrom
bug-superman:codex/issue-3860-delegated

Conversation

@bug-superman

Copy link
Copy Markdown

Closes #3860.

Summary

  • Add a Host-owned, revision/CAS-gated lazy adoption path for unlocked legacy Sessions.
  • Revalidate the adopted identity through the bound ID + slug resolver before execution.
  • Keep locked legacy Sessions fail-closed and require explicit account selection.
  • Run adoption before fresh root execution, recovery, and safe-boundary continuation admission.
  • Preserve the existing immutable Connection ID propagation into AgentRuns and derived Session paths.

Safety

Bound execution continues to resolve by immutable connectionId and verifies the stored slug. Missing IDs and ID/slug mismatches never fall back to a same-slug catalog entry, so deleted/reused Connections cannot cross credentials. OAuth reconciliation remains ID-bound through the existing resolver.

A pre-upgrade legacy Session has no deletion tombstone or historical identity evidence. If its original Connection was deleted and the slug reused before this upgrade, the original identity cannot be reconstructed; locked legacy Sessions therefore remain blocked instead of being silently rebound.

Verification

  • npm --workspace @maka/runtime-host run typecheck (Node 24)
  • npm --workspace @maka/runtime-host run build (Node 24)
  • Focused runtime-host session catalog and host availability tests: all pass
  • Focused storage runtime-policy tests: all pass
  • Full monorepo build reaches unrelated pre-existing @maka/ui type errors after core/storage/runtime/runtime-host compile successfully.

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 3, 2026
@bug-superman
bug-superman force-pushed the codex/issue-3860-delegated branch 3 times, most recently from 0fe755d to b494457 Compare September 3, 2026 07:41
@bug-superman
bug-superman force-pushed the codex/issue-3860-delegated branch from b494457 to 410468d Compare September 3, 2026 07:42
@testikun

testikun commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Generative tooling disclosure: OpenAI Codex contributed materially to this change; final review and ownership remain with the human contributor.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction is right and the identity store stays single-owner: llmConnectionId is still only written by HostSessionCatalogCoordinator and the revision inheritance path. I verified the Host has one SessionAdmissionGate instance shared by the catalog, root and message coordinators, that moving the sessionCatalog construction earlier does not use anything defined after it, and that the six readSessionHeader call sites in message-coordinator.ts that do not pass a lease only read isArchived, so they are not a gap. Three things to fix before merge, plus one scope question.

P2: the wiring has no test. All five new tests call coordinator.adoptLegacySessionConnectionIdentity(...) directly; nothing exercises RootTurnCoordinator or HostMessageCoordinator, so the user-visible outcome the PR claims (a legacy Session can send again) is not covered. Two things make that riskier than usual. The adopter is an optional trailing constructor parameter, so every existing RootTurnCoordinator construction silently keeps the old behavior and a wiring mistake cannot turn a test red. And a lease that crosses task contexts makes SessionAdmissionGate throw (Cannot reuse a Session admission lease from another task, Session admission lease was not issued by this gate), which would surface as a hard error on the normal send path. I could not verify by running whether the lease passed at root-turn-coordinator.ts:2213 is always in the issuing context, so that is an evidence gap on my side. Please add one end to end test: an unlocked legacy Session that is refused on main and sends here, asserting llmConnectionId is persisted.

P3: readSessionHeader now writes to the store (root-turn-coordinator.ts:510, contract at message-coordinator.ts:197), while its name and interface still say read. Consider renaming it or lifting the adoption out into its own call.

P3: prepareSessionHeader is called at nine sites, each one a readHeaderSnapshot immediately followed by a prepareSessionHeader. Folding both into a single private readAdmittedHeader(sessionId, lease) puts each call site back to one line and makes it impossible for a future entry point to miss the step.

Scope question, not a finding: connectionLocked is set as soon as a user message lands (packages/storage/src/sqlite-session-metadata-store.ts:1494), so this only unblocks legacy Sessions that never sent anything. Every pre-upgrade Session with history stays blocked and needs the user to explicitly reselect a Connection. The Safety section explains why locked Sessions cannot be guessed, and I agree with that, but the title and summary read as if all legacy Sessions are covered. Please state the actual scope, and confirm the reselect path is reachable in the UI for a Session that is currently reporting the legacy unavailable reason.

On CI: gh pr checks reports no checks on this branch, and the only workflow run at this head is CI sitting at action_required. Nothing has been verified by CI at 410468db.

if (!connection) return undefined;
let committed: SessionHeaderSnapshot;
try {
committed = await this.#stores.updateHeaderVersioned(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: the CAS write happens before the revalidation below, and a failed revalidation does not undo it. If the catalog changes between the snapshot on line 260 and resolveExecutionConnection on line 275, or the two simply disagree, the method returns undefined (caller sees the pre-write header and fails closed for this execution) while the store now holds a llmConnectionId that never passed revalidation. On the next admission the first guard on line 254 sees a defined id and skips adoption entirely, so the Session executes with that binding and nothing ever clears it. Credentials cannot cross, since bound never falls back to slug, but it is still a committed wrong state, and the new test at session-catalog-coordinator.test.ts:1144 asserts the bad id stays persisted. Smallest fix: revalidate first and commit only on a resolvable identity. #3860 asks for revalidation before execution, which validate-then-write also satisfies, with one fewer residual state.

) {
return undefined;
}
const connection = (await this.#runtimePolicy.connectionCatalog.getSnapshot()).connections.find(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: this is a second slug to identity resolver. resolveExecutionConnection({ kind: 'catalog_slug', connectionSlug }) already does exactly this lookup, inside the runtime policy lane and with the retired and enabled checks, at packages/storage/src/runtime-policy/coordinator.ts:826. Resolving here off a raw catalog snapshot is what lets the two disagree, which is precisely the case the post-commit revalidation then has to fail closed on. Going through the existing resolver and taking connection.connectionId off the ready result removes this duplication and lets the revalidation block on lines 275 to 282 disappear.


/**
* Compatibility migration for pre-identity Sessions that have not yet
* committed a user turn. The write is revision/CAS guarded and the resulting

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: the comment says "have not yet committed a user turn" but the code branches on connectionLocked (line 256). They are equivalent today because the store sets the lock on the first user message, but the comment should name the field it actually reads, otherwise it becomes wrong the moment connectionLocked picks up another meaning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(runtime): bind Sessions and AgentRuns to immutable Connection identity

3 participants