mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-03 13:06:06 +01:00
b10844efce
* 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>