Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6a14f61
agentHost: support named fork titles
DonJayamanne Oct 6, 2026
f2c356d
chat: update fork test for connections service
DonJayamanne Oct 6, 2026
6c903d8
agentHost: support prompted forks
DonJayamanne Oct 8, 2026
bdae0fa
agentHost: adapt fork title test to utility model service
DonJayamanne Oct 12, 2026
ebab00d
chat: distinguish fork completion descriptions
DonJayamanne Oct 13, 2026
b19e54e
Merge origin/main into fork prompt exploration
DonJayamanne Oct 13, 2026
508c2b9
chat: preserve fork context and drafts across asynchronous creation
DonJayamanne Oct 13, 2026
30baf6d
Fix fork CI regressions after upstream sync
DonJayamanne Oct 13, 2026
29e29e2
agentHost: drain session metadata before idle eviction
DonJayamanne Oct 13, 2026
d69e571
agentHost: align fork titles and errors with protocol review
DonJayamanne Oct 13, 2026
9d35ae7
agentHost: drop changes unrelated to named and prompted forks
DonJayamanne Oct 13, 2026
8ec1b77
Merge remote-tracking branch 'origin/main' into copilot/fork-prompt-e…
DonJayamanne Oct 13, 2026
19b175d
chat: surface the real cause when a prompt fork fails
DonJayamanne Oct 13, 2026
4aeb97e
agentHost: persist only rejections the submitter cannot recover
DonJayamanne Oct 13, 2026
b7e3b62
Revert "agentHost: drop the session metadata drain"
DonJayamanne Oct 13, 2026
b2840e5
agentHost: drain session catalog writes before idle release
DonJayamanne Oct 14, 2026
2bc40eb
Merge origin/main into copilot/fork-prompt-exploration-vscode
DonJayamanne Oct 14, 2026
2e89653
Merge main into copilot/fork-prompt-exploration-vscode
DonJayamanne Oct 16, 2026
c832471
Merge latest main into copilot/fork-prompt-exploration-vscode
DonJayamanne Oct 16, 2026
7b32658
test: include Claude default multi-root capability
DonJayamanne Oct 17, 2026
593d8c8
Merge main into copilot/fork-prompt-exploration-vscode
DonJayamanne Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/vs/platform/agentHost/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ Artifact tools read and mutate only the invoking chat's artifacts. The session d

### UI/provider layer (`sessions/services/sessions/common/session.ts:ISessionCapabilities`)

- 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`).
- Protocol `AgentCapabilities` (`multipleChats?: { fork?: boolean; sideChat?: boolean }`) flows from `AgentInfo.capabilities` (protocol) through the provider adapter. `ISession.capabilities` (`ISessionCapabilities`) exposes `supportsMultipleChats` and `supportsFork`, derived from `multipleChats` and `multipleChats.fork`; both flow into VS Code context keys (`sessionContextKeys.ts:SessionSupportsMultipleChatsContext`, `SessionSupportsForkContext`). Forks can carry the optional `createChat.title` and existing `createChat.initialMessage` without additional capabilities. VS Code preserves a supplied title on first publication and reports initial-message errors in the new chat.
- UI actions read context keys — no provider-id switches.

---
Expand Down Expand Up @@ -352,7 +352,7 @@ interface AgentCapabilities {
}
```

