Files
vscode/.github
Sandeep Somavarapu b10844efce sessions: support Multi-Chat in the Claude agent-host harness (#323625)
* Support multiple chats for Claude agent-host sessions

Enable a single Claude (agent-host `provider === 'claude'`) session to own
multiple peer chats in the Agents window, matching the Copilot CLI experience.

- ClaudeAgent: add `_chatSessions` map plus `createChat` / `disposeChat` /
  `getChats`, per-chat persistence, lazy resume of restored peer chats, and
  per-chat routing on `sendMessage` / `abortSession` / `changeModel` /
  `changeAgent`. Fork a peer chat from a source chat's SDK conversation at a
  turn, falling back to a fresh chat when the fork anchor can't be resolved.
- Agent-host sessions provider: advertise `supportsMultipleChats` for the
  `claude` logical session type in addition to `copilotcli`.
- Update SESSIONS.md and ClaudeAgent tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* sessions: fix Claude peer-chat signal routing and harden multi-chat lifecycle

Fix additional (peer) chats in Claude agent-host sessions getting stuck in
progress: a peer chat passes its `ahp-chat` channel URI as the session's
`sessionUri`, but `ClaudeAgentSession` derived its routing channel via
`buildDefaultChatUri(sessionUri)`, double-encoding it so the renderer never
matched the channel. Use the chat URI directly when `sessionUri` is already an
`ahp-chat` channel.

Also harden the peer-chat lifecycle per code review:
- serialize all catalog read-modify-write on the parent session id (createChat /
  disposeChat / _updateChatCatalogModel) to avoid lost updates
- hold the per-chat lock across both materialize and send so disposeChat /
  disposeSession serialize against an in-flight turn (no use-after-dispose)
- make _disposeChildChats async + per-chat serialized to avoid zombie entries
- abort provisional peer chats up front during shutdown
- route setPendingMessages steering to peer chats
- shape-guard the persisted catalog model

Refs https://github.com/microsoft/vscode/issues/322776

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* sessions: thread chat channel through setPendingMessages for peer-chat steering

Address CCR feedback: peer-chat steering was non-functional because
`AgentSideEffects._syncPendingMessages` always dispatched the parent session URI
to `agent.setPendingMessages`, so the Claude peer-chat routing branch was never
reached and steering landed on the default chat.

Add an optional `chat?` param to `IAgent.setPendingMessages` (mirroring
sendMessage/abortSession/changeModel), dispatch the chat channel from
`_syncPendingMessages` (undefined for the default chat), and route via it in
ClaudeAgent. Copilot/Codex 3-param implementations remain valid and unchanged.

Adds an AgentSideEffects dispatch test asserting the peer chat URI is forwarded
as the `chat` arg (and is undefined for the default chat).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* sessions: unify Claude session/peer-chat plumbing into one entry container

Address review feedback (connor4312): stop overloading session/chat URIs and
the parallel-map split that special-cased peer-chat dispatch.

- ClaudeAgentSession now takes an explicit `chatChannelUri`; its `sessionUri`
  is always the real session URI and is never a chat URI
  (`isAhpChatChannel(sessionUri)` can no longer be true). Per-chat resources
  (db, overlay, config scope, server-tool advertise) key off a derived
  `_storageUri` so peer chats stay isolated without overloading `sessionUri`.
- Drop the parallel `_chatSessions` map: a single `_sessions` map of
  `ClaudeSessionEntry` containers now holds each session's default chat plus its
  peer chats. Dispatch resolves a chat via `_findChat(session, chat)` / the
  entry, and teardown disposes the whole entry (main + peers) via
  `_teardownEntry`.
- Unify peer-chat message reconstruction with `getSessionMessages` via a shared
  `_reconstructTurns(sdkId, routingUri, primeOn)`; remove the duplicated
  `_getChatMessages`.

No behavior change to storage keying (main -> session URI, peer -> chat URI).
All ClaudeAgent / AgentSideEffects / CopilotAgent / AgentService node tests pass
(425 claude tests).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: add opaque providerData to chat catalog + multi-chat tests

Wave A + gate G-B1 of the multi-chat unification:
- AgentHostStateManager: add an opaque, agent-owned `providerData?: string`
  to peer-chat catalog entries (addChat/restoreChat) plus getChatProviderData.
  Stored verbatim and never parsed; the default chat carries none. This becomes
  the single source of truth for a peer chat's backing-conversation token,
  replacing the agents' private copilot.chats/claude.chats persistence.
- Add characterization tests for the StateManager catalog (default chat, add/
  remove/restore, summary roll-up) and peer-chat + restore round-trip tests for
  CopilotAgent and ClaudeAgent, guarding the upcoming de-dup waves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: orchestrator owns the peer-chat catalog (Wave B de-dup)

Make AgentHostStateManager's catalog the single source of truth for peer
chats, removing the agents' private copilot.chats/claude.chats persistence:

- agentService: restore peer chats by enumerating the orchestrator's own
  catalog (using the opaque providerData blob) instead of agent.getChats;
  call materializeConversation(chatUri, providerData) before getSessionMessages
  so the agent re-attaches its conversation backing; persist providerData on
  createChat and re-persist on onDidChangeConversationData.
- IAgent: createChat returns IAgentCreateChatResult { providerData? };
  add materializeConversation + onDidChangeConversationData.
- CopilotAgent / ClaudeAgent: stop writing their private *.chats catalogs;
  shrink _chatSessions to a live-only map; decode providerData to rebuild the
  chatUri -> sdkSessionId mapping; emit onDidChangeConversationData on per-chat
  model/fork change. A one-time legacy *.chats READ (triggered by an undefined
  providerData blob) migrates in-flight sessions.

Typecheck, valid-layers-check, and the agentService/Copilot/Claude/StateManager
suites (511 tests) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: add scope/conversation IAgent surface + dispatch mapper (gate G-C1)

Introduce the orchestrator-owned scope/conversation vocabulary on IAgent,
additively alongside the legacy (session, chat?) surface (kept as a compat
shim until waves C2-C5 migrate each agent):

- IAgent: add createScope/disposeScope and an IAgentConversations surface
  (createConversation/disposeConversation/getMessages/fork, conversation-
  addressed sendMessage/abort/changeModel/changeAgent).
- AgentService: map feature-level (session, chat) -> (agent, scope,
  conversation) and own default-chat resolution; resolveConversationUri helper.

Typecheck, valid-layers-check, and the AgentService dispatcher suites
(112 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: agents adopt scope/conversation surface (Wave C)

CopilotAgent, ClaudeAgent and CodexAgent now implement the new scope/
conversation IAgent surface (createScope + conversations:
createConversation/disposeConversation/getMessages/fork and conversation-
addressed sendMessage/abort/changeModel/changeAgent), and agentSideEffects
threads it through where straightforward. The legacy (session, chat?) compat
shim is intentionally retained for now; it is removed centrally in gate G-C2.

Typecheck, valid-layers-check, and the Copilot/Claude/Codex/AgentService
suites (563 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: remove legacy (session, chat?) shim from IAgent (gate G-C2)

With every agent migrated to the scope/conversation surface (Wave C), drop
the agent-facing legacy methods — sendMessage(session,chat,...)/createChat/
disposeChat/getChats and the chat?-suffixed abort/changeModel/changeAgent —
leaving only the conversation-addressed surface on IAgent. AgentService,
agentSideEffects and the three agents migrate their remaining call sites; the
mock agent is updated to the new surface. The orchestrator-facing
IAgentService/IAgentConnection (session,chat) API and the wire protocol are
unchanged — they remain the (session,chat) -> conversation mapping boundary.

Net -209 lines. Typecheck, valid-layers-check, and the Copilot/Claude/Codex/
AgentService suites (559 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: add harness spawn-conversation channel + catalog routing (gate G-D1)

Generalize the subagent_started/subagent_completed signals into a first-class
membership channel: IAgent.onDidSpawnConversation({ scope, conversation,
parent? }) / onDidEndConversation(conversation). AgentService subscribes on
provider registration and routes spawned conversations straight into the
chat catalog (addChat/removeChat), so harness-spawned chats (teams, fleet,
subagents) and user-driven chats share ONE catalog path, preserving the
parent relation. Per-agent emission of these events lands in Wave D.

Typecheck, valid-layers-check, and the AgentService suite (107 passing) pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: agents emit spawn events + capability-driven UI gating (Wave D)

- CopilotAgent / ClaudeAgent emit onDidSpawnConversation/onDidEndConversation
  from their subagent/fan-out paths, so harness-spawned chats flow into the
  shared catalog via the G-D1 channel (carrying the parent relation).
- IAgentDescriptor advertises IAgentCapabilities { supportsMultipleChats,
  supportsFork, supportsTeams }; the agent-host sessions provider maps these
  onto ISessionCapabilities instead of the hardcoded
  supportsMultipleChats(logicalSessionType) session-type check, and exposes
  supportsFork/supportsTeams context keys so UI gates generically with no
  per-harness branches.

Typecheck, valid-layers-check, and the agentHost + sessions provider suites
(1725 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: test default-chat rename is restored on restoreSession

Re-add coverage for restoring a default chat's independently-persisted custom
title (customChatTitle:<defaultChatUri>), homed in the dedicated restoreSession
suite using the localService + TestSessionDatabase pattern. A version of this
test arrived via a merge but was misplaced in the createChat suite; this puts it
in the right place. The behavior itself lives in AgentService.restoreSession.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unused supportsTeams capability

The supportsTeams capability was fully plumbed (protocol to agents to
ISessionCapabilities to SessionSupportsTeamsContext) but had zero
consumers: no when-clause and no widget read it. Harness-spawned
teams/subagents surface automatically via onDidSpawnConversation
regardless of any flag, so this was speculative dead weight. Remove all
13 references across 9 files. supportsMultipleChats and supportsFork are
left untouched as they are actually consumed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: unify subagent catalog membership onto the spawn channel (DR1)

Make the spawn-conversation channel the single owner of subagent catalog
membership, removing the duplicate add path:

- AgentSideEffects._handleSubagentStarted no longer calls addChat; it keeps
  only the subagent lifecycle (ChatTurnStarted, _subagentChats tracking, parent
  tool-call Subagent content, buffered-signal drain, teardown).
- AgentService now sequences a subagent_started/subagent_completed signal onto
  the spawn-channel handlers (_onConversationSpawned/_onConversationEnded) via a
  new onDidSessionProgress subscription registered BEFORE the side-effects
  progress listener. This deterministically guarantees the subagent chat exists
  in the catalog before its turn is started, independent of when the agent
  registers its own subagent->spawn bridge (addChat/removeChat are idempotent).
- Extract the subagent-signal -> spawn-event mapping into shared helpers
  (subagentSpawnConversationEvent/subagentEndConversation) reused by the agents'
  bridges and the AgentService sequencer.

Adds a "subagent membership sequencing" suite: exactly one catalog entry with
parent origin/title/started turn regardless of order, buffered inner-signal
drain, and completion teardown. Typecheck, valid-layers, and the agent suites
(567 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: add multi-chat architecture spec

Living architecture spec for the agent-host multi-chat design (scope/session vs
conversation/chat, orchestrator-owned catalog, opaque providerData, unified
spawn channel, capability gating) with mermaid diagrams. Kept in sync with the
implementation, like SESSIONS.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: legacy peer-chat migration (BC1) + Copilot session container (F2)

Two changes to the Copilot/Claude agents and the orchestrator restore path:

BC1 - backward-compatible restore of legacy peer chats: sessions whose
additional chats were persisted only in the old agent-owned copilot.chats /
claude.chats format (no orchestrator peerChats catalog) previously restored
with those chats invisible. AgentService now performs a one-time migration when
the orchestrator catalog is absent (undefined, not []): it enumerates the
agent's legacy chats via a new migration-only IAgent.listLegacyChats, restores
them through the normal catalog path, and writes the peerChats key so the drain
runs once. Fixes the stale JSDoc that claimed a fallback removed in G-C2.

F2 - collapse CopilotAgent's default-vs-peer _sessions/_chatSessions two-map
split into a single _sessions map of a CopilotSessionEntry container (mirroring
ClaudeSessionEntry): the entry holds the default chat plus a nested _peerChats
map. Removes the special-casing Connor flagged on #323625.

Typecheck, valid-layers-check, and the AgentService/Copilot/Claude suites
(570 passing, incl. the migrate-once / empty-catalog / new-format restore
cases) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: rename agent scope/conversation surface to session/chat (N1)

Collapse the agent-facing vocabulary back to session/chat, so the whole stack
(protocol, orchestrator, UI, agents) speaks one language. The scope/conversation
terms were a 1:1 veneer over concepts already named session/chat elsewhere (the
create* methods even returned IAgentCreateSessionResult). The sessionUri vs
chatChannelUri TYPE separation is preserved — this is a naming change only.

- IAgent: createScope/disposeScope -> createSession/disposeSession; the
  conversations surface (IAgentConversations) -> chats (IAgentChats) with
  createChat/disposeChat/getMessages/fork + conversation-addressed send/abort/
  changeModel/changeAgent now chat-addressed; materializeConversation ->
  materializeChat; onDidSpawn/End/ChangeConversation* -> onDidSpawn/End/ChangeChat*.
- Types: IAgentSpawnConversationEvent -> IAgentSpawnChatEvent,
  IAgentConversationDataChange -> IAgentChatDataChange; drop
  IAgentCreateConversationOptions (reuse IAgentCreateChatOptions).
- Helpers: resolveConversationUri -> resolveChatUri and the private
  _*Conversation* members across AgentService/agents renamed to _*Chat*.
- IAgentService/IAgentConnection/protocol/UI names unchanged (already session/chat).
- Reconcile agentSideEffects tests to the renamed chat surface (mock URI
  normalization) and update MULTI_CHAT_ARCHITECTURE.md terms/diagrams.

Typecheck, valid-layers-check, and the agent suites (686 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: align architecture diagram label with chat terminology

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: align multi-chat spec terminology with the session/chat rename

Refine MULTI_CHAT_ARCHITECTURE.md wording after N1: the default chat's backing
SDK *session* (not "SDK chat") is the session, peer chats are backed by their
own sdkSessionId, and clarify the (session, chat) -> (agent, session URI, chat
URI) mapping label and the per-chat state description.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: group refactor-added helpers into logical units (NS1)

Reduce loose top-level exports in common/agentService.ts introduced by the
multi-chat refactor (the pre-existing config/env helpers are left untouched):

- Move resolveChatUri to common/state/sessionState.ts next to its sibling
  chat-URI helpers (buildChatUri/buildDefaultChatUri/isDefaultChatUri/
  parseChatUri) — its logical home.
- Group the subagent signal -> spawn-channel mappers into an
  `export namespace SubagentChatSignal { toSpawnEvent, toEndChat }` (mirroring
  the existing AgentSession namespace), updating the Copilot/Claude bridges and
  AgentService._sequenceSpawnedChat call sites.

Pure move/regroup, no behavior change. Typecheck, valid-layers-check, and the
agent suites (686 passing) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Make ISession.capabilities observable so late-hydrating capabilities reconcile

The agent-host adapter exposed `capabilities` as a live plain getter reading
the connection's root state. When `rootState.agents[].capabilities` hydrated
after a session's first `SessionState`, existing sessions were never
reconciled: a multi-chat catalog processed while `supportsMultipleChats` was
still `false` stayed collapsed to `[defaultChat]`, and the
`supportsMultipleChats`/`sessionSupportsFork` context keys stayed stale because
a plain getter cannot be tracked by the `setActiveSessionContextKeys` autorun.

Change `ISession.capabilities` to `IObservable<ISessionCapabilities>`. The
agent-host adapter derives it from `connection.rootState` (bridged via
`observableFromEvent`) with `derivedOpts` + `structuralEquals`, and re-applies
the last `SessionState` catalog in an autorun when capabilities change. Static
providers wrap their capabilities in `constObservable`; consumers read
`.read(reader)` (context keys) or `.get()` (one-shot).

Adds a regression test and updates SESSIONS.md and the sessions skill.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Keep completed subagent chats live (fix subagent integration tests)

A DR1 regression conflated 'subagent turn completed' with 'chat removed':
a subagent_completed -> removeChat path tore the child subagent chat out of
the catalog on completion, so subscribing to it after the parent turn
completed failed with 'Resource not found'. A completed subagent chat must
stay live and subscribable (merely hidden from listSessions), with its turn
completed via AgentSideEffects.completeSubagentSession; subagent chats are
removed only on session teardown via removeSubagentSessions.

- agentService._sequenceSpawnedChat: handle spawn only (no removal on completion)
- copilotAgent/claudeAgent spawn bridges: stop firing onDidEndChat on completion
- remove now-unused SubagentChatSignal.toEndChat (keep toSpawnEvent)
- keep onDidEndChat as a generic membership-removal hook
- tests: assert the subagent chat survives subagent_completed and that
  completion does not fire onDidEndChat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add non-opaque backingSession to IAgentCreateChatResult

Introduces a first-class, non-opaque backingSession URI on the peer-chat
create result so the orchestrator can correlate and suppress a peer chat's
backing SDK session. Kept distinct from the opaque providerData blob so the
providerData opacity invariant is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Report peer-chat backingSession from Claude and Copilot agents

ClaudeAgent._createChat mints a fresh top-level SDK session per peer chat in
the same store its listSessions enumerates, so it now returns that session as
backingSession for the orchestrator to suppress. CopilotAgent sets it too for
uniformity (harmless — its peer sessions already don't leak).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Filter peer-chat backing sessions from the top-level session list

createChat now stamps a persisted peerChatBacking marker into the backing
session's database, and listSessions drops any enumerated session carrying it
(batched into the existing metadata overlay, mirroring the subagent filter).
Fixes Claude peer chats leaking as separate top-level sessions. Adds a unit
test covering the filter and its persistence across a restart, plus doc
invariant I7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Share peer-chat scaffolding across Claude & Copilot agents

Extract the near-verbatim multi-chat peer scaffolding shared by the Claude
and Copilot agents into a new node-target module
`src/vs/platform/agentHost/node/agentPeerChats.ts`:

- Move the opaque `providerData` codec (`IPersistedChat`,
  `encodeProviderData`, `decodeProviderData`) into the shared module and
  export it. Use Claude's stricter `model` validation, which is a superset
  of Copilot's unconditional cast. Both agents import it and drop their
  private copies.
- Add a generic `AgentSessionEntry<TSession extends IDisposable>` container
  holding the optional default session plus the peer-chat map. Rewrite
  `CopilotSessionEntry` as an empty subclass and `ClaudeSessionEntry` as a
  subclass that narrows `session` to non-optional.

Behavior-identical refactor; existing Agent* suites stay green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Persist migrated legacy peer chats in a single atomic catalog write

`_migrateLegacyPeerChats` wrote the migrated peer chats to the orchestrator
catalog one entry at a time in a loop. Each `_persistPeerChat` is a separate
read-modify-write of `PEER_CHATS_METADATA_KEY`, so after the first write the key
is present containing only the first entry. If the agent-host process crashed
(OS kill, power loss, forced restart) after write 1 but before write N, the
catalog was left partial; on the next restart `_readPersistedPeerChatCatalog`
returns that subset (not undefined), the catalog-present branch short-circuits,
and migration never re-runs -- chats 1..N-1 are lost forever.

Write the whole migrated set in a single atomic `_enqueuePeerChatCatalogWrite`,
so the key is absent before and complete after; no partial catalog can survive a
crash mid-migration. Adds regression tests asserting the full set is persisted in
one write and that a rejected write leaves the key absent (never a subset).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 16:53:16 +00:00
..