Repository navigation
Resumed session retains stale connection item IDs after interrupted response #4505
Description
Describe the bug
After reopening and resuming an existing Copilot CLI session, every prompt fails with:
Execution failed: CAPIError: 400 input item ID does not belong to this connection
The affected session does not recover after retrying. Forking it with /fork also 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
GitHub Copilot CLI 1.0.81-0
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:
Execution failed: CAPIError: 400 input item ID does not belong to this connection (Request ID: 00000-ed11ed35-1761-4d58-95f1-7f22a29c4556) -
Retry the prompt and receive the same error with a new request ID:
Execution failed: CAPIError: 400 input item ID does not belong to this connection (Request ID: 00000-b4c9a489-fc2f-4b5f-921f-13a2e409f386) -
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
- Operating system: Windows
- CPU architecture: ARM64
- Shell: PowerShell 7
- The original session log shows the old Copilot process shutting down and cleaning up the persisted session before it was reopened.
- The failure is isolated to the affected conversation and forks made from it.
- Restarting/reconnecting and retrying do not recover the conversation.
- A comment on CAIP 400: input item ID does not belong to this connection #2147 reports the same failure on v1.0.79-4, suggesting the issue may have regressed before v1.0.81-0.
- I have not attached the complete session log because it contains conversation and tool history.
Investigated and filed with assistance from GitHub Copilot CLI.
Activity
1fanwang commentedon Sep 8, 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.1fanwang commentedon Sep 8, 2026
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: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:It 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.
1fanwang commentedon Sep 8, 2026
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: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:
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.
Stono commentedon Sep 14, 2026
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.
Osshaikh commentedon Sep 22, 2026
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:
/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.
copilot-cli-bot commentedon Oct 5, 2026
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.