The 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`).
The agent declares these in `getDescriptor().capabilities` (`common/agent.ts:IAgentDescriptor`). `multipleChats` and `multipleChats.fork` flow to the UI as `ISessionCapabilities.supportsMultipleChats` and `supportsFork` (`sessions/services/sessions/common/session.ts`) and are bound to context keys (`sessions/services/sessions/common/sessionContextKeys.ts:SessionSupportsMultipleChatsContext`, `SessionSupportsForkContext`). Optional initial titles require no separate capability.

UI 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).

Expand Down Expand Up @@ -538,7 +538,7 @@ Claude deliberately has no AH-session container and no membership/role concept o
- `_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`).
- `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`.

Every 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 }`.
Every 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, sideChat: true }`.

Each 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.)

Expand All @@ -551,7 +551,7 @@ Copilot also has no AH-session container:
- 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.
- The backing records preserve the existing `providerData` codec and one-time `copilot.chats` migration.

No `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 }`.
No `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, sideChat: true }`.

Copilot managed-settings queries share only in-flight work for the same runtime client, credential and working directory. The runtime owns policy freshness, composition, cache fallback and strict-refresh failures; the host never substitutes an unmanaged or stale snapshot. Catalog refresh and pre-session config discovery warm this path. After 3.5 seconds the host reports standard AHP progress without failing the pending operation; the overall query is bounded at 30 seconds, and timeout releases cancellation listeners even if the RPC never settles. Editor and Agents windows display indeterminate progress, including remote hosts. Cancelling the last waiter dismisses progress, while a later waiter can still join the bounded query. Policy failures are retryable `policyResolutionFailed` errors, including configuration and creation failures before the first turn starts. Their classification survives IPC and JSON-RPC; the chat error's Retry button resends the saved request as a new turn, never resumes a runtime turn that did not start. Connection failures retain their existing crash-recovery classification. Credential, endpoint and client-generation changes invalidate pending results. Stop cancels both queued model and agent selection, including their initial create/resume policy waits, without applying late results or blocking the next queued operation. Model-configuration reporting uses the session lifetime, independent of turn cancellation. The separate diagnostics probe retains its short deadline.

Expand Down Expand Up @@ -620,7 +620,7 @@ When a restored Agent Host conversation's intended model is missing from the pic

Metadata and history reads use `thread/read` and `thread/turns/list` without resuming the thread or claiming its writer lock, so a thread still open in another Codex client remains readable. Subscribing to an interactive chat invokes the optional provider-internal `IAgentChats.prepareChat` seam before returning the snapshot. Codex resumes an existing backing to acquire its writer lock without sending a turn; fresh chats remain lazy. The host publishes temporary input restrictions through existing read-only chat interactivity and validated `vscode.chatInputState` session metadata. These restrictions are not persisted as permanent read-only state. Clients retain history and the editable draft; Retry refreshes the existing subscription without sending a turn or discarding pending local actions. No additional AHP method or capability is required. Sends still apply pending launch configuration and replace a never-persisted backing only when the SDK reports that its rollout is missing. Reads still retry when their app-server connection is replaced.

An additional chat is backed by a **fresh top-level thread minted eagerly** in `chats.createChat` (via `thread/start` or `thread/fork` at the requested turn, reusing `_forkSession`). For these internal peer backings only, the backing entry and URI are keyed by the app-server-assigned thread id. This does not couple the parent AH session id to its default thread id; it gives the peer-chat-backing marker a stable `codex:/<threadId>` database across restart. `_createChat`/`fork` therefore return `backingSession: AgentSession.uri(this.id, threadId)` so the orchestrator suppresses that backing from the top-level session list (invariant I7), plus an opaque `providerData` blob (the backing thread id + model) that `materializeChat` decodes on restore. The additional chat inherits the parent session's working directory, model, and permissions. Exact disposal/release affects only the addressed chat's own thread — there is no cascade between chats of the same session. The persisted `codex.threadId`, `codex.cwd`, and `codex.model` keys and app-server protocol are unchanged, and Codex still never recognizes or derives a default-chat URI. The orchestrator registry contains the parent AH session, not these chat backing URIs; provider-owned discovery pushes external chats through `onDidDiscoverChats`. Capabilities are `multipleChats: { fork: true }`.
An additional chat is backed by a **fresh top-level thread minted eagerly** in `chats.createChat` (via `thread/start` or `thread/fork` at the requested turn, reusing `_forkSession`). For these internal peer backings only, the backing entry and URI are keyed by the app-server-assigned thread id. This does not couple the parent AH session id to its default thread id; it gives the peer-chat-backing marker a stable `codex:/<threadId>` database across restart. `_createChat`/`fork` therefore return `backingSession: AgentSession.uri(this.id, threadId)` so the orchestrator suppresses that backing from the top-level session list (invariant I7), plus an opaque `providerData` blob (the backing thread id + model) that `materializeChat` decodes on restore. The additional chat inherits the parent session's working directory, model, and permissions. Exact disposal/release affects only the addressed chat's own thread — there is no cascade between chats of the same session. The persisted `codex.threadId`, `codex.cwd`, and `codex.model` keys and app-server protocol are unchanged, and Codex still never recognizes or derives a default-chat URI. The orchestrator registry contains the parent AH session, not these chat backing URIs; provider-owned discovery pushes external chats through `onDidDiscoverChats`. Capabilities are `multipleChats: { fork: true, sideChat: true }`.


---
Expand Down
7 changes: 7 additions & 0 deletions src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2067,9 +2067,16 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
}

async createChat(session: URI, chat: URI, options?: IAgentCreateChatRequestOptions): Promise<void> {
const initialMessage = options?.initialMessage && {
...options.initialMessage,
...(options.initialMessage.model === undefined && options.model !== undefined ? { model: options.model } : {}),
...(options.initialMessage.agent === undefined && options.agent !== undefined ? { agent: options.agent } : {}),
};
await this._sendRequest('createChat', {
channel: session.toString(),
chat: chat.toString(),
...(options?.title !== undefined ? { title: options.title } : {}),
...(initialMessage ? { initialMessage } : {}),
...(options?.workingDirectories !== undefined && !options.fork
? { workingDirectories: options.workingDirectories.map(directory => fromAgentHostUri(directory).toString()) }
: {}),
Expand Down
13 changes: 12 additions & 1 deletion src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,11 @@ export interface IAgentCreateChatOptions {
* carries no destination the permission layer can check against the scope.
*/
readonly hasScopedEditSurface?: boolean;
/** Optional display title for the new chat. */
/**
* Authoritative initial title supplied to host creation. When present, it
* takes precedence over a generated fork placeholder; explicit provenance
* determines whether automatic title replacement is allowed.
*/
readonly title?: string;
/** Optional model override; defaults to the session's model. */
readonly model?: ModelSelection;
Expand Down Expand Up @@ -898,6 +902,13 @@ export interface IAgentCreateChatOptions {
* {@link IAgentCreateChatOptions} and never receive side-chat provenance.
*/
export interface IAgentCreateChatRequestOptions extends IAgentCreateChatOptions {
/**
* Host-owned first user message submitted after creating the chat. This is
* intentionally excluded from {@link IAgentCreateChatOptions}: providers
* create the chat before the host processes this message through the normal
* client-aware send path.
*/
readonly initialMessage?: Message;
/**
* Create this new chat as a side chat branching from a turn in an existing
* chat (via `/btw`). The host resolves this into {@link fork} before calling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ export type TurnEndReason =
| { readonly kind: 'success' }
| { readonly kind: 'cancelled' }
| { readonly kind: 'error'; readonly error: ErrorInfo; readonly resumable: boolean }
| { readonly kind: 'rejected'; readonly error: ErrorInfo }
/**
* `recoverable` reports whether the submitter still holds the message, as an
* ordinary client does in its composer draft. Only an unrecoverable
* rejection needs to outlive the session in state, because nothing else
* retains what the user asked for.
*/
| { readonly kind: 'rejected'; readonly error: ErrorInfo; readonly recoverable: boolean }
| { readonly kind: 'localCommand' };

/** A terminal turn outcome offered to contributions after a turn ended. */
Expand Down
16 changes: 13 additions & 3 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ export interface IAgentHostIpcConnectionOptions {
/** Configuration key that controls whether AHP JSONL logs are written for agent host transports. */
export const AgentHostAhpJsonlLoggingSettingId = 'chat.agentHost.ahpJsonlLoggingEnabled';

/**
* Trusted connection facts retained by the protocol ingress while it asks the
* host to submit a create-chat initial message. This is deliberately not part
* of the protocol request or provider creation options.
*/
export interface IAgentCreateChatInitialMessageContext {
readonly clientId: string;
readonly telemetryContext: IAgentHostClientTelemetryContext;
}

export type AgentHostDebugLogsArtifactKind = 'archive' | 'directory';
/** Maximum number of files in one Agent Host debug-log artifact. */
export const AGENT_HOST_DEBUG_LOGS_MAX_ENTRIES = 1000;
Expand Down Expand Up @@ -872,7 +882,7 @@ export interface IAgentHostManagementService {
createSessionWithExtensions(config: IAgentCreateSessionConfig): Promise<URI>;
/**
* Local-only compatibility path for chat fields not yet represented by AHP
* `createChat` (`title` and `model`).
* `createChat` (`model`).
*/
createChatWithExtensions(session: URI, chat: URI, options: IAgentCreateChatRequestOptions): Promise<void>;
/** Installs the chat's sandbox CA through trusted local management IPC after user consent, preserving its advertised identity. */
Expand Down Expand Up @@ -1003,7 +1013,7 @@ export interface IAgentService {
* registers the chat in the session's catalog so subscribers observe a
* `session/chatAdded` action. The `chat` URI is the client-chosen channel.
*/
createChat(session: URI, chat: URI, options?: IAgentCreateChatRequestOptions, credentials?: IAgentHostCredentialSource): Promise<void>;
createChat(session: URI, chat: URI, options?: IAgentCreateChatRequestOptions, credentials?: IAgentHostCredentialSource, initialMessageContext?: IAgentCreateChatInitialMessageContext): Promise<void>;

/** Dispose an additional chat created via {@link createChat}. */
disposeChat(session: URI, chat: URI): Promise<void>;
Expand Down Expand Up @@ -1428,7 +1438,7 @@ export interface IAgentConnection {
* client-chosen chat URI (see {@link buildChatUri}). The host adds the
* chat to the session's catalog and publishes `session/chatAdded`.
*/
createChat(session: URI, chat: URI, options?: IAgentCreateChatRequestOptions): Promise<void>;
createChat(session: URI, chat: URI, options?: IAgentCreateChatRequestOptions, initialMessageContext?: IAgentCreateChatInitialMessageContext): Promise<void>;
/** Dispose an additional chat created via {@link createChat}. */
disposeChat(chat: URI): Promise<void>;

Expand Down
12 changes: 12 additions & 0 deletions src/vs/platform/agentHost/common/state/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ The generated protocol includes the Changesets model:
3. If VS Code consumers need short aliases or type guards, update the wrapper files in this directory after the sync.
4. Compile. The generated registry catches missing action/notification version map entries.

### Current targeted backport

The generated copy intentionally carries the optional `CreateChatParams.title` on top of the `.ahp-version` source commit `6eec1b91`. Until the recorded pin advances to an AHP commit containing this field, preserve or reapply this generated delta when syncing. Neither titles nor initial messages require an additional capability; source-based creation still uses the existing `fork` and `sideChat` capabilities.

A reproducible sync from `6eec1b91` must also retain the existing VS Code-only
compatibility deltas: deprecated `SessionChatSummary.archived` and
`ChatIsReadChanged` introduced in `0.10.0`.

### Initial-message transport delta

Host-facing `IAgentCreateChatRequestOptions.initialMessage` uses the existing `CreateChatParams.initialMessage` field and is deliberately not part of provider creation options: the host creates the provider chat first, then processes the message through its normal client-aware admission path. Message failures appear in the target chat's turn state rather than a special creation RPC error. Protocol clients serialize a supplied initial message unchanged except that a missing message `model` or `agent` inherits the corresponding request option. Do not add top-level `model` or `agent` fields to `CreateChatParams`.

## Adding optional fields to existing types

Optional protocol fields are usually backwards-compatible. Add the field in the source protocol repo, sync `protocol/**`, and add wrapper exports only when VS Code code needs them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export interface CreateChatParams extends BaseParams {
* {@link AgentCapabilities.multipleWorkingDirectories}.
*/
workingDirectories?: URI[];
/** Client-selected initial display title for the new chat. */
title?: string;
}

// ─── moveChat ────────────────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,9 @@ function hasSessionExtensions(config: IAgentCreateSessionConfig): boolean {
}

function hasChatExtensions(options: IAgentCreateChatRequestOptions): boolean {
return options.title !== undefined || options.model !== undefined;
// AHP carries the first message's model/agent selection and client identity.
// The management channel cannot preserve that admission path.
return options.model !== undefined && options.initialMessage === undefined;
}

/**
Expand Down
Loading
Loading