Skip to content

Resumed session retains stale connection item IDs after interrupted response #4505

Description

@Adamkadaban

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:

  1. Use an existing Copilot CLI session normally.

  2. Exit or close the CLI while the agent is about to respond or while a response is in progress.

  3. Reopen Copilot CLI and resume the existing session.

  4. Enter a prompt such as continue.

  5. Observe:

    Execution failed: CAPIError: 400 input item ID does not belong to this connection
    (Request ID: 00000-ed11ed35-1761-4d58-95f1-7f22a29c4556)
    
  6. 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)
    
  7. Run /fork and try another prompt in the fork. The error persists.

  8. 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

  1. added
    area:networkingProxy, SSL/TLS, certificates, corporate environments, and connectivity issues
    area:sessionsSession management, resume, history, session picker, and session state
    and removed on Aug 17, 2026
  2. 1fanwang commented on Sep 8, 2026

    @1fanwang

    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-sol to gpt-5.6-terra still failed. Switching the same session to claude-opus-5 recovered it, and the next prompt completed normally. Model switching is therefore a partial workaround, but the session remains unusable on the original model path.

  3. 1fanwang commented on Sep 8, 2026

    @1fanwang

    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 debug and counting Opening WebSocket responses connection in 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=true moves 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: 0
    

    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 the capi.enableWebSocketResponses session 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.

  4. 1fanwang commented on Sep 8, 2026

    @1fanwang

    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": false and "include": ["reasoning.encrypted_content"]. Because nothing is stored server side, the whole prior conversation is replayed as input on each new connection. User messages are sent without an id. 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_* and call_* 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.jsonl as data.apiCallId on assistant.message events, 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 connection for one identical prompt gives 1 for gpt-5.6-sol, 1 for gpt-5.6-terra, and 0 for claude-opus-5 and gpt-5-mini. Anthropic backed sessions persist a plain msg_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 where gpt-5.6-sol to gpt-5.6-terra changed 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=true

    The equivalent service.agent.disableWebSocketResponses key in settings.json is 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. Omitting id on 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.

  5. Stono commented on Sep 14, 2026

    @Stono

    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:

    Image

    Somewhat grim, but it works, and I can now resume sessions.

  6. Osshaikh commented on Sep 22, 2026

    @Osshaikh

    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 connection

    Forking did not resolve it, and automatic compaction also encountered the error.

    The combination that recovered the session was:

    1. Close the affected Copilot CLI instance.
    2. In PowerShell, resume the saved session with WebSocket Responses disabled:
    $env:COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES = "true"
    copilot --resume "<session-id>"
    1. 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.

  7. copilot-cli-bot commented on Oct 5, 2026

    @copilot-cli-bot

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:networkingProxy, SSL/TLS, certificates, corporate environments, and connectivity issuesarea:sessionsSession management, resume, history, session picker, and session state

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions