Skip to content

Agent host session hangs forever when a startup-config change restarts the CLI client mid-turn #327362

Description

@roblourens

Agent host: a startup-config change (sessionSync) restarts the CLI client mid-turn and the session hangs forever

What happens

CopilotAgent._restartClientIfStartupConfigChanged() tears down the CopilotClient whenever a startup-only config value changes, even if a turn is actively running:

if (this._client) {
    this._logService.info(`[Copilot] Startup config changed (${changed}), restarting CopilotClient`);
    this._sessions.clearAndDisposeAll();
    await this._stopClient();
}

CopilotAgentSession never finalizes its in-flight turn on disposal — ChatTurnComplete / ChatTurnCancelled / ChatError are only emitted in response to SDK events (_completeActiveTurn, _beginSteeringTurn). A disposed session gets no more SDK events, so nothing closes the turn.

Net effect: the client's turn stays open forever. The UI spins with no output, no error, and no way to recover other than reloading.

Evidence (from an Agent Host debug log bundle)

Time (local) Event
09:05:03 chat/turnStarted turnId=request_bbf818da-…
09:05–09:26 ~30 tool calls run normally under that turn
09:26:44 Tool started: apply_patch, assistant.turn_start — model still working
09:27:02 [DefaultAccount] Managed settings fetch returned non-success status 404; falling back to local-only policy
09:27:06 [Copilot] Startup config changed (sessionSync=true), restarting CopilotClient
09:27:07 session.shutdown; CLI subprocess exits
after No chat/turnComplete, no chat/turnCancelled, no chat/error is ever sent for request_bbf818da
11:36 ~2h later the user exports logs; session still shows as running

The agent host process itself stayed healthy — it kept serving a different session for another 2 hours. Only the session with the in-flight turn was orphaned.

Note that sessionSync maps to chat.sessionSync.enabled, which is experiment-driven (experiment: { mode: 'auto' }) and policy-controlled (CopilotSessionSync / cloud_session_storage_enabled). It can flip on its own from an async experiment or policy refresh, so this can kill a running turn with no user action at all. That's what happened here — the flip landed 4s after a managed-settings fetch 404'd.

How it regressed

The original implementation only restarted when idle — #317390 (5d8b31703319):

if (this._client && this._sessions.size === 0) {
    ...
    await this._stopClient();
}

#319430 (225f0854cc65) removed the _sessions.size === 0 guard so the rubber duck toggle would take effect immediately. That PR's own commit message notes the change: "Corrects the misleading 'only if idle' restart comment to match the new behavior (active sessions are disposed on a startup-config change)."

Later PRs added more restart triggers to the same unguarded path (enterpriseHost, copilotSdkLogLevel, systemProxy), widening the window.

Suggested fix

Two independent problems, both worth fixing:

  1. Don't kill live work. Restore an idle check: defer the restart until all sessions are idle (or at minimum until no turn is running), rather than disposing sessions mid-turn. Especially important because sessionSync can change without any user action.
  2. Never leave a turn open. Finalize the in-flight turn when a CopilotAgentSession is disposed — emit ChatError (or ChatTurnCancelled) + ChatTurnComplete so the client stops spinning and the user sees something actionable. This should be dispose-driven so it also covers shutdown() and releaseSession().

Possibly related

At 16:06:46Z a steering turn was started and completed while the original turn was still open, with no turnComplete for the original — so the chat had two overlapping open turns even before the restart. May be a separate _beginSteeringTurn bug.

(Written by Copilot)

Metadata

Metadata

Assignees

Labels

agent-hostIssues for the agent-host implementation (not the protocol)bugIssue identified by VS Code Team member as probable buginsiders-releasedPatch has been released in VS Code InsidersverifiedVerification succeeded

Type

No type

Projects

Status
Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions