Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/claudeCodeAgent.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { EffortLevel, McpServerConfig, Options, PermissionMode, Query, SDKUserMessage, SdkPluginConfig } from '@anthropic-ai/claude-agent-sdk';6import Anthropic from '@anthropic-ai/sdk';7import * as l10n from '@vscode/l10n';8import type * as vscode from 'vscode';9import { IChatDebugFileLoggerService } from '../../../../platform/chat/common/chatDebugFileLoggerService';10import { INativeEnvService } from '../../../../platform/env/common/envService';11import { ILogService } from '../../../../platform/log/common/logService';12import { IMcpService } from '../../../../platform/mcp/common/mcpService';13import { IOTelService, type ISpanHandle, SpanStatusCode, type TraceContext } from '../../../../platform/otel/common/index';14import { deriveClaudeOTelEnv } from '../../../../platform/otel/common/agentOTelEnv';15import { CapturingToken } from '../../../../platform/requestLogger/common/capturingToken';16import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';17import { DeferredPromise } from '../../../../util/vs/base/common/async';18import { Disposable, DisposableMap } from '../../../../util/vs/base/common/lifecycle';19import { isWindows } from '../../../../util/vs/base/common/platform';20import { URI } from '../../../../util/vs/base/common/uri';21import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';22import { LanguageModelToolMCPSource } from '../../../../vscodeTypes';23import { IClaudePluginService } from './claudeSkills';24import { ExternalEditTracker } from '../../common/externalEditTracker';25import { buildMcpServersFromRegistry } from '../common/claudeMcpServerRegistry';26import { dispatchMessage, KnownClaudeError } from '../common/claudeMessageDispatch';27import { IClaudeRuntimeDataService } from '../common/claudeRuntimeDataService';28import { ClaudeSessionUri } from '../common/claudeSessionUri';29import { IClaudeToolPermissionService } from '../common/claudeToolPermissionService';30import { IClaudeCodeSdkService } from './claudeCodeSdkService';31import { ClaudeLanguageModelServer } from './claudeLanguageModelServer';32import { resolvePromptToContentBlocks } from './claudePromptResolver';33import { ClaudeSettingsChangeTracker } from './claudeSettingsChangeTracker';34import { ParsedClaudeModelId } from '../common/claudeModelId';35import { IClaudeSessionStateService } from '../common/claudeSessionStateService';36import { ClaudeOTelTracker } from './claudeOTelTracker';3738// Manages Claude Code agent interactions and language model server lifecycle39export class ClaudeAgentManager extends Disposable {40private _langModelServer: ClaudeLanguageModelServer | undefined;41private _sessions = this._register(new DisposableMap<string, ClaudeCodeSession>());4243private async getLangModelServer(): Promise<ClaudeLanguageModelServer> {44if (!this._langModelServer) {45this._langModelServer = this.instantiationService.createInstance(ClaudeLanguageModelServer);46await this._langModelServer.start();47}4849return this._langModelServer;50}5152constructor(53@ILogService private readonly logService: ILogService,54@IInstantiationService private readonly instantiationService: IInstantiationService,55) {56super();57}5859public async handleRequest(60claudeSessionId: string,61request: vscode.ChatRequest,62stream: vscode.ChatResponseStream,63token: vscode.CancellationToken,64isNewSession: boolean,65yieldRequested?: () => boolean66): Promise<vscode.ChatResult> {67try {68const langModelServer = await this.getLangModelServer();6970this.logService.trace(`[ClaudeAgentManager] Handling request for sessionId=${claudeSessionId}.`);71let session = this._sessions.get(claudeSessionId);72if (session) {73this.logService.trace(`[ClaudeAgentManager] Reusing Claude session ${claudeSessionId}.`);74} else {75this.logService.trace(`[ClaudeAgentManager] Creating Claude session for sessionId=${claudeSessionId}.`);76session = this.instantiationService.createInstance(ClaudeCodeSession, langModelServer, claudeSessionId, isNewSession);77this._sessions.set(claudeSessionId, session);78}7980await session.invoke(81request,82stream,83yieldRequested,84token,85);8687return {};88} catch (invokeError) {89// Check if this is an abort/cancellation error - don't show these as errors to the user90const isAbortError = invokeError instanceof Error && (91invokeError.name === 'AbortError' ||92invokeError.message?.includes('aborted') ||93invokeError.message?.includes('cancelled') ||94invokeError.message?.includes('canceled')95);96if (isAbortError) {97this.logService.trace('[ClaudeAgentManager] Request was aborted/cancelled');98return {};99}100101this.logService.error(invokeError as Error);102const errorMessage = (invokeError instanceof KnownClaudeError) ? invokeError.message : l10n.t('Claude CLI Error: {0}', invokeError.message);103stream.markdown(l10n.t('Error: {0}', errorMessage));104return {105// This currently can't be used by the sessions API https://github.com/microsoft/vscode/issues/263111106errorDetails: { message: errorMessage },107};108}109}110}111112/**113* Represents a queued chat request waiting to be processed by the Claude session114*/115interface QueuedRequest {116readonly request: vscode.ChatRequest;117readonly stream: vscode.ChatResponseStream;118readonly token: vscode.CancellationToken;119readonly yieldRequested?: () => boolean;120readonly deferred: DeferredPromise<void>;121readonly modelId: ParsedClaudeModelId;122readonly permissionMode: PermissionMode;123readonly effort: EffortLevel | undefined;124readonly toolsSnapshot: ReadonlySet<string>;125}126127export class ClaudeCodeSession extends Disposable {128private static readonly GATEWAY_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes129130private _queryGenerator: Query | undefined;131/** The deferred promise that should be resolved when the session should wake up and consume from the queued requests. */132private _pendingPrompt: DeferredPromise<void> | undefined;133/** Requests waiting to be sent to the SDK. */134private _queuedRequests: QueuedRequest[] = [];135/** Requests that have been sent to the SDK and are awaiting completion; index 0 is the request currently being processed. */136private _inFlightRequests: QueuedRequest[] = [];137private _abortController = new AbortController();138private _editTracker: ExternalEditTracker;139private _settingsChangeTracker: ClaudeSettingsChangeTracker;140private _currentModelId: ParsedClaudeModelId | undefined;141private _currentPermissionMode: PermissionMode = 'acceptEdits';142private _currentEffort: EffortLevel | undefined;143private _isResumed: boolean;144private _pendingRestart = false;145private _sessionStarting: Promise<void> | undefined;146private _currentToolNames: ReadonlySet<string> | undefined;147private _gateway: vscode.McpGateway | undefined;148private _gatewayIdleTimeout: ReturnType<typeof setTimeout> | undefined;149private _otelTracker: ClaudeOTelTracker;150151private get _currentRequest(): QueuedRequest | undefined {152return this._inFlightRequests[0];153}154155/**156* Sets the model on the active SDK session, or stores it for the next session start.157*/158private async _setModel(modelId: ParsedClaudeModelId): Promise<void> {159if (modelId === this._currentModelId) {160return;161}162this._currentModelId = modelId;163if (this._queryGenerator) {164const sdkId = modelId.toSdkModelId();165this.logService.trace(`[ClaudeCodeSession] Setting model to ${sdkId} on active session`);166await this._queryGenerator.setModel(sdkId);167}168}169170/**171* Sets the permission mode on the active SDK session, or stores it for the next session start.172*/173private async _setPermissionMode(mode: PermissionMode): Promise<void> {174if (mode === this._currentPermissionMode) {175return;176}177this._currentPermissionMode = mode;178if (this._queryGenerator) {179this.logService.trace(`[ClaudeCodeSession] Setting permission mode to ${mode} on active session`);180await this._queryGenerator.setPermissionMode(mode);181}182}183184constructor(185private readonly langModelServer: ClaudeLanguageModelServer,186public readonly sessionId: string,187isNewSession: boolean,188@ILogService private readonly logService: ILogService,189@IWorkspaceService private readonly workspaceService: IWorkspaceService,190@INativeEnvService private readonly envService: INativeEnvService,191@IInstantiationService private readonly instantiationService: IInstantiationService,192@IClaudeCodeSdkService private readonly claudeCodeService: IClaudeCodeSdkService,193@IClaudeToolPermissionService private readonly toolPermissionService: IClaudeToolPermissionService,194@IClaudeSessionStateService private readonly sessionStateService: IClaudeSessionStateService,195@IClaudeRuntimeDataService private readonly runtimeDataService: IClaudeRuntimeDataService,196@IMcpService private readonly mcpService: IMcpService,197@IClaudePluginService private readonly claudePluginService: IClaudePluginService,198@IOTelService private readonly _otelService: IOTelService,199@IChatDebugFileLoggerService private readonly _debugFileLogger: IChatDebugFileLoggerService,200) {201super();202this._isResumed = !isNewSession;203this._otelTracker = new ClaudeOTelTracker(this.sessionId, this._otelService, this.sessionStateService);204this._debugFileLogger.startSession(this.sessionId).catch(err => {205this.logService.error('[ClaudeCodeSession] Failed to start debug log session', err);206});207this._register({208dispose: () => {209this._debugFileLogger.endSession(this.sessionId).catch(err => {210this.logService.error('[ClaudeCodeSession] Failed to end debug log session', err);211});212}213});214// Initialize edit tracker with plan directory as ignored215const planDirUri = URI.joinPath(this.envService.userHome, '.claude', 'plans');216this._editTracker = new ExternalEditTracker([planDirUri]);217this._settingsChangeTracker = this._createSettingsChangeTracker();218}219220/**221* Creates and configures the settings change tracker with path resolvers.222* Add additional path resolvers here for new file types to track.223*/224private _createSettingsChangeTracker(): ClaudeSettingsChangeTracker {225const tracker = this.instantiationService.createInstance(ClaudeSettingsChangeTracker);226227// Track CLAUDE.md files228tracker.registerPathResolver(() => {229const paths: URI[] = [];230// User-level CLAUDE.md231paths.push(URI.joinPath(this.envService.userHome, '.claude', 'CLAUDE.md'));232// Project-level CLAUDE.md files233for (const folder of this.workspaceService.getWorkspaceFolders()) {234paths.push(URI.joinPath(folder, '.claude', 'CLAUDE.md'));235paths.push(URI.joinPath(folder, '.claude', 'CLAUDE.local.md'));236paths.push(URI.joinPath(folder, 'CLAUDE.md'));237paths.push(URI.joinPath(folder, 'CLAUDE.local.md'));238}239return paths;240});241242// Track settings/hooks files243tracker.registerPathResolver(() => {244const paths: URI[] = [];245// User-level settings246paths.push(URI.joinPath(this.envService.userHome, '.claude', 'settings.json'));247// Project-level settings files248for (const folder of this.workspaceService.getWorkspaceFolders()) {249paths.push(URI.joinPath(folder, '.claude', 'settings.json'));250paths.push(URI.joinPath(folder, '.claude', 'settings.local.json'));251}252return paths;253});254255// Track agent files in agents directories256tracker.registerDirectoryResolver(() => {257const dirs: URI[] = [];258// User-level agents directory259dirs.push(URI.joinPath(this.envService.userHome, '.claude', 'agents'));260// Project-level agents directory261for (const folder of this.workspaceService.getWorkspaceFolders()) {262dirs.push(URI.joinPath(folder, '.claude', 'agents'));263}264return dirs;265}, '.md');266267return tracker;268}269270public override dispose(): void {271this._cancelGatewayIdleTimer();272this._disposeGateway();273this._abortController.abort();274this._inFlightRequests.forEach(req => {275if (!req.deferred.isSettled) {276req.deferred.error(new Error('Session disposed'));277}278});279this._inFlightRequests = [];280this._queuedRequests.forEach(req => {281if (!req.deferred.isSettled) {282req.deferred.error(new Error('Session disposed'));283}284});285this._queuedRequests = [];286this._pendingPrompt?.error(new Error('Session disposed'));287this._pendingPrompt = undefined;288super.dispose();289}290291/**292* Invokes the Claude Code session with a user prompt293* @param request The full chat request294* @param stream Response stream for sending results back to VS Code295* @param yieldRequested Function to check if the user has requested to interrupt296* @param token Cancellation token for request cancellation297*/298public async invoke(299request: vscode.ChatRequest,300stream: vscode.ChatResponseStream,301yieldRequested: (() => boolean) | undefined,302token: vscode.CancellationToken,303): Promise<void> {304if (this._store.isDisposed) {305throw new Error('Session disposed');306}307308this._cancelGatewayIdleTimer();309310// Snapshot per-request metadata from session state311const modelId = this.sessionStateService.getModelIdForSession(this.sessionId);312if (!modelId) {313throw new Error(`Model not set for session ${this.sessionId}. State must be committed before invoking.`);314}315const permissionMode = this.sessionStateService.getPermissionModeForSession(this.sessionId);316const effort = this.sessionStateService.getReasoningEffortForSession(this.sessionId);317const toolsSnapshot = this._computeToolsSnapshot(request.tools);318319// Add this request to the queue with its metadata snapshot320const deferred = new DeferredPromise<void>();321const queuedRequest: QueuedRequest = {322request,323stream,324token,325yieldRequested,326deferred,327modelId,328permissionMode,329effort,330toolsSnapshot,331};332333this._queuedRequests.push(queuedRequest);334335if (!this._queryGenerator) {336await this._startSession(token);337}338339// Wake up the iterable if it's awaiting the next request.340if (this._pendingPrompt) {341const pendingPrompt = this._pendingPrompt;342this._pendingPrompt = undefined;343pendingPrompt.complete();344}345346return deferred.p;347}348349/**350* Starts a new Claude Code session with the configured options.351* Guards against concurrent starts (e.g., from yield restart racing with a new invoke).352*/353private async _startSession(token: vscode.CancellationToken): Promise<void> {354// If a session start is already in progress, wait for it rather than starting a second355if (this._sessionStarting) {356await this._sessionStarting;357return;358}359360const startPromise = this._doStartSession(token);361this._sessionStarting = startPromise;362try {363await startPromise;364} finally {365this._sessionStarting = undefined;366}367}368369private async _doStartSession(token: vscode.CancellationToken): Promise<void> {370const folderInfo = this.sessionStateService.getFolderInfoForSession(this.sessionId);371if (!folderInfo) {372throw new Error(`No folder info found for session ${this.sessionId}. State must be committed before invoking.`);373}374const headRequest = this._queuedRequests[0];375if (!headRequest) {376throw new Error(`No queued request to start session ${this.sessionId} with.`);377}378379// Seed session state from the head request's metadata380this._currentModelId = headRequest.modelId;381this._currentPermissionMode = headRequest.permissionMode;382this._currentEffort = headRequest.effort;383this._currentToolNames = headRequest.toolsSnapshot;384385const { cwd, additionalDirectories } = folderInfo;386387// Build options for the Claude Code SDK388this.logService.trace(`appRoot: ${this.envService.appRoot}`);389const pathSep = isWindows ? ';' : ':';390const mcpServers: Record<string, McpServerConfig> = await buildMcpServersFromRegistry(this.instantiationService) ?? {};391392// Create or reuse the MCP gateway for this session393try {394this._gateway ??= await this.mcpService.startMcpGateway(ClaudeSessionUri.forSessionId(this.sessionId)) ?? undefined;395if (this._gateway) {396for (const server of this._gateway.servers) {397const serverId = server.label.toLowerCase().replace(/[^a-z0-9_-]/g, '_').replace(/^_+|_+$/g, '') || `vscode-mcp-server-${Object.keys(mcpServers).length}`;398mcpServers[serverId] = {399type: 'http',400url: server.address.toString(),401};402}403}404} catch (error) {405const errorMessage = error instanceof Error ? (error.stack ?? error.message) : String(error);406this.logService.warn(`[ClaudeCodeSession] Failed to start MCP gateway: ${errorMessage}`);407}408409// Build plugins from skill directories410const plugins: SdkPluginConfig[] = [];411try {412const pluginLocations = await this.claudePluginService.getPluginLocations(token);413for (const pluginLocation of pluginLocations) {414plugins.push({ type: 'local', path: pluginLocation.fsPath });415}416if (plugins.length > 0) {417this.logService.info(`[ClaudeCodeSession] Passing ${plugins.length} plugin(s) from skill locations`);418}419} catch (error) {420const errorMessage = error instanceof Error ? (error.stack ?? error.message) : String(error);421this.logService.warn(`[ClaudeCodeSession] Failed to resolve skill locations for plugins: ${errorMessage}`);422}423424// Take a snapshot of settings files so we can detect changes425await this._settingsChangeTracker.takeSnapshot();426427const serverConfig = this.langModelServer.getConfig();428const options: Options = {429cwd,430additionalDirectories,431// We allow this because we handle the visibility of432// the permission mode ourselves in the options433allowDangerouslySkipPermissions: true,434abortController: this._abortController,435effort: headRequest.effort,436executable: process.execPath as 'node', // get it to fork the EH node process437// TODO: CAPI does not yet support the WebSearch tool438// Once it does, we can re-enable it.439disallowedTools: ['WebSearch'],440// Use sessionId for new sessions, resume for existing ones (mutually exclusive)441...(this._isResumed442? { resume: this.sessionId }443: { sessionId: this.sessionId }),444// Pass the model selection to the SDK445model: headRequest.modelId.toSdkModelId(),446// Pass the permission mode to the SDK447permissionMode: headRequest.permissionMode,448includeHookEvents: true,449mcpServers,450plugins,451settings: {452env: {453ANTHROPIC_BASE_URL: `http://localhost:${serverConfig.port}`,454ANTHROPIC_AUTH_TOKEN: `${serverConfig.nonce}.${this.sessionId}`,455CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',456USE_BUILTIN_RIPGREP: '0',457PATH: `${this.envService.appRoot}/node_modules/@vscode/ripgrep/bin${pathSep}${process.env.PATH}`,458// Forward OTel configuration to the Claude SDK subprocess459...deriveClaudeOTelEnv(this._otelService.config),460},461attribution: {462commit: '',463pr: '',464},465},466canUseTool: async (name, input) => {467if (!this._currentRequest) {468return { behavior: 'deny', message: 'No active request' };469}470this.logService.trace(`[ClaudeCodeSession]: canUseTool: ${name}(${JSON.stringify(input)})`);471return this.toolPermissionService.canUseTool(name, input, {472toolInvocationToken: this._currentRequest.request.toolInvocationToken,473permissionMode: this._currentPermissionMode,474stream: this._currentRequest.stream475});476},477systemPrompt: {478type: 'preset',479preset: 'claude_code'480},481settingSources: ['user', 'project', 'local'],482stderr: data => this.logService.error(`claude-agent-sdk stderr: ${data}`)483};484485this.logService.trace(`claude-agent-sdk: Starting query`);486this._queryGenerator = await this.claudeCodeService.query({487prompt: this._createPromptIterable(),488options489});490491// Cache runtime data (agents, etc.) for the customization provider.492// Fire-and-forget to avoid blocking session startup — error handling is inside the service.493void this.runtimeDataService.update(this._queryGenerator);494495496// Start the message processing loop (fire-and-forget, but _processMessages497// handles all errors internally via try/catch → _cleanup)498void this._processMessages().catch(err => {499this.logService.error('[ClaudeCodeSession] Unhandled error in message processing loop', err);500});501}502503private async *_createPromptIterable(): AsyncIterable<SDKUserMessage> {504while (true) {505// Wait for a request to be available506while (this._queuedRequests.length === 0) {507this._pendingPrompt = new DeferredPromise<void>();508await this._pendingPrompt.p;509}510const request = this._queuedRequests.shift()!;511512// Check settings file changes when no other request is in flight513if (this._inFlightRequests.length === 0 && await this._settingsChangeTracker.hasChanges()) {514this.logService.trace('[ClaudeCodeSession] Settings files changed, restarting session with resume');515this._queuedRequests.unshift(request);516this._pendingRestart = true;517this._isResumed = true;518return;519}520521// Check non-hot-swappable changes that require a session restart522if (request.effort !== this._currentEffort || !this._toolsMatch(request.toolsSnapshot)) {523this._queuedRequests.unshift(request);524this._pendingRestart = true;525this._isResumed = true;526return;527}528529// Hot-swap model and permission mode on the active session530await this._setModel(request.modelId);531await this._setPermissionMode(request.permissionMode);532533// Mark this request as yielded to the SDK; it becomes the current request.534this._inFlightRequests.push(request);535536// Increment user-initiated message count for this model537// This is used by the language model server to track which requests are user-initiated538this.langModelServer.incrementUserInitiatedMessageCount(request.modelId.toEndpointModelId());539540// Resolve the prompt content blocks now that this request is being handled541const prompt = await resolvePromptToContentBlocks(request.request);542543// Create a capturing token for this request to group tool calls under the request544// we use the last text block in the prompt as the label for the token, since that is most representative of the user's intent545const promptLabel = prompt.filter(p => p.type === 'text').at(-1)?.text ?? 'Claude Session Prompt';546this.sessionStateService.setCapturingTokenForSession(547this.sessionId,548new CapturingToken(promptLabel, 'claude', undefined, undefined, this.sessionId)549);550551// Start OTel tracking for this request552this._otelTracker.startRequest(request.modelId.toEndpointModelId());553554// Emit user_message span event for the debug panel555this._otelTracker.emitUserMessage(promptLabel);556557yield {558type: 'user',559message: {560role: 'user',561content: prompt562},563priority: 'now',564parent_tool_use_id: null,565session_id: this.sessionId,566// NOTE: messageId seems to be in the format request_<uuid> but it doesn't seem567// to be a problem to use as the message ID for the SDK.568uuid: request.request.id as `${string}-${string}-${string}-${string}-${string}`569};570}571}572573/**574* Processes messages from the Claude Code query generator575* Routes messages to appropriate handlers and manages request completion576*/577private async _processMessages(): Promise<void> {578const otelToolSpans = new Map<string, ISpanHandle>();579const otelHookSpans = new Map<string, ISpanHandle>();580const subagentTraceContexts = new Map<string, TraceContext>();581try {582const unprocessedToolCalls = new Map<string, Anthropic.Beta.Messages.BetaToolUseBlock>();583for await (const message of this._queryGenerator!) {584// Mark session as resumed after first SDK message confirms session exists on disk.585// This ensures future restarts (yield, settings change) use `resume` instead of `sessionId`.586if (message.session_id && !this._isResumed) {587this._isResumed = true;588}589590// Skip if no current request (e.g., after yield cleared it)591if (!this._currentRequest) {592this.logService.trace('[ClaudeCodeSession] Skipping message - no current request');593continue;594}595596const currentRequest = this._currentRequest;597598// Check if current request was cancelled599if (currentRequest.token.isCancellationRequested) {600throw new Error('Request was cancelled');601}602603// Track OTel metrics from SDK messages604this._otelTracker.onMessage(message, subagentTraceContexts);605606this.logService.trace(`claude-agent-sdk Message: ${JSON.stringify(message, null, 2)}`);607608let result;609try {610result = this.instantiationService.invokeFunction(dispatchMessage, message, this.sessionId, {611stream: currentRequest.stream,612toolInvocationToken: currentRequest.request.toolInvocationToken,613editTracker: this._editTracker,614token: currentRequest.token,615}, {616unprocessedToolCalls,617otelToolSpans,618otelHookSpans,619parentTraceContext: this._otelTracker.traceContext,620subagentTraceContexts,621});622} catch (dispatchError) {623this.logService.warn(`[ClaudeCodeSession] Failed to dispatch message (stream may be disposed after yield): ${dispatchError}`);624}625626if (currentRequest.yieldRequested?.()) {627this.logService.trace('[ClaudeCodeSession] Yield requested - signaling session completion so next request can start');628629// Complete the current request gracefully but don't kill the session630if (!currentRequest.deferred.isSettled) {631await currentRequest.deferred.complete();632}633}634635if (result?.requestComplete) {636// End the invoke_agent span for this request637this._otelTracker.endRequest();638// Clear the capturing token so subsequent requests get their own639this.sessionStateService.setCapturingTokenForSession(this.sessionId, undefined);640const completed = this._inFlightRequests.shift();641if (completed && !completed.deferred.isSettled) {642await completed.deferred.complete();643}644if (this._inFlightRequests.length === 0 && this._queuedRequests.length === 0) {645this._startGatewayIdleTimer();646}647subagentTraceContexts.clear();648}649}650// Generator ended normally - clean up so next invoke starts fresh651throw new Error('Session ended unexpectedly');652} catch (error) {653// Graceful restart: the prompt iterable detected a non-hot-swappable change654// (effort or tools). Preserve queued requests and start a fresh session.655if (this._pendingRestart) {656this._pendingRestart = false;657this._restartSession();658const headToken = this._queuedRequests[0]?.token;659if (headToken) {660await this._startSession(headToken);661}662return;663}664665// Clear the capturing token so it doesn't leak across sessions or error boundaries666this.sessionStateService.setCapturingTokenForSession(this.sessionId, undefined);667// End invoke_agent span with error if still open668this._otelTracker.endRequestWithError(error.message);669670// Resets session state so the next session start can begin fresh.671// Preserves the sessionId for SDK resume.672673this._queryGenerator = undefined;674this._abortController = new AbortController();675676// Rejects all pending requests and clears the queues.677678this._inFlightRequests.forEach(req => {679if (!req.deferred.isSettled) {680req.deferred.error(error);681}682});683this._inFlightRequests = [];684this._queuedRequests.forEach(req => {685if (!req.deferred.isSettled) {686req.deferred.error(error);687}688});689this._queuedRequests = [];690if (this._pendingPrompt && !this._pendingPrompt.isSettled) {691this._pendingPrompt.error(error);692}693this._pendingPrompt = undefined;694} finally {695// Clean up any remaining OTel spans696for (const [, span] of otelToolSpans) {697span.setStatus(SpanStatusCode.ERROR, 'session ended before tool completed');698span.end();699}700otelToolSpans.clear();701for (const [, span] of otelHookSpans) {702span.setStatus(SpanStatusCode.ERROR, 'session ended before hook completed');703span.end();704}705otelHookSpans.clear();706// End any lingering invoke_agent span707this._otelTracker.endRequestWithError('session ended');708}709}710711/**712* Restarts the session by aborting the current SDK connection.713* The abort causes _processMessages to enter error cleanup, which714* rejects any remaining requests and resets session state.715*/716private _restartSession(): void {717this._queryGenerator = undefined;718this._abortController.abort();719this._abortController = new AbortController();720this._isResumed = true;721}722723// #region Gateway Lifecycle724725private _cancelGatewayIdleTimer(): void {726if (this._gatewayIdleTimeout !== undefined) {727clearTimeout(this._gatewayIdleTimeout);728this._gatewayIdleTimeout = undefined;729}730}731732private _startGatewayIdleTimer(): void {733this._cancelGatewayIdleTimer();734this._gatewayIdleTimeout = setTimeout(() => {735this._gatewayIdleTimeout = undefined;736this._disposeGateway();737this._restartSession();738}, ClaudeCodeSession.GATEWAY_IDLE_TIMEOUT_MS);739}740741private _disposeGateway(): void {742this._gateway?.dispose();743this._gateway = undefined;744}745746// #endregion747748/**749* Computes a snapshot of the MCP tool names from a chat request's tools map.750*/751private _computeToolsSnapshot(tools: vscode.ChatRequest['tools']): ReadonlySet<string> {752// TODO: Handle the enabled/disabled (true/false) state per tool once we have UI for it753return new Set(754[...tools]755.filter(([tool]) => tool.source instanceof LanguageModelToolMCPSource)756.map(([tool]) => tool.name)757);758}759760/**761* Checks whether a tools snapshot matches the current session's tools.762*/763private _toolsMatch(snapshot: ReadonlySet<string>): boolean {764if (!this._currentToolNames) {765return true;766}767768if (snapshot.size !== this._currentToolNames.size) {769return false;770}771772for (const name of snapshot) {773if (!this._currentToolNames.has(name)) {774return false;775}776}777778return true;779}780781}782783784