Repository navigation
Resumed session retains stale connection item IDs after interrupted response #4505
Description
Activity
- addedarea:networkingProxy, SSL/TLS, certificates, corporate environments, and connectivity issuesProxy, SSL/TLS, certificates, corporate environments, and connectivity issuesarea:sessionsSession management, resume, history, session picker, and session stateSession management, resume, history, session picker, and session stateand removed
on Aug 17, 2026 Confirmed on Copilot CLI 1.0.83 on macOS arm64. A previously healthy session was resumed about 16 hours later, then every prompt failed with the same 400 error. Retrying and reauthenticating did not help.
Switching from
gpt-5.6-soltogpt-5.6-terrastill failed. Switching the same session toclaude-opus-5recovered it, and the next prompt completed normally. Model switching is therefore a partial workaround, but the session remains unusable on the original model path.Narrowing this down on 1.0.83, macOS arm64: the failure tracks the CAPI WebSocket responses transport, and only the models that use it are affected.
Running the same trivial prompt under
--log-level debugand countingOpening WebSocket responses connectionin the session log:Model WebSocket responses connections gpt-5.6-sol 1 gpt-5.6-terra 1 claude-opus-5 0 gpt-5-mini 0 That matches the recovery behaviour reported here and in #2147. A corrupted session keeps failing while it stays on a model that opens this transport, and starts working again as soon as it moves to one that does not. Switching between two models that both use it changes nothing, which is what makes the workaround look inconsistent.
Correcting an earlier version of this comment: there is a working opt-out. Setting
COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES=truemoves the session onto the HTTP Responses transport, verified by the same counter dropping to zero:gpt-5.6-sol, no env var: 1 gpt-5.6-sol, env var set: 0 gpt-5.6-terra, env var set: 0It is not listed in
copilot help environment, so affected users are unlikely to find it. Documenting it would give people a recovery route that does not involve changing models. The SDK already describes it as the equivalent of thecapi.enableWebSocketResponsessession option.Beyond documentation, the 400 is still surfaced as a bare execution failure. Since the session stays unrecoverable on that model, detecting this specific rejection and either rebuilding the connection-bound state or naming the action that recovers the session would avoid the dead end.
Reproduction here was a session resumed roughly 16 hours after it was last used; every prompt then failed until the transport changed.
Still reproducing on 1.0.83. I spent some time instrumenting this at the wire level with
--log-level debug, and I have enough to describe what the client actually sends and to rule out two of the obvious explanations. I could not force the 400 on demand, so I have tried to separate what I measured from what I am inferring.What the client sends
Every assistant turn opens its own WebSocket responses connection, sends a single
response.create, and closes it:[rust:copilot_runtime::network::websocket_responses] Opening WebSocket responses connection [rust:copilot_runtime::network::websocket_responses] WebSocket responses connection established {"connection_id":"websocket-responses-43a255b8-...","request_id":"00000-cefeca16-..."} [rust:model_wire] Wire request: { "type": "response.create", ... } [rust:copilot_runtime::network::websocket_responses] Closing WebSocket responses connection {"connection_id":"websocket-responses-43a255b8-..."}The request sets
"store": falseand"include": ["reasoning.encrypted_content"]. Because nothing is stored server side, the whole prior conversation is replayed asinputon each new connection. User messages are sent without anid. Assistant output, function calls, and reasoning items are all replayed carrying one, which is what makes an "input item ID" available to be rejected:{ "type": "message", "id": "msg_2baf1c2b-1928-37dc-4e10-bb68d77a7f6e", "role": "assistant", "content": [{ "type": "output_text", "text": "PROBE_OK" }], "status": "completed" }Three ID shapes appear on replayed items:
msg_<uuid>on assistant messages,fc_call_*andcall_*on function calls, and an opaque base64 blob on reasoning items.The assistant message ID is derived from content, not from the session
This is the part I did not expect. That
msg_<uuid>is a version 3 UUID, so it is a name based hash rather than a random one, and it is not stored anywhere in the session directory. It is recomputed at replay time.I ran the same prompt in two unrelated sessions, created minutes apart, each on its own connection, and both sent the byte identical ID:
session 06cb1ed7-77d9-4018-9eeb-6eef66c1b97b -> "id": "msg_2baf1c2b-1928-37dc-4e10-bb68d77a7f6e" session 72e4ea38-d113-47ed-99e4-61c4c29e6b98 -> "id": "msg_2baf1c2b-1928-37dc-4e10-bb68d77a7f6e"So identical assistant text always yields the identical input item ID, for every session and every user. Any ID uniqueness the server assumes per connection does not hold on the client side.
Reasoning item IDs are written to disk and replayed after resume
The opaque base64 ID on reasoning items is persisted in
events.jsonlasdata.apiCallIdonassistant.messageevents, and it is sent again when the session resumes on a later connection. That is genuine connection era state surviving in the session file, which matches the title of this issue.Two theories I was able to rule out
I resumed two sessions concurrently so both live connections replayed the same
msg_2baf1c2b-...at once. Both succeeded, so a plain ID collision across simultaneous connections is not sufficient on its own.I also replaced the persisted reasoning ID with a nine day old one taken from a different connection. That did not fail either, but it silently cost the whole prompt cache. A normal resume of the same session reported
↑ 118.1k (56.4k cached, 61.6k written), and the same resume with the foreign ID reported↑ 118.1k (118.1k written)with nothing cached. So a stale reasoning ID degrades quietly rather than erroring.Why switching models behaves the way people describe
The recovery pattern reported here is consistent with which transport each model uses. Counting
Opening WebSocket responses connectionfor one identical prompt gives 1 forgpt-5.6-sol, 1 forgpt-5.6-terra, and 0 forclaude-opus-5andgpt-5-mini. Anthropic backed sessions persist a plainmsg_01...provider ID and never send these encrypted reasoning items, so moving from one OpenAI model to another keeps you on the failing path while moving to a Claude model leaves it. That matches the reports wheregpt-5.6-soltogpt-5.6-terrachanged nothing and a switch to Claude recovered immediately.Workaround
Forcing the HTTP responses transport avoids it without changing models:
export COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES=trueThe equivalent
service.agent.disableWebSocketResponseskey insettings.jsonis dropped by the settings parser and has no effect, so the environment variable is the only form that works today. This is documented in #4770.What would help
Given
"store": false, the server does not need client supplied IDs on replayed input items at all. Omittingidon replayed items, or salting the derivation per session so two sessions cannot mint the same one, would both remove the class of failure. Detecting this specific 400 and retrying once without the IDs would at least make an affected session recoverable instead of permanently wedged, which is the part that hurts most: once a long session starts returning this, every subsequent prompt fails and the transcript is effectively lost.Environment: 1.0.83, macOS 26.6.2 (25G83), arm64.
So... apologies for just being a meat proxy but copilot "fixed" this for me.
In our work we use copilot-sdk to do sessions remotely, but you can resume them locally if you want to.
I was experiencing this problem too and our cli now santises the json file like this:
Somewhat grim, but it works, and I can now resume sessions.
Confirmed recovery on Windows with Copilot CLI 1.0.87.
Resuming my session failed with:
Execution failed: 400 input item ID does not belong to this connectionForking did not resolve it, and automatic compaction also encountered the error.
The combination that recovered the session was:
- Close the affected Copilot CLI instance.
- In PowerShell, resume the saved session with WebSocket Responses disabled:
$env:COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES = "true" copilot --resume "<session-id>"
- Inside the resumed session, run
/model, select an available Claude model, and retry the prompt.
Recovery was confirmed after switching to Claude with the transport workaround in place. I have not isolated the two changes, so this confirms the combination—not that disabling WebSocket Responses alone fixes it.
The session files were backed up beforehand. No manual editing or deletion of conversation history was needed. The environment variable above applies only to that PowerShell process and its child processes, not permanently.
This appears to be resolved as of GitHub Copilot CLI v1.0.91. Please update to that release or a newer stable version. We're closing this as completed. If it still happens, please comment with your version and reproduction steps.
Describe the bug
After reopening and resuming an existing Copilot CLI session, every prompt fails with:
The affected session does not recover after retrying. Forking it with
/forkalso does not help: prompts in the fork fail with the same error. A separate new Copilot CLI session works normally.This appears to be a regression of #2147, which was closed as fixed in v1.0.10. The reported root cause there was stale WebSocket state being reused after a reconnection.
Affected version
Steps to reproduce the behavior
I am not certain of the exact trigger, but the likely sequence was:
Use an existing Copilot CLI session normally.
Exit or close the CLI while the agent is about to respond or while a response is in progress.
Reopen Copilot CLI and resume the existing session.
Enter a prompt such as
continue.Observe:
Retry the prompt and receive the same error with a new request ID:
Run
/forkand try another prompt in the fork. The error persists.Start a separate new session. The new session works normally.
Expected behavior
Copilot CLI should discard or reconstruct connection-bound response state after reconnecting. An interrupted response should not permanently make a persisted session, or a fork of that session, unusable.
Additional context
Investigated and filed with assistance from GitHub Copilot CLI.