mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-21 00:42:58 +01:00
* agentHost: relocate Session ownership into the orchestrator (T2/T4)
Make the orchestrator (AgentService + AgentHostStateManager) own the Session
concept - identity, lifecycle, and grouping - so the agent harness talks only in
chats. Session provisioning stays agent-specific but is now invoked through the
chat surface instead of a Session-typed method, honoring "represent, don't
orchestrate".
- Create: `_provisionSessionViaDefaultChat` allocates the session URI and drives
`chats.createChat(defaultChatUri, { provisionSession })`; the agent's
provisioning runs inside creating the default chat and returns
`IAgentCreateChatResult.provision`.
- Dispose: routes to `chats.disposeChat(defaultChatUri)`.
- Enumerate: `_enumerateProviderSessions` groups `listConversations()` into
sessions via the default-chat URI convention.
Gated per harness by `IAgent.orchestratorOwnsSession` (Codex, Claude, Copilot all
opt in). Storage-preserving: session URIs and the derived `sdkSessionId == session
raw id` (I3) are unchanged, agents read/write the same SDK stores, and
providerData / PEER_CHATS_METADATA_KEY / protocol types are untouched. The legacy
createSession/disposeSession/listSessions remain as the delegated fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: drop orchestratorOwnsSession opt-in; chat surface is the single path
Address review feedback: the agent should not declare an orchestration policy,
and the interface should not carry an optional flag that splits behavior. The
opt-in was transitional scaffolding for a per-agent rollout; all harnesses have
migrated, so remove it and make the orchestrator drive the chat surface
unconditionally.
- Remove `IAgent.orchestratorOwnsSession`; make `listConversations` required.
- `AgentService` always provisions (non-fork/import) / disposes / enumerates
through the chat surface; no per-agent branch.
- Drop the flag from Claude/Copilot/Codex.
- Make both test mocks first-class chat-surface agents (provisionSession bridge,
default-chat disposeChat, listConversations) so their existing createSession/
disposeSession assertions still hold via the bridge.
- Update the routing test to assert session create/dispose now also flow through
the chat surface; refresh the architecture doc.
Storage-preserving; no protocol/data change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: trim verbose comments on the T2/T4 session-ownership code
Shorten the JSDoc/inline comments added for the session-ownership relocation to
1-2 sentences per the coding guidelines; drop obvious per-field comments. No
behavior change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: drop redundant session-typed methods from IAgent (Category C)
Remove `listSessions` and `getSessionMessages` from the IAgent contract - they are
superseded by `listConversations` and `chats.getMessages`. Reroute the one
remaining internal caller (the restore metadata catalog fallback) to
`_enumerateProviderSessions` (which uses `listConversations`). The harnesses keep
those methods privately as the implementation their chat/conversation bridges
delegate to.
`createSession`/`disposeSession` stay on IAgent as the session-lifecycle
provisioning primitives the chat-surface bridge delegates to; `createSession` is
also still used directly for fork/import, whose session id is minted server-side
(sessions.fork) and so cannot fit the orchestrator-allocates-URI seam - left as a
documented follow-up.
No behavior change; storage-preserving.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: rename IAgent.getSessionMetadata to getConversationMetadata
Align the single-item metadata lookup with the chat-addressed conversation
surface: the method is now keyed by a chat URI and returns
IAgentConversationMetadata, mirroring listConversations. All five implementers
(Copilot, Claude, Codex, and both test mocks) derive the session from the chat
URI and return chat-keyed metadata; the orchestrator maps the default-chat URI
back to a session when hydrating restore metadata.
Also reframe the fork/import createSession path in MULTI_CHAT_ARCHITECTURE.md
from a deferred follow-up into a permanent, by-design exception (the fork id is
minted server-side by the SDK, so the orchestrator cannot pre-allocate the URI).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: clarify IAgentConversationMetadata._meta is session-generic by design
Document why the field keeps the SessionMeta alias rather than a
conversation-specific type: _meta is the protocol's open property bag on
SessionState / SessionSummary, carried through verbatim.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: clarify Claude disposeSession takes the agent's own SDK session URI
Document that the session parameter is the provider's own SDK session (the
SDK's terminology), NOT the AH-level Session grouping - that grouping lives in
the orchestrator and the agent only ever deals in chats. The URI backs the
default chat (invariant I3), so chats.disposeChat routes here when a default
chat is disposed; peer chats go to _disposeChat. Teardown disposes that SDK
session plus the peer-chat backings the agent parents under it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: document the AH-session vs SDK-session terminology convention
Add a 'session is overloaded' convention table to the Mental Model section:
in the protocol/orchestrator 'session' means the AH grouping; inside an agent
harness it means the provider's own SDK session (Codex: thread); at the IAgent
seam the session URI is a shared identity (AH-minted, SDK-session-id raw id per
I3). Explains why we do not rename the provider-internal 'session' symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: make IAgent enumeration session-keyed (orchestrator owns session->chat)
Revert the chat-keyed enumeration surface (listConversations /
getConversationMetadata) back to session-keyed listSessions / getSessionMetadata
on IAgent. Chat-keyed enumeration forced every harness to derive default-chat
URIs via buildDefaultChatUri for cold (SDK-discovered) sessions it never created
in-process - re-deriving the session<->default-chat encoding that belongs to the
orchestrator/protocol.
Now each agent returns its own SDK-session identity (AgentSession.uri: provider
scheme + SDK id, no protocol-chat knowledge) and the orchestrator owns the
session->chat mapping. Drops the orchestrator's _conversationToSessionMetadata
bridge (the enumeration/restore round-trip) and deletes the now-unused
IAgentConversationMetadata type.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: stop agents synthesizing default-chat URIs at runtime
Agents no longer call buildDefaultChatUri. Outbound events, default-vs-peer
comparisons, and session-or-chat normalizers now reuse the chat URI the agent
was already given - read back from the session entry's stored defaultChatKey (new
getter on AgentSessionEntry) or the live session's stored chat channel, or tested
with isDefaultChatUri - instead of re-deriving it from the session URI.
The one irreducible conversion (a session URI first born inside the agent: a
freshly forked SDK-assigned id, or a cold-restore/create seed) is centralized in
a single node-layer helper, defaultChatUriForSession, in agentPeerChats.ts. This
is creation-time only; no runtime routing/event path derives chat URIs anymore.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agents: reuse orchestrator default-chat URI on the provision path
The provision (create) path already hands the agent the orchestrator-allocated
default-chat URI via createChat(defaultChatUri, { provisionSession }). Claude and
Codex decoded it to a session and then re-derived the identical URI inside
createSession. Thread the supplied chat URI straight through (createSession's new
optional defaultChat argument) so the agent seeds its entry with the URI it was
handed instead of re-deriving the session-to-default-chat mapping itself.
Copilot has no synchronous create seed (it stores a provisional session and
seeds the default-chat key at materialize/resume), so it has no provision
round-trip to thread; its derivations are the restart-lazy category.
The remaining defaultChatUriForSession callers are the restart-lazy paths (cold
resume, peer-send provisional default, fork/restore materialize) where the
orchestrator supplies no chat URI in-call; documented as the single sanctioned,
irreducible conversion. Behavior is unchanged (the mapping is deterministic);
two Claude tests that deep-equal the emitted URI object are aligned with the
file's toString-based convention since keying now populates the URI's cache.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: flatten provider chat bindings
Make Agent Host own chat membership and pass contextual data only for individual operations. Providers route exact chats to their SDK conversations without deriving default or peer roles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: make chat lifecycle exact and retry-safe
Post-merge repairs and review follow-ups for the AH-owned multi-chat
architecture, keeping providers on exact chat-to-SDK bindings:
- Make `IAgentChats.releaseChat` mandatory so AgentService has one
exact-chat release path with no optional legacy fallback; Claude,
Copilot, Codex, and the test agents implement it explicitly.
- Copilot chat disposal now propagates SDK deletion failures (preserving
routing/state for retry) but tolerates an already-deleted session via an
O(1) `getSessionMetadata` recheck, keeping a partially-completed
multi-chat teardown retry-safe.
- Claude: gate materialization on post-await cancellation, abort every
live session's controller on dispose, and restore session-addressed
resume without inferring chat membership from the URI.
- Rename `IAgentCreateChatOptions.provisionSession` to `newSession` to
state intent (this createChat creates the owning session).
Verified typecheck, transpile, AgentService/Claude/Copilot/Codex unit
suites, valid-layers, and hygiene.
* agentHost: restore subagent transcripts via the chat-surface getMessages
Copilot's `chats.getMessages` routes to `_getChatMessages`, which lacked the
subagent-session-URI branch that only lived in the now-orphaned
`getSessionMessages`. On the persisted replay/restore path the orchestrator
loads a subagent's turns through the chat surface, so reopening a session
rebuilt an empty subagent transcript — failing the "reopening a session keeps
sub-agent messages out of the parent transcript (replay path)" E2E test on all
platforms. Extract a shared `_getSubagentMessages` helper and route subagent
URIs through it from both `_getChatMessages` and `getSessionMessages` (matching
Claude, which already shares one path).
Also address PR review feedback:
- `AgentService._releaseSession` releases every catalog chat even if one
rejects, then propagates the first error (idle eviction has already dropped
the session state, so a skipped leaf would stay resident indefinitely).
- `CopilotAgentSession` stores the host-supplied `IAgentChatContext.resource`
as its persistence scope instead of re-deriving it from the mutable chat
channel via `isDefaultChatUri`, so an explicitly chosen resource survives a
later `bindChatChannel`.
- MULTI_CHAT_ARCHITECTURE.md: correct the flat `IClaudeChatBinding` shape
(`{ sdkSessionId, model? }`, no retained session/storageUri).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: orchestrator owns session provisioning; agents only create chats
Removes the `newSession` seam so an agent no longer distinguishes "a chat for a
new session" from "a chat for an existing session". Session provisioning now
always goes through the agent's dedicated `createSession` + `chats.bindSessionChat`
(the same path fork/import already used), and `chats.createChat` has exactly one
meaning: add an additional chat to an already-provisioned session.
Contract:
- Delete `IAgentProvisionSession`, `IAgentCreateChatOptions.newSession`,
`IAgentProvisionResult`, and `IAgentCreateChatResult.provision`.
- Add `IAgentCreateChatOptions.inheritedContext` ({ workingDirectory, config }):
the orchestrator supplies the owning session's resolved context when creating
an additional chat, so the agent never reads it back from the parent session.
Orchestrator:
- `_createProviderSession` always provisions via `createSession` +
`bindSessionChat`; delete `_provisionSessionViaDefaultChat`.
- `_buildInheritedChatContext` resolves the AH-owned worktree/folder + session
config values and passes them to `chats.createChat`/`fork`.
Agents (Claude, Copilot, Codex):
- Drop the `if (options.newSession)` branch and the `_provisionChat` method; the
chat surface handles additional chats only.
- Claude/Copilot consume `inheritedContext` for the additional-chat working
directory (and Claude for its permission mode) instead of resolving the parent
session; remove the now-dead `_createSession(target)` plumbing where the
provision path was its only caller.
Tests/docs:
- Rewrite the AgentService routing test to assert provisioning via
createSession + bindSessionChat.
- Update MULTI_CHAT_ARCHITECTURE.md §2/§7 to the new seam.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: enable multi-chat for Codex (base for I3-removal branch)
Adds Codex multi-chat support (parity with Claude/Copilot): the
`multipleChats: { fork: true }` capability, `chats.createChat`/`chats.fork`
minting a fresh backing Codex thread per chat, `materializeChat` restore, and a
providerData codec. This is committed as the base of the dedicated I3-removal
branch (it is intentionally not on the PR branch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: add orchestrator-owned session registry (I3 removal stage 1)
Introduce AgentSessionRegistry, a durable, orchestrator-owned index of the
sessions that exist, keyed by session URI and persisted as a JSON blob in a
reserved session database with serialized read-modify-write. Wire it into
AgentService: register on every createSession success and on restoreSession,
unregister on true delete (disposeSession). Add a Stage 1 validation surface
(getRegisteredSessions) plus component and parity unit tests.
This is additive and does NOT yet drive enumeration; listSessions still uses
the provider-derived path. It is the foundation for switching enumeration off
invariant I3 in stage 2.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: enumerate sessions from the registry, not providers (I3 removal stage 2)
Switch AgentService.listSessions to iterate the orchestrator-owned session
registry instead of unioning each provider's listSessions(). Per-session
metadata still comes from the agent's direct getSessionMetadata lookup (I3
keeps the default chat's SDK id == session id, so it resolves), then flows
through the existing DB and state-manager overlays unchanged.
This decouples AH enumeration from the agents' SDK stores: peer-chat backings
and subagent sessions never enter the registry (so they cannot leak as
top-level entries), and a provider that transiently drops a session from its
own snapshot no longer evicts it. Idle provisional sessions are suppressed
explicitly via a new state-manager predicate (isIdleProvisionalSession),
preserving #321269 now that the registry — not the provider snapshot — is the
session source. A one-time, marker-gated backfill seeds the registry from the
legacy provider enumeration so hosts created before the registry keep their
on-disk sessions.
I3 is unchanged; agents are untouched. Adds backfill and transient-drop tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: clarify Codex is already I3-decoupled (I3 removal stage 3a)
Codex's default chat does not actively satisfy I3: a fresh session's raw id is
an AH-minted provisional UUID while its backing thread id is app-server-assigned,
with the real mapping persisted in the per-session metadata overlay. The residual
sessionId == threadId uses (_readSession's ?? sessionId fallback and listSessions'
thread->URI mapping) are legacy-compat shims for pre-existing sessions whose
persisted identity is the thread id; they cannot be removed without a data
migration (disallowed), so Codex is treated as already I3-satisfied.
Comment/doc-only: clarifies the two shim sites and adds a per-agent nuance note
to the I3 invariant in MULTI_CHAT_ARCHITECTURE.md. No behavior change. The active
I3 removal targets are Claude and Copilot.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: collapse fresh-session provisioning into chats.createSessionChat (I3 removal stage 4, step 1)
Add an optional chat-surface entry, chats.createSessionChat, that provisions a
session AND binds its session-backed (default) chat in one call — the
replacement for the IAgent.createSession + bindSessionChat provisioning pair.
The agent reuses the session id as its SDK id (id-reuse kept; no storage
change, no I7 for the default chat). The orchestrator mints the session URI,
derives the default-chat URI, and calls createSessionChat; agents that don't
implement it fall back to the create-then-bind pair.
Claude implements it via its existing { kind: 'chat' } provisioning path (also
used by truncate), so routing state is identical to create-then-bind. Only
fresh sessions collapse: fork and import mint a fresh SDK-assigned session id
inside the agent, so the orchestrator can't know the default-chat URI up front
and keeps them on the create-then-bind pair. bindSessionChat is now documented
as the restore-time counterpart.
Additive and always-green: Copilot/Codex still use createSession. Validated
typecheck, layers, eslint, hygiene; Claude units 206, AgentService 140, Claude
E2E replay 8.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: implement chats.createSessionChat in Copilot and Codex (I3 removal stage 4, step 2)
Both agents now provision a fresh session and bind its session-backed (default)
chat through the collapsed chats.createSessionChat entry, delegating to their
existing _createSession and then binding the default chat (id-reuse; no storage
change, no I7 for the default chat). The orchestrator already prefers this path
for fresh sessions across all providers; fork/import still use createSession.
Validated typecheck, layers, eslint, hygiene; Copilot 347, Codex 47,
AgentService 140 units; E2E replay Copilot 15 / Codex 6 / Claude 8.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: enable Codex multi-chat capability
Advertise Codex multiple-chat and fork support now that the exact chat binding, model-provider forwarding, and registry-owned enumeration paths are complete. Keep provider-owned side chats disabled for Codex.
Add replay-only parity gating for Codex model-backed peer/fork tests. Host-only capability checks and conformance catalog/lifecycle coverage remain enabled; recording mode still runs the gated tests once the documented live Codex recording defect is fixed. No capture files are fabricated or hand-edited.
Validated typecheck, layers, ESLint, Agent Host unit suites, and Claude/Copilot/Codex strict replay.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: allow recording gated multi-chat E2E tests
Keep Codex model-backed peer/fork tests skipped in strict replay while permitting both focused recording modes to execute them and generate fixtures.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: harden session registry and chat lifecycle
Make registry load/write mutations durable and retryable, require successful provider enumeration before marking backfill complete, and unregister before irreversible deletion. Dispose every peer and always run provider-level session finalization before surfacing the first error.
Harden Codex workspace-less peer/fork managed-directory ownership across create, release, restore, and disposal; refresh an empty model catalog before validating restored provider-qualified models.
Remove unsupported multi-chat capability from ScriptedMockAgent and add regression coverage for every reported failure/retry path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: remove Claude default-chat URI inference
Use exact chat state routing and retain only the legacy bare-session compatibility path.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: make chat backings session-neutral
Give every provider an exact default-chat backing, keep Agent Host authoritative for membership and lifecycle, and isolate provider enumeration to legacy discovery.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: dispose legacy Claude sessions
Retain exact default-chat disposal while falling back to an unbound same-ID SDK conversation for direct legacy provider callers.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: route peer session events to owners
Normalize session-scoped progress from exact chat resources, keep Codex peer lifecycle off backing session URIs, and resolve peer configuration through the owning AH session.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: restore Codex peer chat history
Resume cold Codex peer threads before reading their turns and honor the persisted replacement thread ID. Share concurrent resume work with the first send and suppress idle usage notifications emitted during restore.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: update AgentService worktree deletion stub
Use the current prepare/remove worktree deletion contract so durable registry retry coverage reaches the expected cleanup path.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* agentHost: fork the exact Copilot source chat
Pass the orchestrator-owned default chat channel through session forks so Copilot resolves independent SDK backings. Preserve refork support when imported protocol turn IDs already match provider event IDs.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: clarify btw command input
Document that selecting the slash command consumes the command token, so the remaining input must contain only the side question.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Restore persisted subagent chats
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Store orchestrator state separately
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Attach restored peer rejection eagerly
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Cancel session cleanup before revival
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Enforce Agent Host routing channels
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Use plural session working directories
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply provider feedback consistently
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Clarify provider chat backing terminology
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Stop inferring chat role from resource
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify provider chat resolution
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Require exact source chat for forks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Let providers observe session config
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Restore subagent transcripts lazily
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Complete chat-only provider ownership
Route provider provisioning, restore, lifecycle, configuration, and active-client behavior through exact chat-addressed seams. Preserve additive legacy default-chat migration across Claude, Copilot, and Codex and remove obsolete session compatibility paths.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix chat test field initialization
Avoid directly reading the overridden chat surface from subclass field initializers so define-class-fields compilation remains safe.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Keep session chat roles in Agent Host
Move legacy backing selection and session-versus-peer materialization filtering into Agent Host. Providers now recover or materialize exact opaque chat backings without retaining session/peer classifications.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make provider chat creation uniform
Remove provider-visible chat role classification and collapse runtime initialization and additional chat creation into one createChat operation. Document and test the registry backfill's idempotent, coalesced migration behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Flatten provider chat creation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Remove session ownership from agent chats
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address active clients by chat
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make agent provider seams chat-only
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Organize agent provider contract
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Group legacy chat recovery APIs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document agent capability optionality
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Separate agent provider model
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix cold peer-chat fork to read source chat's own persistence resource
Cold peer-chat fork previously read the shared configurationResource
overlay instead of the source chat's own persistence resource, so
inherited model/agent/permissionMode came from the wrong scope for any
non-default source chat. _chatConfigScopes now records both the
configurationResource and the exact resource (IChatScopeBinding) for
each chat, and _bindInheritedConversation reads the source overlay by
the source's own resource. Also adds a backing-model fallback for a
source that was created but never materialized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add regression tests for cold peer-chat fork scope inheritance
Covers two scenarios for the claudeAgent.ts fix (commit aabdead86d):
- a peer chat materialized before a cold restart forks with its own
model/agent/permissionMode/workingDirectories, not the session-wide
decoy overlay
- a peer chat never materialized before a cold restart still recovers
its model via the _chatBackings fallback
Also adds a per-resource-aware ISessionDataService test double, since
the shared sessionTestHelpers.ts fake ignores the resource argument and
returns one flat database for all resources, which would otherwise mask
the session-vs-peer overlay bug.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Complete Agent Host chat ownership migration
Make session registry migration durable, preserve exact chat backings across provider restore and lifecycle paths, and harden deletion and rollback behavior. Rename the architecture spec and add regression coverage across providers, migration, concurrency, and protocol restore.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Restore Agent Host checkpoint lifecycle
* Fix Agent Host CI test portability
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
196 lines
7.3 KiB
TypeScript
196 lines
7.3 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { lookup } from 'dns';
|
|
import { streamToBuffer } from '../../../base/common/buffer.js';
|
|
import { CancellationToken } from '../../../base/common/cancellation.js';
|
|
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
|
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
|
import { ILogService } from '../../log/common/log.js';
|
|
import { IProductService } from '../../product/common/productService.js';
|
|
import { IRequestService, NO_FETCH_TELEMETRY } from '../../request/common/request.js';
|
|
import { IAgentHostNetworkEndpoint } from '../common/agent.js';
|
|
import { IAgentHostDnsResult, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult } from '../common/agentService.js';
|
|
import { IAgentHostProxyResolver } from './agentHostProxyResolver.js';
|
|
|
|
export const INetworkDiagnosticsService = createDecorator<INetworkDiagnosticsService>('networkDiagnosticsService');
|
|
|
|
/**
|
|
* Owns agent-host network connectivity diagnostics: host-level network context
|
|
* ({@link getInfo}) and the per-URL reachability probe ({@link fetch}). Split
|
|
* out from {@link IAgentService} so the network stack dependencies
|
|
* ({@link IRequestService}, {@link IAgentHostProxyResolver}) are injected here
|
|
* rather than threaded through the session orchestrator.
|
|
*/
|
|
export interface INetworkDiagnosticsService {
|
|
readonly _serviceBrand: undefined;
|
|
|
|
/** Host-level network context: version, OS/arch, account, proxy settings/env, and endpoints worth probing. */
|
|
getInfo(endpoints: readonly IAgentHostNetworkEndpoint[], account?: string): Promise<IAgentHostNetworkDiagnosticsInfo>;
|
|
|
|
/** Probe connectivity from the agent host process to a single `url`. */
|
|
fetch(url: string): Promise<IAgentHostNetworkFetchResult>;
|
|
}
|
|
|
|
/** Per-probe timeout: DNS lookup and the reachability request each get this long. */
|
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
|
|
/** Cap on the response body returned to callers (for expected-content checks), to bound the IPC payload. */
|
|
const MAX_BODY_CHARS = 64 * 1024;
|
|
|
|
/**
|
|
* Proxy-related environment variables surfaced in the diagnostics report so a
|
|
* mismatch between the OS/config proxy and an explicit env override is visible.
|
|
*/
|
|
const PROXY_ENV_KEYS = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy'] as const;
|
|
|
|
/** VS Code `http.*` proxy settings surfaced alongside the env vars. */
|
|
const PROXY_CONFIG_KEYS = ['http.proxy', 'http.proxyStrictSSL', 'http.proxySupport', 'http.noProxy'] as const;
|
|
|
|
export class NetworkDiagnosticsService implements INetworkDiagnosticsService {
|
|
|
|
declare readonly _serviceBrand: undefined;
|
|
|
|
constructor(
|
|
@IRequestService private readonly _requestService: IRequestService,
|
|
@IAgentHostProxyResolver private readonly _proxyResolver: IAgentHostProxyResolver,
|
|
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
|
@IProductService private readonly _productService: IProductService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
) { }
|
|
|
|
async getInfo(endpoints: readonly IAgentHostNetworkEndpoint[], account?: string): Promise<IAgentHostNetworkDiagnosticsInfo> {
|
|
const proxyEnv: Record<string, string> = {};
|
|
for (const key of PROXY_ENV_KEYS) {
|
|
const value = process.env[key];
|
|
if (value) {
|
|
proxyEnv[key] = value;
|
|
}
|
|
}
|
|
|
|
const proxySettings: Record<string, string> = {};
|
|
for (const key of PROXY_CONFIG_KEYS) {
|
|
const value = this._configurationService.getValue(key);
|
|
if (value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) {
|
|
continue;
|
|
}
|
|
proxySettings[key] = Array.isArray(value) ? value.join(', ') : String(value);
|
|
}
|
|
|
|
return {
|
|
version: this._productService.version,
|
|
os: process.platform,
|
|
arch: process.arch,
|
|
account,
|
|
proxySettings,
|
|
proxyEnv,
|
|
endpoints,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Probe connectivity from the agent host process to a single `url`. Resolves
|
|
* the proxy (for reporting), performs an IPv4 DNS lookup, and then a
|
|
* reachability request through {@link IRequestService} — so the probe
|
|
* traverses the same proxy / TLS / certificate stack the rest of VS Code
|
|
* uses. Each step is individually timed and never throws; failures are
|
|
* captured on the result.
|
|
*/
|
|
async fetch(url: string): Promise<IAgentHostNetworkFetchResult> {
|
|
const target = new URL(url);
|
|
const host = target.hostname;
|
|
|
|
// DNS: resolve both address families so a host that only answers on one is visible.
|
|
const [dnsIpv4, dnsIpv6] = await Promise.all([
|
|
resolveDns(host, 4),
|
|
resolveDns(host, 6),
|
|
]);
|
|
|
|
// Proxy resolution (for reporting; IRequestService resolves its own proxy internally).
|
|
let proxyUrl: string | undefined;
|
|
try {
|
|
proxyUrl = await this._proxyResolver.resolveProxy(url);
|
|
} catch (err) {
|
|
this._logService.debug(`[AgentHost] Network diagnostics: proxy resolution for ${url} failed: ${errorMessage(err)}`);
|
|
}
|
|
|
|
const base = {
|
|
url,
|
|
proxyUrl,
|
|
dnsIpv4, dnsIpv6,
|
|
};
|
|
|
|
// Reachability: a GET through IRequestService, which applies VS Code's proxy,
|
|
// strictSSL, and certificate handling — the path the rest of VS Code uses.
|
|
const probeStart = Date.now();
|
|
try {
|
|
const context = await this._requestService.request({
|
|
url,
|
|
type: 'GET',
|
|
timeout: PROBE_TIMEOUT_MS,
|
|
callSite: NO_FETCH_TELEMETRY,
|
|
}, CancellationToken.None);
|
|
const body = (await streamToBuffer(context.stream)).toString();
|
|
return {
|
|
...base,
|
|
statusCode: context.res.statusCode,
|
|
body: body.length > MAX_BODY_CHARS ? body.slice(0, MAX_BODY_CHARS) : body,
|
|
durationMs: Date.now() - probeStart,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
...base,
|
|
error: errorMessage(err),
|
|
durationMs: Date.now() - probeStart,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
function dnsLookup(host: string, family: 4 | 6): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
lookup(host, { family }, (err, address) => err ? reject(err) : resolve(address));
|
|
});
|
|
}
|
|
|
|
async function resolveDns(host: string, family: 4 | 6): Promise<IAgentHostDnsResult> {
|
|
const start = Date.now();
|
|
try {
|
|
const address = await withTimeout(dnsLookup(host, family), PROBE_TIMEOUT_MS);
|
|
return { address, durationMs: Date.now() - start };
|
|
} catch (err) {
|
|
return { durationMs: Date.now() - start, error: errorMessage(err) };
|
|
}
|
|
}
|
|
|
|
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error(`Timed out after ${ms / 1000}s`)), ms);
|
|
promise.then(
|
|
value => { clearTimeout(timer); resolve(value); },
|
|
err => { clearTimeout(timer); reject(err); },
|
|
);
|
|
});
|
|
}
|
|
|
|
function errorMessage(error: unknown): string {
|
|
const seen = new Set<unknown>();
|
|
function collect(error: unknown): string {
|
|
if (seen.has(error)) {
|
|
return '';
|
|
}
|
|
seen.add(error);
|
|
if (!(error instanceof Error)) {
|
|
return String(error);
|
|
}
|
|
const details = [
|
|
error.cause ? collect(error.cause) : '',
|
|
...(error instanceof AggregateError ? error.errors.map(collect) : []),
|
|
].filter(Boolean).join(', ');
|
|
return details ? `${error.message}: ${details}` : error.message;
|
|
}
|
|
return collect(error);
|
|
}
|