agentHost: make the orchestrator own session enumeration and chat lifecycle - #329633
Draft
Sandeep Somavarapu (sandy081) wants to merge 37 commits into
Draft
agentHost: make the orchestrator own session enumeration and chat lifecycle#329633Sandeep Somavarapu (sandy081) wants to merge 37 commits into
Sandeep Somavarapu (sandy081) wants to merge 37 commits into
Conversation
Make the orchestrator (AgentService + AgentHostStateManager) own the Session
concept - identity, lifecycle, and grouping - so the agent harness talks only in
chats. Session provisioning stays agent-specific but is now invoked through the
chat surface instead of a Session-typed method, honoring "represent, don't
orchestrate".
- Create: `_provisionSessionViaDefaultChat` allocates the session URI and drives
`chats.createChat(defaultChatUri, { provisionSession })`; the agent's
provisioning runs inside creating the default chat and returns
`IAgentCreateChatResult.provision`.
- Dispose: routes to `chats.disposeChat(defaultChatUri)`.
- Enumerate: `_enumerateProviderSessions` groups `listConversations()` into
sessions via the default-chat URI convention.
Gated per harness by `IAgent.orchestratorOwnsSession` (Codex, Claude, Copilot all
opt in). Storage-preserving: session URIs and the derived `sdkSessionId == session
raw id` (I3) are unchanged, agents read/write the same SDK stores, and
providerData / PEER_CHATS_METADATA_KEY / protocol types are untouched. The legacy
createSession/disposeSession/listSessions remain as the delegated fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ingle path Address review feedback: the agent should not declare an orchestration policy, and the interface should not carry an optional flag that splits behavior. The opt-in was transitional scaffolding for a per-agent rollout; all harnesses have migrated, so remove it and make the orchestrator drive the chat surface unconditionally. - Remove `IAgent.orchestratorOwnsSession`; make `listConversations` required. - `AgentService` always provisions (non-fork/import) / disposes / enumerates through the chat surface; no per-agent branch. - Drop the flag from Claude/Copilot/Codex. - Make both test mocks first-class chat-surface agents (provisionSession bridge, default-chat disposeChat, listConversations) so their existing createSession/ disposeSession assertions still hold via the bridge. - Update the routing test to assert session create/dispose now also flow through the chat surface; refresh the architecture doc. Storage-preserving; no protocol/data change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shorten the JSDoc/inline comments added for the session-ownership relocation to 1-2 sentences per the coding guidelines; drop obvious per-field comments. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove `listSessions` and `getSessionMessages` from the IAgent contract - they are superseded by `listConversations` and `chats.getMessages`. Reroute the one remaining internal caller (the restore metadata catalog fallback) to `_enumerateProviderSessions` (which uses `listConversations`). The harnesses keep those methods privately as the implementation their chat/conversation bridges delegate to. `createSession`/`disposeSession` stay on IAgent as the session-lifecycle provisioning primitives the chat-surface bridge delegates to; `createSession` is also still used directly for fork/import, whose session id is minted server-side (sessions.fork) and so cannot fit the orchestrator-allocates-URI seam - left as a documented follow-up. No behavior change; storage-preserving. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align the single-item metadata lookup with the chat-addressed conversation surface: the method is now keyed by a chat URI and returns IAgentConversationMetadata, mirroring listConversations. All five implementers (Copilot, Claude, Codex, and both test mocks) derive the session from the chat URI and return chat-keyed metadata; the orchestrator maps the default-chat URI back to a session when hydrating restore metadata. Also reframe the fork/import createSession path in MULTI_CHAT_ARCHITECTURE.md from a deferred follow-up into a permanent, by-design exception (the fork id is minted server-side by the SDK, so the orchestrator cannot pre-allocate the URI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sions-design-review # Conflicts: # src/vs/platform/agentHost/node/codex/codexAgent.ts
…c by design Document why the field keeps the SessionMeta alias rather than a conversation-specific type: _meta is the protocol's open property bag on SessionState / SessionSummary, carried through verbatim. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssion URI Document that the session parameter is the provider's own SDK session (the SDK's terminology), NOT the AH-level Session grouping - that grouping lives in the orchestrator and the agent only ever deals in chats. The URI backs the default chat (invariant I3), so chats.disposeChat routes here when a default chat is disposed; peer chats go to _disposeChat. Teardown disposes that SDK session plus the peer-chat backings the agent parents under it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a 'session is overloaded' convention table to the Mental Model section: in the protocol/orchestrator 'session' means the AH grouping; inside an agent harness it means the provider's own SDK session (Codex: thread); at the IAgent seam the session URI is a shared identity (AH-minted, SDK-session-id raw id per I3). Explains why we do not rename the provider-internal 'session' symbols. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ession->chat) Revert the chat-keyed enumeration surface (listConversations / getConversationMetadata) back to session-keyed listSessions / getSessionMetadata on IAgent. Chat-keyed enumeration forced every harness to derive default-chat URIs via buildDefaultChatUri for cold (SDK-discovered) sessions it never created in-process - re-deriving the session<->default-chat encoding that belongs to the orchestrator/protocol. Now each agent returns its own SDK-session identity (AgentSession.uri: provider scheme + SDK id, no protocol-chat knowledge) and the orchestrator owns the session->chat mapping. Drops the orchestrator's _conversationToSessionMetadata bridge (the enumeration/restore round-trip) and deletes the now-unused IAgentConversationMetadata type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agents no longer call buildDefaultChatUri. Outbound events, default-vs-peer comparisons, and session-or-chat normalizers now reuse the chat URI the agent was already given - read back from the session entry's stored defaultChatKey (new getter on AgentSessionEntry) or the live session's stored chat channel, or tested with isDefaultChatUri - instead of re-deriving it from the session URI. The one irreducible conversion (a session URI first born inside the agent: a freshly forked SDK-assigned id, or a cold-restore/create seed) is centralized in a single node-layer helper, defaultChatUriForSession, in agentPeerChats.ts. This is creation-time only; no runtime routing/event path derives chat URIs anymore. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The provision (create) path already hands the agent the orchestrator-allocated
default-chat URI via createChat(defaultChatUri, { provisionSession }). Claude and
Codex decoded it to a session and then re-derived the identical URI inside
createSession. Thread the supplied chat URI straight through (createSession's new
optional defaultChat argument) so the agent seeds its entry with the URI it was
handed instead of re-deriving the session-to-default-chat mapping itself.
Copilot has no synchronous create seed (it stores a provisional session and
seeds the default-chat key at materialize/resume), so it has no provision
round-trip to thread; its derivations are the restart-lazy category.
The remaining defaultChatUriForSession callers are the restart-lazy paths (cold
resume, peer-send provisional default, fork/restore materialize) where the
orchestrator supplies no chat URI in-call; documented as the single sanctioned,
irreducible conversion. Behavior is unchanged (the mapping is deterministic);
two Claude tests that deep-equal the emitted URI object are aligned with the
file's toString-based convention since keying now populates the URI's cache.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make Agent Host own chat membership and pass contextual data only for individual operations. Providers route exact chats to their SDK conversations without deriving default or peer roles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sions-design-review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Post-merge repairs and review follow-ups for the AH-owned multi-chat architecture, keeping providers on exact chat-to-SDK bindings: - Make `IAgentChats.releaseChat` mandatory so AgentService has one exact-chat release path with no optional legacy fallback; Claude, Copilot, Codex, and the test agents implement it explicitly. - Copilot chat disposal now propagates SDK deletion failures (preserving routing/state for retry) but tolerates an already-deleted session via an O(1) `getSessionMetadata` recheck, keeping a partially-completed multi-chat teardown retry-safe. - Claude: gate materialization on post-await cancellation, abort every live session's controller on dispose, and restore session-addressed resume without inferring chat membership from the URI. - Rename `IAgentCreateChatOptions.provisionSession` to `newSession` to state intent (this createChat creates the owning session). Verified typecheck, transpile, AgentService/Claude/Copilot/Codex unit suites, valid-layers, and hygiene.
Copilot's `chats.getMessages` routes to `_getChatMessages`, which lacked the
subagent-session-URI branch that only lived in the now-orphaned
`getSessionMessages`. On the persisted replay/restore path the orchestrator
loads a subagent's turns through the chat surface, so reopening a session
rebuilt an empty subagent transcript — failing the "reopening a session keeps
sub-agent messages out of the parent transcript (replay path)" E2E test on all
platforms. Extract a shared `_getSubagentMessages` helper and route subagent
URIs through it from both `_getChatMessages` and `getSessionMessages` (matching
Claude, which already shares one path).
Also address PR review feedback:
- `AgentService._releaseSession` releases every catalog chat even if one
rejects, then propagates the first error (idle eviction has already dropped
the session state, so a skipped leaf would stay resident indefinitely).
- `CopilotAgentSession` stores the host-supplied `IAgentChatContext.resource`
as its persistence scope instead of re-deriving it from the mutable chat
channel via `isDefaultChatUri`, so an explicitly chosen resource survives a
later `bindChatChannel`.
- MULTI_CHAT_ARCHITECTURE.md: correct the flat `IClaudeChatBinding` shape
(`{ sdkSessionId, model? }`, no retained session/storageUri).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… chats
Removes the `newSession` seam so an agent no longer distinguishes "a chat for a
new session" from "a chat for an existing session". Session provisioning now
always goes through the agent's dedicated `createSession` + `chats.bindSessionChat`
(the same path fork/import already used), and `chats.createChat` has exactly one
meaning: add an additional chat to an already-provisioned session.
Contract:
- Delete `IAgentProvisionSession`, `IAgentCreateChatOptions.newSession`,
`IAgentProvisionResult`, and `IAgentCreateChatResult.provision`.
- Add `IAgentCreateChatOptions.inheritedContext` ({ workingDirectory, config }):
the orchestrator supplies the owning session's resolved context when creating
an additional chat, so the agent never reads it back from the parent session.
Orchestrator:
- `_createProviderSession` always provisions via `createSession` +
`bindSessionChat`; delete `_provisionSessionViaDefaultChat`.
- `_buildInheritedChatContext` resolves the AH-owned worktree/folder + session
config values and passes them to `chats.createChat`/`fork`.
Agents (Claude, Copilot, Codex):
- Drop the `if (options.newSession)` branch and the `_provisionChat` method; the
chat surface handles additional chats only.
- Claude/Copilot consume `inheritedContext` for the additional-chat working
directory (and Claude for its permission mode) instead of resolving the parent
session; remove the now-dead `_createSession(target)` plumbing where the
provision path was its only caller.
Tests/docs:
- Rewrite the AgentService routing test to assert provisioning via
createSession + bindSessionChat.
- Update MULTI_CHAT_ARCHITECTURE.md §2/§7 to the new seam.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds Codex multi-chat support (parity with Claude/Copilot): the
`multipleChats: { fork: true }` capability, `chats.createChat`/`chats.fork`
minting a fresh backing Codex thread per chat, `materializeChat` restore, and a
providerData codec. This is committed as the base of the dedicated I3-removal
branch (it is intentionally not on the PR branch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce AgentSessionRegistry, a durable, orchestrator-owned index of the sessions that exist, keyed by session URI and persisted as a JSON blob in a reserved session database with serialized read-modify-write. Wire it into AgentService: register on every createSession success and on restoreSession, unregister on true delete (disposeSession). Add a Stage 1 validation surface (getRegisteredSessions) plus component and parity unit tests. This is additive and does NOT yet drive enumeration; listSessions still uses the provider-derived path. It is the foundation for switching enumeration off invariant I3 in stage 2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…moval stage 2) Switch AgentService.listSessions to iterate the orchestrator-owned session registry instead of unioning each provider's listSessions(). Per-session metadata still comes from the agent's direct getSessionMetadata lookup (I3 keeps the default chat's SDK id == session id, so it resolves), then flows through the existing DB and state-manager overlays unchanged. This decouples AH enumeration from the agents' SDK stores: peer-chat backings and subagent sessions never enter the registry (so they cannot leak as top-level entries), and a provider that transiently drops a session from its own snapshot no longer evicts it. Idle provisional sessions are suppressed explicitly via a new state-manager predicate (isIdleProvisionalSession), preserving #321269 now that the registry — not the provider snapshot — is the session source. A one-time, marker-gated backfill seeds the registry from the legacy provider enumeration so hosts created before the registry keep their on-disk sessions. I3 is unchanged; agents are untouched. Adds backfill and transient-drop tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codex's default chat does not actively satisfy I3: a fresh session's raw id is an AH-minted provisional UUID while its backing thread id is app-server-assigned, with the real mapping persisted in the per-session metadata overlay. The residual sessionId == threadId uses (_readSession's ?? sessionId fallback and listSessions' thread->URI mapping) are legacy-compat shims for pre-existing sessions whose persisted identity is the thread id; they cannot be removed without a data migration (disallowed), so Codex is treated as already I3-satisfied. Comment/doc-only: clarifies the two shim sites and adds a per-agent nuance note to the I3 invariant in MULTI_CHAT_ARCHITECTURE.md. No behavior change. The active I3 removal targets are Claude and Copilot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onChat (I3 removal stage 4, step 1)
Add an optional chat-surface entry, chats.createSessionChat, that provisions a
session AND binds its session-backed (default) chat in one call — the
replacement for the IAgent.createSession + bindSessionChat provisioning pair.
The agent reuses the session id as its SDK id (id-reuse kept; no storage
change, no I7 for the default chat). The orchestrator mints the session URI,
derives the default-chat URI, and calls createSessionChat; agents that don't
implement it fall back to the create-then-bind pair.
Claude implements it via its existing { kind: 'chat' } provisioning path (also
used by truncate), so routing state is identical to create-then-bind. Only
fresh sessions collapse: fork and import mint a fresh SDK-assigned session id
inside the agent, so the orchestrator can't know the default-chat URI up front
and keeps them on the create-then-bind pair. bindSessionChat is now documented
as the restore-time counterpart.
Additive and always-green: Copilot/Codex still use createSession. Validated
typecheck, layers, eslint, hygiene; Claude units 206, AgentService 140, Claude
E2E replay 8.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… removal stage 4, step 2) Both agents now provision a fresh session and bind its session-backed (default) chat through the collapsed chats.createSessionChat entry, delegating to their existing _createSession and then binding the default chat (id-reuse; no storage change, no I7 for the default chat). The orchestrator already prefers this path for fresh sessions across all providers; fork/import still use createSession. Validated typecheck, layers, eslint, hygiene; Copilot 347, Codex 47, AgentService 140 units; E2E replay Copilot 15 / Codex 6 / Claude 8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…removal # Conflicts: # src/vs/platform/agentHost/test/node/agentService.test.ts # src/vs/platform/agentHost/test/node/copilotAgent.test.ts
…removal # Conflicts: # src/vs/platform/agentHost/node/agentSideEffects.ts # src/vs/platform/agentHost/node/copilot/copilotAgent.ts
…removal # Conflicts: # src/vs/platform/agentHost/node/agentPeerChats.ts
…removal # Conflicts: # src/vs/platform/agentHost/node/codex/codexAgent.ts
…removal # Conflicts: # .github/skills/sessions/SKILL.md # src/vs/platform/agentHost/MULTI_CHAT_ARCHITECTURE.md # src/vs/platform/agentHost/node/agentHostStateManager.ts # src/vs/platform/agentHost/node/agentService.ts # src/vs/platform/agentHost/node/copilot/copilotAgent.ts # src/vs/platform/agentHost/test/node/agentService.test.ts # src/vs/platform/agentHost/test/node/copilotAgent.test.ts
…removal # Conflicts: # .github/skills/sessions/SKILL.md # src/vs/platform/agentHost/common/agentService.ts # src/vs/platform/agentHost/node/agentService.ts # src/vs/platform/agentHost/node/claude/claudeAgent.ts # src/vs/platform/agentHost/node/claude/claudeAgentSession.ts # src/vs/platform/agentHost/node/codex/codexAgent.ts # src/vs/platform/agentHost/node/copilot/copilotAgent.ts # src/vs/platform/agentHost/test/node/agentService.test.ts # src/vs/platform/agentHost/test/node/copilotAgent.test.ts
Advertise Codex multiple-chat and fork support now that the exact chat binding, model-provider forwarding, and registry-owned enumeration paths are complete. Keep provider-owned side chats disabled for Codex. Add replay-only parity gating for Codex model-backed peer/fork tests. Host-only capability checks and conformance catalog/lifecycle coverage remain enabled; recording mode still runs the gated tests once the documented live Codex recording defect is fixed. No capture files are fabricated or hand-edited. Validated typecheck, layers, ESLint, Agent Host unit suites, and Claude/Copilot/Codex strict replay. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot started reviewing on behalf of
Sandeep Somavarapu (sandy081)
August 7, 2026 16:42
View session
Keep Codex model-backed peer/fork tests skipped in strict replay while permitting both focused recording modes to execute them and generate fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Moves Agent Host session enumeration and chat lifecycle ownership into the orchestrator while adding Codex multi-chat support.
Changes:
- Adds a durable session registry and orchestrator-owned lifecycle/config fan-out.
- Extends chat APIs with explicit host context and session-chat provisioning.
- Enables Codex multi-chat capabilities and expands unit/E2E coverage.
Show a summary per file
| File | Description |
|---|---|
.github/skills/sessions/SKILL.md |
Documents ownership constraints. |
MULTI_CHAT_ARCHITECTURE.md |
Updates architecture and lifecycle model. |
common/agentService.ts |
Extends agent/chat contracts. |
node/agentChatBackings.ts |
Re-exports chat backing codecs. |
node/agentHostStateManager.ts |
Detects idle provisional sessions. |
node/agentService.ts |
Owns registry and chat lifecycle. |
node/agentSessionRegistry.ts |
Implements durable session enumeration. |
node/agentSideEffects.ts |
Adds chat context and config fan-out. |
node/claude/claudeAgentSession.ts |
Uses host-provided storage context. |
node/claude/claudeSubagentSignals.ts |
Updates terminology. |
node/codex/codexAgent.ts |
Implements Codex multi-chat routing. |
node/codex/codexMapAppServerEvents.ts |
Updates subagent terminology. |
node/copilot/copilotAgentSession.ts |
Separates routing and storage URIs. |
test/node/agentService.test.ts |
Tests registry and lifecycle behavior. |
test/node/agentSessionRegistry.test.ts |
Tests registry persistence. |
test/node/agentSideEffects.test.ts |
Tests config fan-out. |
test/node/codex/codexAgent.test.ts |
Tests Codex conversation resolution. |
test/node/codex/codexMapAppServerEvents.test.ts |
Updates expected terminology. |
test/node/codex/codexPrewarmEviction.test.ts |
Adapts Codex lifecycle tests. |
test/node/e2e/KNOWN_ISSUES.md |
Records Codex replay limitations. |
test/node/e2e/harness/agentHostE2ETestHarness.ts |
Adds multi-chat E2E gate. |
test/node/e2e/providers/codexTestConfiguration.ts |
Advertises Codex capabilities. |
test/node/e2e/suites/multiChatSuite.ts |
Gates model-backed scenarios. |
test/node/e2e/suites/sessionPersistenceSuite.ts |
Gates persistence replay coverage. |
test/node/mockAgent.ts |
Adapts mock chat APIs. |
Review details
Suppressed comments (3)
src/vs/platform/agentHost/test/node/e2e/suites/multiChatSuite.ts:98
- The default
enabledalready becomesfalsewhensupportsMultipleChatsE2Eis false, so the later|| RECORDINGcheck can never enable Codex peer tests during recording. Default this parameter only from the product capability and letproviderReplayEnabledapply the replay/recording gate.
function providerTest(title: string, run: Mocha.AsyncFunc, enabled = config.supportsMultipleChats): void {
src/vs/platform/agentHost/node/agentService.ts:1581
- Session creation returns before the authoritative registry write completes. A process exit or write failure immediately after creation leaves a valid provider session absent from all future enumeration. Await this durable registration before returning the created session.
void this._sessionRegistry.register(session, provider.id, Date.now());
src/vs/platform/agentHost/node/agentService.ts:2447
- Deletion returns before unregistering the authoritative registry. If the host exits in that window, the deleted session remains registered and resurfaces after restart. Await the unregister before completing disposal.
void this._sessionRegistry.unregister(session);
- Files reviewed: 29/29 changed files
- Comments generated: 7
- Review effort level: Balanced
Make registry load/write mutations durable and retryable, require successful provider enumeration before marking backfill complete, and unregister before irreversible deletion. Dispose every peer and always run provider-level session finalization before surfacing the first error. Harden Codex workspace-less peer/fork managed-directory ownership across create, release, restore, and disposal; refresh an empty model catalog before validating restored provider-qualified models. Remove unsupported multi-chat capability from ScriptedMockAgent and add regression coverage for every reported failure/retry path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…removal # Conflicts: # .github/skills/sessions/SKILL.md
Use exact chat state routing and retain only the legacy bare-session compatibility path.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This completes the Agent Host multi-chat ownership refactor:
createSessionChat); additional chats use exactcreateChat/forkoperations with transient host-owned context.AgentSessionRegistrydrives top-level enumeration and one-time legacy backfill, removing the host's dependency on I3/default-chat SDK-id reuse.Codex E2E coverage
Codex host-only capability assertions and provider-independent conformance catalog/lifecycle scenarios are enabled and replay green.
Model-backed Codex peer/fork parity remains replay-gated with
supportsMultipleChatsE2E: false/supportsChatForkE2E: false. Focused live recording was attempted without accepting or hand-editing captures, but the current Codex live recording path fails before a usable model response (including the pre-existing simple-turn recording path). The focused reproduction and expected/observed behavior are recorded ine2e/KNOWN_ISSUES.md. Recording mode still enables these tests automatically once that environment defect is fixed.Validation
npm run typecheck-client— 0 errorsnpm run valid-layers-check— passedDesign notes
MULTI_CHAT_ARCHITECTURE.mddocuments the final ownership model, provider-specific I3 compatibility, registry/backfill behavior, multi-root/client-type propagation, and Codex capability status.