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:
- 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.
- 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)
Agent host: a startup-config change (
sessionSync) restarts the CLI client mid-turn and the session hangs foreverWhat happens
CopilotAgent._restartClientIfStartupConfigChanged()tears down the CopilotClient whenever a startup-only config value changes, even if a turn is actively running:CopilotAgentSessionnever finalizes its in-flight turn on disposal —ChatTurnComplete/ChatTurnCancelled/ChatErrorare 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)
chat/turnStarted turnId=request_bbf818da-…Tool started: apply_patch,assistant.turn_start— model still working[DefaultAccount] Managed settings fetch returned non-success status 404; falling back to local-only policy[Copilot] Startup config changed (sessionSync=true), restarting CopilotClientsession.shutdown; CLI subprocess exitschat/turnComplete, nochat/turnCancelled, nochat/erroris ever sent forrequest_bbf818daThe 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
sessionSyncmaps tochat.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):#319430 (
225f0854cc65) removed the_sessions.size === 0guard 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:
sessionSynccan change without any user action.CopilotAgentSessionis disposed — emitChatError(orChatTurnCancelled) +ChatTurnCompleteso the client stops spinning and the user sees something actionable. This should be dispose-driven so it also coversshutdown()andreleaseSession().Possibly related
At 16:06:46Z a steering turn was started and completed while the original turn was still open, with no
turnCompletefor the original — so the chat had two overlapping open turns even before the restart. May be a separate_beginSteeringTurnbug.(Written by Copilot)