microsoft / microsoft/vscode

Steering message remains queued when sent during Copilot SDK session refresh

Open
#334,023 0 comments 0 reactions 1 assignee Claimed by @aeschli View on GitHub
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

## User-facing problem

A steering message submitted while an Agent Host Copilot session is running can appear in the chat's pending-message queue but never reach the agent. It remains visibly queued even after the active turn continues and the underlying SDK session becomes available again.

There is no error surfaced to the user and no way to tell that delivery failed. In the observed case, the message was still present in the authoritative AHP chat snapshot more than three minutes later.

@aeschli it seems like this basically happened because the "VS Code Synced Data" bundle changed in the middle of a session running and we didn't handle that correctly. Lmk if you are not the correct person to look at this.

## Expected behavior

A steering message accepted by the Agent Host should either:

- be delivered to the running Copilot SDK turn; or
- remain pending and be retried once a transient SDK-session refresh completes.

It must not be silently abandoned while still appearing queued in protocol state.

## Observed sequence

Times below are from the same Agent Host debug-log bundle.

1. **19:10:12** — The session is evicted as idle. Its host-side in-memory state, including the connected window's transient active-client contribution, is removed.
2. **19:11:18.819** — Subscribing to the session starts `AgentService.restoreSession`.
3. **19:11:18.843** — Restore asks the Copilot provider for chat history, causing the provider to resume the SDK conversation before the window has republished its active-client state. The launch projection has no session-configured MCP servers and lists `github-mcp-server` as disabled.
4. **19:11:20.251** — The VS Code window publishes `session/activeClientSet` after its customization scope resolves. It contains the ten integrated-browser client tools and five plugin customizations, including the synthetic **VS Code Synced Data** plugin.
5. **19:11:25.945** — The initial SDK resume completes. Its applied structural snapshot is now stale relative to the active-client state published while it was resuming.
6. **19:14:56.454** — The user sends the next normal message.
7. **19:14:57.751** — `CopilotAgent._sendMessageOnce` compares the current active-client snapshot with the SDK session's applied snapshot. `ActiveClient.requiresRestart` reports a structural change, so the provider destroys the live SDK entry while preserving routing and starts a resume with the now-current configuration. The new launch projection includes `component-explorer` and `vscode-automation-mcp`.
8. **19:15:00.804** — During that resume window, the user submits a steering message. The client sends `chat/pendingMessageSet`; the Agent Host accepts it, updates authoritative chat state, and echoes the action back to the client.
9. **19:15:00.805** — `QueueDrainContribution._syncPendingMessages` calls `CopilotAgent.setPendingMessages`.
10. `setPendingMessages` finds the preserved chat backing, but the corresponding live entry was removed by `_destroyLiveSession`. It logs:

```text
[Copilot] setPendingMessages: chat not found for ahp-chat://default/...
```

It then returns without delivering or scheduling a retry.
11. **19:15:05.345** — SDK `resumeSession` succeeds and `_registerLiveChat` restores the live entry, but no code replays the pending steering state.
12. **19:18:28.976** — A later AHP snapshot still contains the same steering message.

No corresponding `Sending steering message` or steering-consumed event appears in the provider logs.

## Why the SDK session restarted without a manual configuration change

The user did not change configuration. This is a restore-order race involving client-owned transient configuration:

- `AgentService._restoreSessionState` materializes the chat and calls `agent.chats.getMessages(...)`, which resumes the Copilot SDK session as part of reading history.
- The Sessions provider independently acquires an `IAgentCustomizationScope` and waits for `scope.whenResolved()` before dispatching `session/activeClientSet`. Waiting is intentional: publishing an unresolved scope would transiently wipe host customization state.
- Consequently, the SDK can begin resuming before active-client tools, plugins, and VS Code-configured MCP servers reach the provider.
- When `session/activeClientSet` later arrives, `AgentSideEffects._fanOutActiveClient` updates the provider's live `ActiveClient`, but does not rebind the SDK immediately.
- The next normal send notices the stale applied snapshot and performs the corrective destroy/resume.

The changed structural item was the active-client customization/plugin snapshot, particularly the generated **VS Code Synced Data** bundle and its projected MCP servers. The model and working directory did not change, and the ten client tools were stable across the published active-client payloads.

The synthetic bundle identity also changed across restoration because `createScopeAuthority(sessionType, scopeKey)` derives it from the resolved workspace-root scope. This does not imply a user edit.

## Relevant code

- `src/vs/platform/agentHost/node/agentService.ts`
- `restoreSession`
- `_restoreSessionState`
- `src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts`
- `_syncActiveClient`
- `_dispatchActiveClientWhenResolved`
- `src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.ts`
- `AgentCustomizationScope`
- `createScopeAuthority`
- `src/vs/platform/agentHost/node/agentSideEffects.ts`
- `_fanOutActiveClient`
- `src/vs/platform/agentHost/node/copilot/copilotAgent.ts`
- `_sendMessageOnce`
- `_destroyLiveSession`
- `_registerLiveChat`
- `setPendingMessages`
- `ActiveClient.requiresRestart`
- `src/vs/platform/agentHost/node/chatContributions/queueDrain/queueDrainContribution.ts`
- `_syncPendingMessages`

## Root cause

Two races combine:

1. **Restore/configuration ordering:** SDK restoration can begin before the connected window's resolved active-client structural configuration is available. This creates an avoidable corrective restart on the next send.
2. **Non-resilient steering handoff:** `CopilotAgent.setPendingMessages` treats the temporary absence of a live SDK entry as an unknown chat and drops the delivery attempt. Restoring the live entry does not reconcile or replay the authoritative pending steering message.

The second race is sufficient to lose steering whenever a message happens to arrive during any provider refresh, regardless of what initiated the refresh.

## Suggested direction

At minimum, steering accepted into AHP state should survive a transient Copilot SDK refresh. Possible approaches include:

- serialize steering delivery through the same per-chat operation queue used by send/refresh so it waits for the in-flight resume; or
- replay the current authoritative steering state when `_registerLiveChat` restores a backing; or
- make `setPendingMessages` explicitly defer delivery when routing exists but its live SDK entry is temporarily unavailable.

Separately, consider coordinating session restore with initial active-client publication so the first SDK resume uses the resolved structural configuration and does not require a corrective restart on the next message.

Regression coverage should exercise both the temporary missing-live-entry case and the full restore → late `activeClientSet` → send → refresh → steering sequence.

(Written by Copilot)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.