

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1<!--2 AGENTS.md3 Living spec — keep in sync with code after each significant change.4 See: node/agentService.ts, node/agentHostStateManager.ts,5 node/claude/claudeAgent.ts, node/copilot/copilotAgent.ts,6 node/codex/codexAgent.ts, node/agentSideEffects.ts,7 common/agent.ts (IAgent, IAgentChats, IAgentCapabilities),8 common/agentService.ts (IAgentService, IAgentConnection).9-->1011# Multi-Chat Architecture1213> **Status: COMPLETE** (2026-07-01)14> All waves A–D and gates G-B1, G-C1, G-C2, G-D1 are done. Codex, Claude, and15> Copilot all use the unified orchestrator path.16>17> Codex advertises `multipleChats: { fork: true }`. Host-only capability checks18> and provider-independent conformance scenarios run in replay; model-backed19> Codex peer/fork parity remains gated by `supportsMultipleChatsE2E` /20> `supportsChatForkE2E` until the documented live-recording defect is fixed.21>22> The *operational* chat surface (send/abort/model/agent/history) is fully23> chat-addressed and uniform across harnesses. Session ownership lives in the24> orchestrator: it drives every harness through the chat-surface seam — see25> [§7 Session Ownership (T2/T4)](#7-session-ownership-t2t4--the-orchestrator-owns-the-session).2627---2829## 1. Mental Model3031### Three distinct concepts3233| Term | What it is | Owner |34|------|-----------|-------|35| **SDK conversation** | A provider-native conversation/thread with its own restore identity and runtime resources. | Agent harness |36| **Chat** | A thread of turns addressed by a chat channel URI. AH owns its URI and membership; the agent owns its SDK backing. | `AgentService` + agent harness |37| **Orchestrator session** | The protocol-visible entity that bundles a session with its chat catalog, state, and persistence. The orchestrator owns the catalog (which chats exist), the default-chat pointer, and all persistence. | `AgentService` + `AgentHostStateManager` |3839### Guiding principles4041- **"Represent, don't orchestrate."** The agent harness creates and drives SDK42 chats; the orchestrator records what exists and routes protocol43 actions. No agent-specific logic leaks into `AgentService` or44 `AgentHostStateManager`.45- **Composition over inheritance.** All harnesses share one membership path46 (`addChat`/`removeChat`), one persistence path (`PEER_CHATS_METADATA_KEY`),47 and one restore path (`registerRestoredChatSummary` + `resolveChatState`).48 Per-harness features are expressed49 through `IAgentCapabilities` flags, not `if (provider === 'claude') ...`50 branches.51- **Single catalog path.** Whether a chat is created by the user ("Add Chat")52 or spawned by the harness (subagent tool call), it enters the catalog through53 exactly one path (`AgentHostStateManager.addChat`). See invariant I4 below.5455### Terminology convention: "session" is overloaded — read it by layer5657The word **session** means two different things depending on which side of the58seam you are on. To avoid confusion, follow this convention:5960| Where | What `session` means | Notes |61|-------|----------------------|-------|62| AHP wire protocol (`common/state/protocol/`) and the orchestrator (`AgentService`, `AgentHostStateManager`) | The **AH session** — the protocol-visible grouping of a default chat plus its peer chats. | This is the vocabulary the generated protocol types pin (`SessionState`, `SessionSummary`, `sessionAdded`, ...); it is immutable and authoritative. |63| Inside an agent harness (`node/claude`, `node/copilot`, `node/codex`) | The agent's **own SDK / provider session** — the provider's native concept (Codex calls it a *thread*). The agent has no notion of the AH grouping; it only ever deals in chats and its own SDK sessions. | Prefer the provider's native term where one exists (Codex "thread"); otherwise spell it out as "SDK session" / "provider session" in comments and local names wherever the two could be confused. |64| The `IAgent` seam (`chats.*` plus chat metadata/configuration events) | Operations receive an exact chat plus opaque persistence/configuration scopes. | Providers never receive AH ownership or chat-role fields. |6566**Why we do not rename the agents' "SDK session" symbols:** the generated67protocol fixes "Session" = AH session across hundreds of references we cannot68change. Provider-internal SDK sessions remain native runtime concepts, while the69chat seam exposes no AH session ownership.7071---7273## 2. Ownership and Layering7475```mermaid76graph TB77 subgraph UI["UI / provider layer (sessions window)"]78 caps["ISessionCapabilities → context keys<br/>(sessionContextKeys.ts)"]79 smgt["ISessionsManagementService"]80 end8182 subgraph Orch["Orchestrator (agent host process)"]83 svc["AgentService<br/>(node/agentService.ts)"]84 stm["AgentHostStateManager<br/>(node/agentHostStateManager.ts)"]85 svc -->|dispatch actions| stm86 stm -->|action envelopes| svc87 end8889 subgraph Agents["Agent harnesses (IAgent)"]90 claude["ClaudeAgent"]91 copilot["CopilotAgent"]92 codex["CodexAgent"]93 end9495 UI -->|"createChat / disposeChat / dispatchAction"| svc96 svc -->|"chats.createChat / fork / sendMessage"| Agents97 Agents -->|"onDidChatProgress / onDidSpawnChat"| svc98 stm -->|state snapshots / envelopes| UI99 Agents -->|"getDescriptor().capabilities"| caps100```101102### Agent layer (`common/agent.ts:IAgent`)103104Responsible for:105- Creating and owning SDK chats (`chats.createChat`, with optional fork input).106- Reading history (`chats.getMessages`).107- Emitting progress signals (`onDidChatProgress`).108- Emitting membership events for harness-spawned chats (`onDidSpawnChat`, `onDidEndChat`).109- Re-attaching a chat's backing on restore (`materializeChat`) — including the session's default chat.110- Advertising static capability flags (`getDescriptor().capabilities`).111112Agents do **not** maintain the chat catalog, persist membership, know whether a chat is the session or a peer, or inject `AgentHostStateManager`. Host facts they genuinely need (subagent origin, session customizations, prompt-cache metadata, session-title changes, active-client chat membership) arrive through typed seams — see §8.113114**File organization rule:** `common/agent.ts` holds the *provider model* — `IAgent` and every type/helper/signal reachable from it (chat lifecycle, create/materialize/legacy-migration payloads, config-resolution parameters, `AgentSignal`/`AgentSession`). `common/agentService.ts` holds the *orchestrator-facing service surface* — `IAgentService`, `IAgentConnection`, `IAgentHostService`, settings/env constants, and diagnostics types. The dependency is one-directional: `agentService.ts` may import from `agent.ts`, but `agent.ts` must never import from `agentService.ts`. `agentService.ts` re-exports the public provider types from `agent.ts` for call-site compatibility; new provider code should import directly from `agent.ts`.115116### Orchestrator layer117118**`AgentService` (`node/agentService.ts`):**119- Owns the `(session, chat)` → `(agent, session URI, chat URI)` mapping.120- Owns `_providers`, `_sessionToProvider`, and `_findProviderForSession` (which falls back through the session URI's scheme when a session was restored without an `AgentService.createSession` call in this process lifetime).121- Owns `AgentSessionRegistry`, the durable source of truth for which sessions exist. `listSessions` enumerates the registry, hydrates each initial chat through `IAgent.getChatMetadata`, and applies the existing DB/state overlays.122- Dispatches user-driven chat lifecycle (`createChat`, `disposeChat`) to `chats.*`.123- Disposes every catalog chat in stable order (peers first, initial chat last); releases every catalog chat on idle eviction.124- Derives the exhaustive per-operation `IAgentChatContext` (persistence scope, opaque configuration scope, catalog origin, host customizations) via the single `createAgentChatContext` helper.125- Supplies complete resolved `IAgentCreateChatOptions` (`workingDirectories`, `project`, provider config, model/agent, active client, and fork/import/side-chat source) on every creation.126- Records side-chat provenance in the catalog but leaves hidden context injection and visible-history filtering to the provider. The source is a stable turn id; active-turn partial response and selected text are immutable creation-time snapshots.127- Passes the full ordered `workingDirectories` set and the initiating `AgentHostClientType` on each send while still supplying transient chat context. Providers launch in index 0, retain additional roots, and attribute usage/telemetry to the correct client surface.128- Persists and restores the orchestrator-owned peer-chat catalog (`PEER_CHATS_METADATA_KEY` in the session database, serialized per session via `_peerChatCatalogWrites`).129- Suppresses a peer chat's separately-enumerable backing SDK session (when `IAgentCreateChatResult.backingSession` is set): marks it via `_markPeerChatBacking` and filters it out of `listSessions` (invariant I7).130- Routes harness-spawned chats into the catalog (`_onChatSpawned`, `_onChatEnded`).131- Owns the restore flow (`restoreSession`, `_restorePeerChats`).132133**`AgentHostStateManager` (`node/agentHostStateManager.ts`):**134- Holds the authoritative in-memory state tree:135 - `_sessionStates: Map<string, ISessionEntry>` — per-session `SessionState` + catalog timestamps.136 - `_chatEntries: Map<string, IChatEntry>` — one entry for every chat catalog137 item. An entry owns its current `ChatSummary`, optional hydrated138 `ChatState`, opaque `providerData`, and (for restored peers) resolver,139 in-flight promise, and invalidation state.140- Owns `_ensureDefaultChat`: creates the default `ChatState` (URI derived deterministically from the session URI via `buildDefaultChatUri`) at create/restore time.141- `addChat`/`registerRestoredChatSummary`/`removeChat`: the paths for live,142 restored, and removed catalog membership.143- `getChatState` is a synchronous, no-I/O peek for reducers and diagnostics.144 Interaction paths use `resolveChatState`, which coalesces one peer's145 materialization, retries failures, and atomically publishes complete state.146- `getChatOrigin` reads a chat's origin from its `ChatSummary`, so a restored147 chat's origin is available before its state is ever hydrated.148- Session-level active-turn tracking via `_sessionsWithActiveTurn` (a set of chat URIs per session, so multi-chat sessions running concurrent turns stay correct).149150### UI/provider layer (`sessions/services/sessions/common/session.ts:ISessionCapabilities`)151152- Protocol `AgentCapabilities` (`multipleChats?: { fork?: boolean }`) flows from `AgentInfo.capabilities` (protocol) through the provider adapter into `ISession.capabilities` (`ISessionCapabilities`), whose `supportsMultipleChats`/`supportsFork` flags derive from the presence of `multipleChats` and `multipleChats.fork`, and from there into VS Code context keys (`sessionContextKeys.ts:SessionSupportsMultipleChatsContext`, `SessionSupportsForkContext`).153- UI actions read context keys — no provider-id switches.154155---156157## 3. Key Invariants158159**I1 — `providerData` is opaque.**160The state-manager-owned `IChatEntry` stores the blob returned by161`chats.createChat` verbatim. Neither `AgentService` nor162`AgentHostStateManager` parses, validates, or mutates it. It is round-tripped163to the agent verbatim on restore via164`materializeChat(chat, context, providerData)`.165166Opaque to the host, but not arbitrary for the provider: whatever id the blob167carries is the *only* handle the provider gets back on the next process, so it168must name the provider's own durable runtime — the key that runtime is169registered and addressed under — and not a transient SDK handle that the170provider decouples from it. Codex's session-backing chat is the worked example:171its runtime keeps the host-minted session id and records its app-server thread172id in a metadata overlay, so a thread-keyed blob would restore the runtime under173an id nothing addresses it by (leaving every notification unroutable) and would174go stale the moment a rematerialization mints a new thread. Where the two175genuinely coincide — a Codex peer chat or fork, whose runtime *is* its thread —176recording the thread id is the same thing as recording the runtime id.177`IAgentCreateChatResult.backingSession` remains the place to name a separately178enumerable SDK conversation (I7); it is not a second id channel for the blob.179180**I2 — `sessionUri` and `chatChannelUri` are never overloaded.**181A session URI (`ahp-copilot://`, `ahp-claude://`, …) identifies a session. A chat channel URI (`ahp-chat://…`) identifies a chat within a session. The two schemes are structurally distinct; `isAhpChatChannel` / `parseDefaultChatUri` / `buildDefaultChatUri` are the only crossing points. Passing a chat URI where a session URI is expected (or vice versa) is a bug.182183**I3 — The default chat uses the same explicit backing contract as every chat.**184The default chat URI is derived from the AH session URI, but its provider identity is opaque `providerData`. Claude and Copilot mint independent SDK ids, return them from `createChat`, and restore them through `materializeChat`; equality with the AH session id is never assumed and there is no identity-reuse bind fallback. Codex persists its explicit thread mapping. AH never depends on provider identity reuse for ownership or enumeration.185186**I4 — Single catalog path (spawn channel).**187Both user-driven chats (`AgentService.createChat` → `addChat`) and harness-spawned chats (`AgentService._onChatSpawned` → `addChat`) go through `AgentHostStateManager.addChat`. The spawn-channel listener is registered **before** `AgentSideEffects` during `registerProvider` (`node/agentService.ts:registerProvider`) to guarantee the chat exists in the catalog before any turn actions arrive for it (DR1 deterministic sequencing).188189**I5 — Orchestrator peer-chat catalog is the restore source of truth (with one-time legacy migration).**190The orchestrator persists additional chats in `PEER_CHATS_METADATA_KEY` and the initial chat's opaque backing in `defaultChatProviderData`. Restore materializes both through the same provider-data contract — `materializeChat` is the *only* way a default chat is re-attached. When a native catalog session has no persisted blob, the provider recovers its backing from the provider-native session id in the Agent Host session URI and returns canonical provider data, which the host persists additively for later restores; an already-canonical blob is never rewritten. A missing additional-chat catalog triggers the one-time `listLegacyChatBackings` migration. Harness-spawned chats remain transient and are re-derived from tool-origin state. `_persistDefaultChatBacking`'s two writes — the `defaultChatProviderData` blob and the default chat's own `_markChatBacking` call (I7) — are independent: a failure persisting the blob is logged and swallowed rather than skipping the backing marker, since the marker is what keeps the default chat's backing session out of the top-level list and must not be held hostage to an unrelated write's success.191192**I6 — `_findProviderForSession` not `_sessionToProvider`.**193The `_sessionToProvider` map is populated only by `AgentService.createSession`. A restored session (alive in the state manager after a host restart but never created in this process) is absent from it. `_findProviderForSession` (`node/agentService.ts:AgentService._findProviderForSession`) falls back to the session URI scheme, which is what makes restored sessions work.194195**I7 — A peer chat's backing SDK session must never surface as a top-level session.**196Some agents store all SDK conversations in one catalog. `IAgentCreateChatResult.backingSession` lets the orchestrator mark any internal chat backing, including the default Claude backing, so continual external-chat discovery never registers it as a top-level AH session. Providers own native enumeration and push candidates through `onDidDiscoverChats`; Agent Host reconciles those candidates against its registry and suppresses separately enumerable internal backings. Existing AH-created rows retain their provenance. Marking a backing session is a durable metadata write on the backing session's own DB (`_markChatBacking`); a transient failure is retried once, and if it keeps failing the session is suppressed from listing/discovery in-process (`_unpersistedChatBackings`) rather than failing the chat creation that triggered it.197198**I8 — Providers are given host facts; they must not re-derive them.**199Everything a provider needs about a chat and its owning session is published on200a typed seam at the call boundary (see §8). Providers do not inject201`AgentHostStateManager` or recover subagent origin or customizations by parsing202a chat URI. New provider code must consume the seams.203204---205206## 3a. Session Registry and External Chat Discovery207208`AgentSessionRegistry` (`node/agentSessionRegistry.ts`) stores `{ sessionUri → { provider, startTime, external, source } }` in the orchestrator-owned `agent-host.db`. `external` is durable provenance: explicitly created Agent Host sessions are `false`; sessions first discovered in a provider-native catalog are `true`. Provider session databases do not duplicate this property.209210`AgentSessionRegistry.list()` reads the registry once and passes every entry through the migration callback supplied by Agent Service. The callback returns a replacement only for legacy entries whose `external` column is `NULL`, resolving them through the `agentHost.workspaceless` classifier. The registry persists all replacements in one transaction and returns the computed list without rereading the database. Migration uses bounded concurrency. Explicit internal registration sources are preserved; externally classified rows become discovery entries.211212`register` takes the resolved provenance and whether to check tombstones. Explicit `AgentService.createSession` calls skip the tombstone check and clear any tombstone for that session URI; restore and discovery calls atomically decline to register if the session is or concurrently becomes tombstoned. An explicit row is never rewritten by catalog discovery. A migration-time host-owned marker can correct a previously discovered row back to internal provenance.213214Providers own discovery lifecycle and push unknown chats with provider-classified provenance through `onDidDiscoverChats`. Claude and Codex classify their unknown native chats as external; Copilot classifies unknown extension-host chats as internal. Agent Service preserves that classification when it additively registers the event payload. Claude and Codex start their initial attempt when the first discovery-event listener is attached, then retry from their own SDK-readiness paths. Ordinary list refreshes never enumerate provider catalogs. Discovery has no migration marker or Copilot migrate-legacy gate. It never prunes a registry row when a provider later omits it and filters subagents and marked internal chat backings.215216Claude and Codex each use one memoized initial path: resolve/download the SDK, enumerate once, classify the native catalog by stored session metadata, then emit only unknown chats as `external: true`. Provider session databases no longer persist a provider-local external property; legacy `claude.external` and `codex.external` values are recognized only as evidence that a chat was known. An empty or absent sidecar remains unknown.217218If a provider cannot enumerate yet, its initial discovery attempt emits nothing; once ready, it emits the resulting chats through `onDidDiscoverChats`. Registry provenance is projected into `IAgentSessionMetadata._meta` with `readSessionExternal` / `withSessionExternal`, and the normal AHP listSessions round trip carries it to the Sessions provider. There is no external-specific UI behavior.219220Legacy registry migration remains a separate `listChatsToMigrate()` contract. It returns only chats known from non-empty provider session metadata, without external provenance, and is gated by durable per-provider/global migration markers. Agent Host writes `agentHost.workspaceless` as either `true` or `false` into every session it creates. Agent Service classifies each migration candidate itself: marker presence means internal, while absence means a known external chat. Extension-host discovery is a separate Copilot flow and emits unknown extension-host chats as internal.221222Provider-private discovery helpers name their concrete source: Claude uses `_listClaudeCodeChats()` / `_emitClaudeCodeChats()`, Codex uses `_listCodexChats()` / `_emitCodexChats()`, and Copilot uses the extension-host names above. Providers filter known session metadata before emitting; Agent Service still performs the authoritative additive registry write and atomic tombstone check.223224For Claude and Codex, migration and discovery partition the same native catalog: migration returns known entries as plain metadata, while discovery emits unknown entries as external. Central `agent-host.db` remains the durable provenance authority.225226---227228## 4. Capabilities Gating229230`AgentCapabilities` (`common/state/protocol/channels-root/state.ts:AgentCapabilities`) is the protocol-level contract:231232```typescript233interface AgentCapabilities {234 // presence (`{}`) signals multi-chat support; absence = unsupported235 multipleChats?: {236 fork?: boolean; // can fork a chat from a turn237 sideChat?: boolean; // can branch hidden context without copied visible history238 };239 multipleWorkingDirectories?: {240 immutablePrimary?: boolean; // index 0 remains the fixed process root241 };242}243```244245The agent declares these in `getDescriptor().capabilities` (`common/agent.ts:IAgentDescriptor`). They flow to the UI as `ISessionCapabilities` (`sessions/services/sessions/common/session.ts`) and are bound to context keys (`sessions/services/sessions/common/sessionContextKeys.ts:SessionSupportsMultipleChatsContext`, `SessionSupportsForkContext`).246247UI code gates "Add Chat" and "Fork" actions on those context keys. No code inside `AgentService` or `AgentHostStateManager` switches on provider id to gate features. `AgentService.createChat` throws synchronously when `!provider.chats` (the structural guard that replaces a capability check in the orchestrator).248249Claude, Copilot, and Codex advertise `multipleChats: { fork: true }`. Codex does250not advertise `sideChat`; side-chat context/restore, subagent E2E, and native251streaming file-creation coverage remain independently disabled and must not be252inferred from its peer-chat/fork support.253254---255256## 5. Diagrams257258### 5a. Ownership/Component259260```mermaid261graph LR262 subgraph SessionsUI["Sessions UI (workbench process)"]263 provider["agentHostSessionsProvider<br/>(copilotChatSessionsProvider)"]264 ctxkeys["context keys<br/>(sessionContextKeys.ts)"]265 end266267 subgraph AHP["Agent Host Process"]268 svc["AgentService"]269 stm["AgentHostStateManager\n• _sessionStates\n• _chatEntries"]270 se["AgentSideEffects"]271 svc --- stm272 svc --- se273 end274275 subgraph Harnesses["Agent Harnesses"]276 claude["ClaudeAgent\n_chatEntriesBySdkId: DisposableMap<sdkId, ClaudeChatEntry>\n_chatBackings: Map<chatUri, backing>"]277 copilot["CopilotAgent\n_chatEntriesBySdkId: DisposableMap<sdkId, CopilotChatEntry>\n_chatBackings: Map<chatUri, backing>"]278 codex["CodexAgent\n_sessions: Map<id, ICodexSession>\n_sessionIdByChatUri: Map<chatUri, id>"]279 end280281 provider -->|"IPC (agentHost channel)"| svc282 svc -->|"IAgentChats.*"| Harnesses283 Harnesses -->|"onDidChatProgress / onDidSpawnChat"| svc284 stm -->|"ActionEnvelope stream"| provider285 provider -->|"capabilities.multipleChats(.fork)"| ctxkeys286```287288### 5b. Sequence: User-Driven Add Chat289290```mermaid291sequenceDiagram292 participant UI as Sessions UI293 participant AS as AgentService294 participant A as IAgent.chats295 participant SM as AgentHostStateManager296297 UI->>AS: createChat(session, chatUri, options?)298 AS->>AS: _findProviderForSession(session)299 AS->>A: chats.createChat(chatUri, session, convOptions)300 A-->>AS: IAgentCreateChatResult { providerData?, backingSession? }301 AS->>SM: addChat(session, chatUri, { providerData })302 SM-->>UI: ActionEnvelope (SessionChatAdded)303 AS->>AS: _persistPeerChat(session, chatUri, providerData)304 Note over AS: enqueued per-session RMW of PEER_CHATS_METADATA_KEY305 opt backingSession set (I7)306 AS->>AS: _markPeerChatBacking(backingSession, chatUri)307 Note over AS: writes peerChatBacking marker into the backing session's DB<br/>so listSessions filters it out308 end309```310311### 5c. Sequence: Harness-Spawned Chat (Subagent via Spawn Channel)312313```mermaid314sequenceDiagram315 participant SDK as Agent SDK316 participant A as IAgent (onDidChatProgress / onDidSpawnChat)317 participant AS as AgentService318 participant SM as AgentHostStateManager319 participant SE as AgentSideEffects320321 SDK->>A: subagent_started signal322 A->>AS: onDidChatProgress(AgentSignal{kind:'subagent_started'})323 Note over AS: _sequenceSpawnedChat (registered BEFORE AgentSideEffects)324 AS->>AS: _onChatSpawned(event)325 AS->>SM: addChat(session, chat, {origin: {kind:Tool, toolCallId}})326 SM-->>AS: ChatSummary327 Note over SE: AgentSideEffects listener fires next, chat already in catalog (DR1)328 SE->>SM: dispatch turn lifecycle actions for the spawned chat329 Note over AS: Spawned chats are NOT persisted to PEER_CHATS_METADATA_KEY\n(transient, re-derived from event log on restore)330```331332On restart, AgentService discovers completed subagents from the already-restored333parent turns and registers metadata-only read-only chat summaries. Their334provider transcripts are resolved through `AgentHostStateManager.resolveChatState`335only when the child chat is subscribed, matching restored peer-chat laziness;336no provider-wide eager child enumeration remains.337338### 5d. Sequence: Restore339340```mermaid341sequenceDiagram342 participant C as Client (subscribe)343 participant AS as AgentService344 participant A as IAgent345 participant SM as AgentHostStateManager346347 C->>AS: subscribe(sessionUri, clientId)348 AS->>AS: restoreSession(sessionUri)349 AS->>AS: read defaultChatProviderData from DB (may be undefined)350 AS->>A: materializeChat(defaultChatUri, context, defaultChatProviderData?)351 A-->>AS: IAgentCreateChatResult | void352 alt no persisted blob and a backing was recovered353 AS->>AS: persist defaultChatProviderData additively (old-DB migration)354 else no persisted blob and nothing recovered355 Note over AS: warn — restore history with no live backing (no bind fallback)356 end357 AS->>A: chats.getMessages(defaultChatUri, context)358 A-->>AS: Turn[]359 AS->>AS: _readPersistedChatTitle(session, defaultChatUri)360 AS->>SM: restoreSession(summary, turns, {draft, defaultChatTitle})361 SM->>SM: _ensureDefaultChat(sessionKey, summary, turns)362 Note over AS: Peer chats: read PEER_CHATS_METADATA_KEY from DB363 alt catalog present (defined)364 loop for each IPersistedPeerChat (in catalog order)365 AS->>SM: registerRestoredChatSummary(session, chatUri, {title, draft, providerData, resolver})366 Note over SM: Retain summary, draft, providerData, and resolver\n(no ChatState yet)367 end368 else catalog absent (undefined) — one-time legacy migration (Copilot only)369 AS->>A: listLegacyChatBackings(configurationResource)370 A-->>AS: {uri, providerData}[]371 loop for each legacy chat372 AS->>SM: registerRestoredChatSummary(session, chatUri, {resolver, providerData})373 Note over SM: Create a retryable entry-owned resolver374 end375 AS->>AS: _persistPeerChat(...) writes PEER_CHATS_METADATA_KEY (drain once)376 end377 AS-->>C: IStateSnapshot378 C->>AS: subscribe(peerChatUri, clientId)379 AS->>SM: resolveChatState(chatUri)380 SM->>AS: invoke entry resolver(providerData?)381 AS->>A: materializeChat(chatUri, context, providerData?)382 AS->>A: chats.getMessages(chatUri, context)383 A-->>AS: Turn[]384 AS->>AS: interleave persisted local turns385 AS-->>SM: resolver result {turns}386 SM->>SM: atomically hydrate current entry summary + draft + turns387```388389Restored peer chats are catalog-only until their entry resolver succeeds. Their390provider backing and history are loaded before the state manager atomically391installs the entry's current summary, persisted draft, and returned turns.392`getChatState` remains a synchronous no-I/O peek; clients that need content use393`resolveChatState`. Failed resolution leaves the summary visible and retryable.394Resolves for one chat coalesce while different chats resolve independently.395Deletion, eviction, disposal, and URI reuse invalidate entries so stale async396work cannot publish state.397398### 5e. The (session, chat) to (agent, session URI, chat URI) Mapping399400```mermaid401graph TD402 A["client dispatch: channel=ahp-chat://session/…/chat/…"]403 B{isAhpChatChannel?}404 C["chatChannel = channel\nsessionChannel = parseRequiredSessionUriFromChatUri(channel)"]405 D["sessionChannel = channel\nchatChannel = undefined"]406 E["agent = _findProviderForSession(sessionChannel)"]407 F["session = sessionChannel (session URI)\nchat = chatChannel (concrete chat channel URI)"]408 A --> B409 B -->|yes| C410 B -->|no| D411 C --> E412 D --> E413 E --> F414 F -->|"chats.sendMessage(chat, …)"| G["agent harness resolves its SDK session\nfrom the concrete chat URI"]415```416417The orchestrator resolves the owning **session** from the session URI for session-scoped work, but passes a concrete **chat channel URI** to `IAgentChats` operations. For the default chat, that is `buildDefaultChatUri(sessionUri)`, not the bare session URI. The provider resolves that concrete chat to its SDK backing; AH does not depend on the backing id matching the session id.418419---420421## 6. Per-Agent Notes422423### Claude (`node/claude/claudeAgent.ts`)424425Claude deliberately has no AH-session container and no membership/role concept of its own:426- `_chatEntriesBySdkId: DisposableMap<string, ClaudeChatEntry>` is the single disposable owner of every live SDK conversation and provides direct SDK-callback routing.427- `_chatBackings: Map<string, IClaudeChatBacking>` maps each exact host-supplied chat URI to only its provider-owned `{ sdkSessionId, model?, sideChat? }` backing data. It deliberately does **not** retain the owning AH session or a storage URI: AH supplies the owning session and persistence/config resource transiently on every operation (`IAgentChatContext`).428- `IClaudeChatBacking` is the source of truth for both live and released chats: releasing a chat drops its `_chatEntriesBySdkId` leaf but keeps the backing data so a later send can cold-resume the corresponding `ClaudeAgentSession`.429430Every chat operation resolves exactly one backing and routes to exactly one live leaf; there is no default-vs-additional branch and no cascade between chats of the same session. An additional chat's send after restart resumes only that chat's `ClaudeAgentSession`. Capabilities remain `multipleChats: { fork: true }`.431432Each additional chat is backed by a fresh top-level SDK session (`sdkSessionId = generateUuid()`) minted in the same global Claude project store that `listSessions` enumerates. `_createChat` therefore returns `backingSession: AgentSession.uri(this.id, sdkSessionId)` so the orchestrator can suppress that backing from the top-level session list (invariant I7); without it the additional chat would leak as a phantom session. The SDK exposes no delete-chat RPC, so `disposeChat` leaves the backing transcript on disk — the orchestrator-owned catalog simply drops the entry so it is never resumed again. (Claude writes no legacy `claude.chats` blob and has no legacy migration: Claude multi-chat shipped only with the orchestrator-owned catalog, so there is nothing to drain. Copilot keeps its own `copilot.chats` migration because `copilot.chats` predates the catalog.)433434435### Copilot (`node/copilot/copilotAgent.ts`)436437Copilot also has no AH-session container:438- `_chatEntriesBySdkId: DisposableMap<string, CopilotChatEntry>` owns every live SDK conversation and its MCP/customization subscriptions.439- `_chatBackings: Map<string, IPersistedChat>` maps each concrete host chat URI to exactly one provider-owned SDK backing record; SDK callbacks route directly through `_chatEntriesBySdkId`.440- Fork/import provisioning binds the exact target chat inside `chats.createChat`, so a create result is never left waiting for a follow-up bind call.441- The backing records preserve the existing `providerData` codec and one-time `copilot.chats` migration.442443No `CopilotSessionEntry`, `AgentSessionEntry`, default-chat URI helper, or sibling cascade remains. Send/history/model/agent/abort/tool/config/dispose/release operations resolve one leaf. Active-client state remains keyed by the owning SDK session where it is genuinely shared, while each live leaf owns its own SDK and MCP lifecycle. Capabilities remain `multipleChats: { fork: true }`.444445### Codex (`node/codex/codexAgent.ts`)446447Codex supports multiple chats per session. Each conversation — the session's448default chat and every additional chat — is a distinct top-level Codex thread,449explicitly bound to the concrete chat URI AH supplies:450- `_sessions: Map<string, ICodexSession>` owns provider-native thread/runtime state. `_sessionIdByChatUri` maps exact chat URIs to those runtime keys and is never used to recover AH membership.451- `_sessionIdByChatUri: Map<string, string>` is the exact chat-operation routing index; unbound chat URIs are rejected.452- `_sessionIdByThreadId` continues to route app-server callbacks by thread id.453- Initializing `chats.createChat` binds a thread to the exact host-supplied chat URI at provisioning time (including restored/forked threads); `materializeChat` re-attaches any chat's backing thread on restore.454- A cold `getChatMetadata` read caches the backing thread's summary, timestamps, and working directories on the live runtime. Later metadata reads return those fields from memory (the app-server may be blocked on a dynamic tool call), so hydrating a runtime must never erase an already-listed session title.455456An additional chat is backed by a **fresh top-level thread minted eagerly** in457`chats.createChat` (via `thread/start` or `thread/fork` at the458requested turn, reusing `_forkSession`). For these internal peer backings only,459the backing entry and URI are keyed by the app-server-assigned thread id. This460does not couple the parent AH session id to its default thread id; it gives the461peer-chat-backing marker a stable `codex:/<threadId>` database across restart.462`_createChat`/`fork` therefore return463`backingSession: AgentSession.uri(this.id, threadId)` so the orchestrator464suppresses that backing from the top-level session list (invariant I7), plus an465opaque `providerData` blob (the backing thread id + model) that `materializeChat`466decodes on restore. The additional chat inherits the parent session's working467directory, model, and permissions. Exact disposal/release affects only the468addressed chat's own thread — there is no cascade between chats of the same469session. The persisted `codex.threadId`, `codex.cwd`, and `codex.model` keys and470app-server protocol are unchanged, and Codex still never recognizes or derives a471default-chat URI. The orchestrator registry contains the parent AH session, not472these chat backing URIs; provider-owned discovery pushes external chats through473`onDidDiscoverChats`. Capabilities are `multipleChats: { fork: true }`.474475476---477478## 7. Session Ownership (T2/T4) — the orchestrator owns the Session479480**Status: implemented — AH owns identity, enumeration, lifecycle, and grouping.**481482Agents expose exact-chat lifecycle and metadata methods for SDK backing data;483they are not the source of protocol-visible membership.484`AgentSessionRegistry` is the durable membership source, and485`AgentHostStateManager` owns each session's chat catalog and default-chat486pointer.487488### The seam489490- **Create.** `AgentService._createProviderSession` mints the AH session URI,491 derives its initial chat URI, resolves complete chat options, and calls492 `chats.createChat`, which493 provisions and binds that chat in one provider call for fresh, fork, and494 import creation. The result preserves provisional /495 `onDidMaterializeChat` / deferred-`sessionAdded` semantics.496- **Fork a session.** The AHP request identifies a source session and turn. The497 protocol adapter derives that session's exact default-chat URI and498 `IAgentCreateSessionConfig.fork.chat` is required at the provider boundary.499 Providers therefore resolve the source backing from the chat rather than500 assuming the Agent Host session id is an SDK conversation/thread id.501- **Add a chat.** `AgentService.createChat` also dispatches to `chats.createChat`,502 supplying the owning session's resolved roots, project, config, and optional503 fork/side-chat source so the agent never reads them back from another chat.504- **Dispose/release.** `AgentService` calls `chats.disposeChat` for every chat,505 peers first and the initial chat last. Providers release shared configuration506 resources when their final exact-chat reference disappears. Idle eviction507 calls `chats.releaseChat`, which remains non-destructive.508- **Config.** Live provider runtimes that react to session config subscribe to509 `IAgentConfigurationService.onDidSessionConfigChange` using their explicit510 config resource. `AgentSideEffects` does not enumerate chats or fan config511 values through provider hooks.512- **Active client.** `AgentSideEffects` calls `getOrCreateActiveClient` once per513 exact chat and client. Providers receive no sibling list (§8c).514- **Enumerate.** `AgentService.listSessions` enumerates515 `AgentSessionRegistry`, asks the registered provider for that exact session's516 metadata via `getChatMetadata`, and applies persisted and live state overlays.517 Provider-owned code activates additive external-chat discovery;518 `listChatsToMigrate` remains the one-time registry migration seam.519520### No provider-side default-chat derivation521522AH supplies the exact chat plus opaque persistence/configuration scopes. Claude523and Copilot record only `chat → SDK conversation`; Codex records only524`chat → thread runtime`. Session-versus-peer decisions remain in Agent Host.525526Provider chat resolution has three valid states:527528| State | Backing | Live runtime | Explicit context |529|---|---|---|---|530| Live exact chat | Present | Present | Optional for chat-only operations |531| Cold exact chat | Present | Absent | Required before operations needing AH owner/storage context |532| Fresh additional chat | Absent | Absent | Required; creation records the returned provider backing |533534`IAgentChatContext.resource` is either the owning session (default-chat storage) or the addressed chat (additional-chat storage); unrelated resources are rejected. When Copilot has both an explicit owner and a live exact-chat runtime, they must agree. Claude and Codex deliberately do not retain AH ownership on provider backing records, so their cold backing resolution relies on the transient owner context instead of attempting to validate or reconstruct membership.535536### Storage-preservation537538All three harnesses use the single `createChat` operation for fresh, fork,539import, and additional-chat provisioning. There is no bind fallback: an initial540chat is re-attached only through `materializeChat`.541The change is storage-preserving: existing session URIs, provider stores,542`providerData`, and `PEER_CHATS_METADATA_KEY` formats are unchanged, and the543one-time `defaultChatProviderData` backfill for old databases is purely544additive. Registry adoption is separate from provider-data545migration.546547### Interface surface548549Provider-native external-chat discovery is pushed through `onDidDiscoverChats`; one-time550registry migration is through `listChatsToMigrate`; direct metadata lookup uses551`getChatMetadata`. Conversation history, provisioning, restoration, and teardown552are all exact-chat-addressed.553554---555556## 8. Host Seams (what a provider is given, and what it must not read)557558Providers are being made pure consumers of host facts. Agent Host derives each559fact once and hands it to the provider at the call boundary; the target is that560no provider injects `AgentHostStateManager` and no provider recovers a host fact561from URI shape. **Status:** the host side is complete — every seam below is562published on every boundary — while the Claude, Codex, and Copilot slices still563inject the state manager and are converted to the seams one at a time. Treat564this section as the contract new and converted provider code must follow.565566### 8a. `IAgentChatContext` — the exhaustive per-operation context567568`AgentService._chatContext` and `AgentSideEffects._chatContext` both delegate to569`node/agentChatContext.ts:createAgentChatContext`, the single derivation. Every570addressed chat operation (create, materialize, send, truncate, dispose, release,571model/agent change, history read, client tool completion) carries:572573| Field | Meaning | Replaces |574|---|---|---|575| `resource` | The provider-owned persistence scope for this exact chat. | `resolveChatUri` in the provider. |576| `configurationResource` | An opaque scope for configuration and other provider resources shared across related chats. | Passing AH ownership into the provider. |577| `origin` | The catalog's `ChatOrigin`, exhaustive across every way a chat comes into existence: `User` for a plain user-created chat and the default chat, `Fork`/`SideChat` with the exact source chat and turn, `Tool` with the spawning chat and tool call for a subagent. | `stateManager.getChatState(chat)?.origin` and `sessionState.chats.find(...)`. |578| `customizations` | The owning session's **last host-published** customization snapshot, including user enablement toggles. Absent (not empty) when the host has published none yet. | `stateManager.getSessionState(session)?.customizations`. |579580Origin is read from the chat's `ChatSummary`, not its `ChatState`: a restored581chat registers its summary before any state exists, so the summary is the one582source populated for restored and spawned chats alike. `addChat` /583`registerRestoredChatSummary` only override the default `User` origin when a584caller supplies one, so a chat is never registered without provenance.585586For a client tool completion the context describes the chat the tool call was587*addressed* to, while the `chat` argument is the host-resolved routing target588(for a subagent, its ancestor chat). That is what makes589`resolveSubagentChatParent(context)` return the real spawn edge.590591Providers read the facts they need through `resolveAgentChatOrigin`,592`resolveSubagentChatParent`, and `resolveAgentHostCustomizations`593(`common/agent.ts`). A subagent is identified by its `Tool` spawn edge,594not by a provider-side role enum or URI shape.595596Fork remains a provider operation because only the provider can clone its597opaque SDK transcript, checkpoints, and event identifiers. Its contract names598only the exact source chat and turn; Agent Host owns source-session lookup and599never passes that membership to the provider.600601### 8b. Session customizations at the update boundary602603`getChatCustomizations(chat, context, hostCustomizations)` receives the host's604**last published snapshot** explicitly, from `AgentService` (create/restore),605`AgentSideEffects._publishSessionCustomizations` (republish), and606`AgentHostSkillCompletionProvider` (slash completions). It is a snapshot to607reconcile against, not a replacement: the provider keeps its own authoritative608view and reapplies the host's enablement decisions on top of it.609610`undefined` means the host has published no snapshot for that session yet —611during creation, or for an unknown/evicted session. That is deliberately612distinct from an empty list, and the host passes `undefined` rather than a613meaningless `[]` so a provider cannot read "no snapshot" as "no614customizations" and clear its reconciled state.615616The contract for provider-internal work that has no host call of its own (a617plugin controller reacting to `onDidRootConfigChange`, an MCP enablement618reconcile) is: **retain the last supplied value and refresh it at the next619boundary**. Every host trigger that can change the list — `RootConfigChanged`,620`SessionCustomizationsChanged`/`Toggled`, an active-client update, a send —621re-enters the provider through one of the seams above, so the retained value is622never more than one host round-trip stale.623624### 8c. Active-client fan-out625626`AgentSideEffects` resolves the exact chat set with `getSessionChatsForFanOut`627and calls `getOrCreateActiveClient(chat, context, client,628hostCustomizations)` once per exact chat. Providers receive no session identity629or sibling list at this seam; each handle controls one client's contribution to630one chat.631632`getSessionChatsForFanOut` returns `undefined` when the host holds no state for633the session, which is **not** the same as "the session has only its default634chat". With no authoritative membership to hand over, the fan-out is skipped635(and logged) instead of inventing one; the client's contribution stays in636session state and is replayed at the next `session/activeClientSet`.637638Membership changes re-enter the same seam: a `session/chatAdded` envelope639fans every current active client into the new exact chat. Client removal is640likewise fanned out as `removeActiveClient(chat, context, clientId)`.641642### 8d. Prompt-cache metadata643644`IAgentHostPromptCache` (`node/agentHostPromptCache.ts`) exposes exactly645`read(session)` / `write(session, state)` over the `vscode.promptCache` `_meta`646slot. `write` re-reads the persisted value first (several live provider sessions647can share one session URI), skips a no-op write, merges rather than replaces648`_meta`, and returns the effective state.649650### 8e. Session-title signal651652`IAgentHostSessionTitleSignal` (`node/agentHostSessionTitleSignal.ts`) fires653`{ provider, session, conversationId, title }`. The provider filter and the654`AgentSession.id` conversation-id derivation happen once, centrally, so a655provider emitting title telemetry needs only this seam.656657### 8f. Session config (already centralized)658659Live provider runtimes that react to session config subscribe to660`IAgentConfigurationService.onDidSessionConfigChange` with their explicit config661resource. `AgentSideEffects` does not enumerate chats or fan config values662through provider hooks.663664Both `IAgentHostPromptCache` and `IAgentHostSessionTitleSignal` are constructed665by `AgentService`, exposed as `agentService.promptCache` /666`agentService.sessionTitleSignal`, and registered in the `agentHostMain` /667`agentHostServerMain` DI containers next to `IAgentHostStateManager`.668669### 8g. Seam → provider read it replaces670671| Provider read | Seam |672|---|---|673| `stateManager.getSessionState(session)?.customizations` | `context.customizations` / `resolveAgentHostCustomizations(context)`, or the `hostCustomizations` argument of `getChatCustomizations` / `getOrCreateActiveClient`. All three carry the host's last published snapshot, and `undefined` means "no snapshot yet", not "no customizations" |674| `stateManager.getChatState(chat)?.origin`, `sessionState.chats.find(...)?.origin` | `context.origin` / `resolveAgentChatOrigin(context)`; for spawn edges `resolveSubagentChatParent(context)` |675| `parseChatUri(chat)?.chatId.startsWith('subagent/')`, `parseSubagentSessionUri(chat)` for routing | `resolveSubagentChatParent(context)` from the host-owned `Tool` origin |676| `isDefaultChatUri(chat)` gates | Host-side filtering of exact-chat materialization receipts; providers emit the addressed chat and do not classify it |677| `buildDefaultChatUri(session)` as an active-client / fan-out default | the required `chats` argument of `getOrCreateActiveClient`, re-sent whenever the catalog grows and withheld entirely while the host has no authoritative membership |678| `stateManager.getSessionSummary(session)?._meta` + `setSessionMeta(...)` for prompt cache | `IAgentHostPromptCache.read` / `.write` |679| `stateManager.onDidChangeSessionTitle` for OTel | `IAgentHostSessionTitleSignal.onDidChangeSessionTitle` |680| `onSessionConfigChanged` / `onChatConfigChanged` provider hooks | `IAgentConfigurationService.onDidSessionConfigChange` |681
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| microsoft/vscodeextensions/copilot/src/platform/authentication/common/AGENTS.md · 189k | AGENTS.md | archsecurityagent-behaviour | 58/100 | 14 days ago | |
| microsoft/vscode.github/instructions/oss.instructions.md · 189k | Copilot instructions | git | 44/100 | 14 days ago | |
| microsoft/vscode.github/copilot-instructions.md · 189k | Copilot instructions | stylearchtypesui+2 | 74/100 | 13 days ago | |
| microsoft/vscode.github/instructions/accessibility.instructions.md · 189k | Copilot instructions | styledo-not | 61/100 | 14 days ago | |
| microsoft/vscode.github/instructions/agentHostTesting.instructions.md · 189k | Copilot instructions | teststyletesting-strategyagent-behaviour | 55/100 | 7 days ago | |
| microsoft/vscode.github/instructions/best-practices.instructions.md · 189k | Copilot instructions | styleui | 60/100 | today | |
| microsoft/vscode.github/instructions/chat.instructions.md · 189k | Copilot instructions | agent-behaviour | 39/100 | today | |
| microsoft/vscode.github/instructions/coding-guidelines.instructions.md · 189k | Copilot instructions | styletypesuidocs | 60/100 | 14 days ago | |
| microsoft/vscode.github/instructions/committing.instructions.md · 189k | Copilot instructions | do-not | 23/100 | 14 days ago | |
| microsoft/vscode.github/instructions/css-best-practices.instructions.md · 189k | Copilot instructions | styleui | 29/100 | 14 days ago | |
| microsoft/vscode.github/instructions/design-philosophy.instructions.md · 189k | Copilot instructions | style | 34/100 | 14 days ago | |
| microsoft/vscode.github/instructions/design-tokens.instructions.md · 189k | Copilot instructions | styledo-not | 65/100 | 14 days ago | |
| microsoft/vscode.github/instructions/interactive.instructions.md · 189k | Copilot instructions | ui | 43/100 | 14 days ago | |
| microsoft/vscode.github/instructions/notebook.instructions.md · 189k | Copilot instructions | no sections | 48/100 | 14 days ago | |
| microsoft/vscode.github/instructions/observables.instructions.md · 189k | Copilot instructions | no sections | 40/100 | 14 days ago | |
| microsoft/vscode.github/instructions/oss-third-party-notices.instructions.md · 189k | Copilot instructions | buildgitdependenciesdeployment+1 | 65/100 | 14 days ago | |
| microsoft/vscode.github/instructions/sessions.instructions.md · 189k | Copilot instructions | no sections | 24/100 | today | |
| microsoft/vscode.github/instructions/source-code-organization.instructions.md · 189k | Copilot instructions | do-not | 73/100 | 14 days ago | |
| microsoft/vscode.github/instructions/telemetry.instructions.md · 189k | Copilot instructions | styletypesdo-not | 65/100 | 14 days ago | |
| microsoft/vscode.github/instructions/tree-widgets.instructions.md · 189k | Copilot instructions | stylearchperformance | 62/100 | 14 days ago |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 68k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 13 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 14 days ago | |
| aaif-goose/gooseAGENTS.md · 53k | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 8 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| deepseek-ai/deepseek-harnessnative/landlock-run/AGENTS.md · 104k | AGENTS.md | setupteststylearch+3 | 100/100 | today | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | today | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 201k | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| elastic/elasticsearchx-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/AGENTS.md · 78k | AGENTS.md | buildtestlint-formatstyle+2 | 100/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/microsoft-vscode-src-vs-platform-agenthost-agents)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.