Skip to content

IDE: resumed compacted thread loses exec/code-mode tools on Responses WebSocket #35751

Open
fscfede-beep/codex
#1

Description

@xionghaizhi

What version of the IDE extension are you using?

openai.chatgpt 26.721.41059, bundled codex-cli 0.146.0-alpha.3.1.

What subscription do you have?

Not captured during diagnostics.

Which IDE are you using?

VS Code Remote.

What platform is your computer?

Linux 5.10.0-60.18.0.50.oe2203.x86_64 x86_64 x86_64

What issue are you seeing?

An older IDE thread containing a compacted record with replacement_history resumes successfully, but the first turn after reopening has no usable terminal/file-edit tool surface. The assistant reports that no terminal execution tool is available.

The same workspace and permission profile work in a fresh thread. Forking the affected thread reproduces the failure. A clean, unmodified OpenAI extension also reproduces it, so no Codex Local Groups patch is required.

The VS Code Remote reconnect and Codex runtime resume both succeed. Sanitized Extension Host logs show:

maybe_resume_success
requestPermissionProfile=:danger-full-access
requestRuntimeWorkspaceRootCount=1

This rules out a missing workspace root, denied permission profile, or failed Extension Host resume as the immediate cause.

What steps can reproduce the bug?

  1. Use a long VS Code Codex thread until its rollout contains compacted.payload.replacement_history.
  2. Close/reopen VS Code, or navigate away and reopen the old thread.
  3. Ask the resumed thread to run printf "TOOL_OK\n".
  4. Observe that it reports no terminal execution tool is available and emits no command execution.
  5. Create a fresh thread in the same workspace and repeat. The command executes.
  6. Fork the affected thread and repeat. The fork remains affected.

Sanitized transport A/B

The same stored compacted history was replayed with request-shape instrumentation:

Responses WebSocket

startup prewarm response.create: generate=false, top-level tools_count=0
actual response.create: top-level tools_count=0
result: no command execution

Responses HTTP POST

same stored history
result: command execution emitted
output: TOOL_OK

Fresh thread over Responses WebSocket

result: command execution emitted
output: TOOL_OK

The WebSocket connection itself is healthy. The failure is specific to resumed compacted history on the WebSocket path.

For Responses Lite models, code-mode tools may be represented through additional_tools, so the decisive symptom is the missing usable tool surface. This report does not assume top-level tools must always be non-empty. The likely boundary is that resume/prewarm does not preserve or rebuild the current step's effective tool manifest.

What is the expected behavior?

Resuming compacted history should rebuild the current runtime tool surface independently of stored conversation items. Responses WebSocket prewarm and the actual turn should expose the same usable code-mode tools as HTTP and a fresh thread.

Additional information

Suggested regression test:

  1. Create a thread with compacted replacement history.
  2. Resume it through app-server.
  3. Use Responses WebSocket startup prewarm.
  4. Run a turn that calls public exec and then exec_command.
  5. Assert command execution succeeds.
  6. Compare the effective tool/additional_tools state with the HTTP request for the same stored history.

Related but distinct reports:

Raw rollout files, provider configuration, and transcripts are private. Sanitized request-shape traces can be provided.

Activity

  1. added
    bugSomething isn't working
    extensionIssues related to the VS Code extension
    app-serverIssues involving app server protocol or interfaces
    contextIssues related to context management (including compaction)
    tool-callsIssues related to tool calling
    on Jul 28, 2026
  2. antmanler commented on Aug 18, 2026

    @antmanler

    Independently reproduced this and traced it to a specific mechanism in the Responses-over-WebSocket incremental request path. Your transport A/B (WS fails on resumed compacted history, HTTP succeeds on the same history, fresh thread over WS succeeds) matches our matrix exactly, and the mechanism below explains every cell — including two more we observed: a warm post-compaction turn succeeds, while thread/fork of a compacted thread fails even in the same process.

    TL;DR: the tool table is transported as input[0] (spliced into the front of the request input), and the first turn after a fresh-process resume is sent as a prefix-based delta chained on the client-synthesized prewarm response. The tool table is therefore always the first casualty of the delta. When that delta contains a compaction item — a context-replacement directive — the upstream rebuilds context from the delta alone, with zero tool declarations (no additional_tools input item, no top-level tools field). No error frame is emitted; the failure is entirely silent.

    Affected versions

    • rust-v0.147.0 (confirmed, deterministic 4/4 via app-server JSON-RPC)
    • rust-v0.148.0-alpha.9 (confirmed, same behaviour)

    Reproduced against a Responses API-compatible proxy. Requires an upstream that rebuilds context from a compaction input item rather than extending the chained prewarm state; it is not observable against an upstream that persists prewarm response state and treats the delta as a pure extension.

    Repro (app-server JSON-RPC, no IDE needed)

    1. thread/start with dynamicTools (flat form and type: "namespace" form both reproduce).
    2. Send one turn that calls one of the dynamic tools — succeeds.
    3. thread/compact/start.
    4. Kill the codex app-server process.
    5. thread/resume in a fresh process (threadId only — restoration from session_meta is expected and does work; the loss is not in rollout reconstruction).
    6. Send a turn asking the model to call the dynamic tool.

    Expected: the tool is callable, as it is on a warm post-compaction turn.
    Actual: the model reports having no tools (including exec) and emits no tool calls. The outbound WebSocket frame carries no tool declarations.

    A resume/fork of a thread with no compaction in its rollout does not reproduce.

    Mechanism (line references at rust-v0.147.0)

    1. The tool table is spliced into the front of the request input rather than carried as a distinct field — codex-rs/core/src/client.rs:884:
      input.splice(0..0, prefix);   // prefix = [AdditionalTools, developer instructions]
    2. The incremental path matches a prefix of the previous request's input and sends only the remainder — codex-rs/core/src/client.rs:1188-1226:
      let previous_items_len = previous_request.input.len().checked_add(response_items.len())?;
      let (request_items_to_compare, incremental_items) = request.input.split_at_checked(previous_items_len)?;
      // ... prefix items compared pairwise ...
      Some(incremental_items.to_vec())
      The comparison only inspects the prefix; it has no notion that the remainder is a post-compaction context rebuild.
    3. The delta replaces the full input on the wire — codex-rs/core/src/client.rs:1651-1660:
      let ws_payload = ResponseCreateWsRequest {
          previous_response_id,
          input: incremental_items.as_deref().unwrap_or(&request.input),
          generate: if warmup { Some(false) } else { None },
          ...
    4. The baseline for that prefix is the prewarm request (:1771 sends warmup = true, stored as last_request at :1685; the first real turn follows at :1830).

    The prewarm input is exactly [additional_tools, developer_instructions], so previous_items_len == 2 and the first real turn's delta is input[2..] — the entire conversation minus the tool table.

    Observed frames (RUST_LOG=trace, tungstenite)

    Prewarm (generate: false), identical in working and failing runs:

    input[0] additional_tools(namespaces = [functions, collaboration, example_ns])
    input[1] message/developer("You are Codex, ...")
    

    Warm post-compaction turn — prefix comparison fails (last request was the compaction trigger), so a full request is sent and the tool call succeeds:

    previous_response_id: null
    input[0] additional_tools(...)
    input[1] message/developer
    input[2] message/user
    input[3] compaction
    input[4..] ...
    

    First turn after a fresh-process resume — delta path engages and the tool call fails:

    previous_response_id: "resp_prewarm_<uuid>"   <- client-synthesized, not a server id
    input[0] message/user       <- equals full input[2]
    input[1] compaction         <- equals full input[3]
    input[2..] ...              <- equals full input[4..]
    no additional_tools item anywhere; no top-level "tools" field
    

    The failing frame is byte-for-byte the full input with [additional_tools, developer_instructions] removed. Top-level keys on that frame: client_metadata, include, input, model, parallel_tool_calls, previous_response_id, prompt_cache_key, reasoning, service_tier, store, stream, text, tool_choice, type — zero tool declarations anywhere.

    Why each matrix cell behaves the way it does:

    Case Delta contains compaction? Result
    Fresh thread over WS no (pure extension; chained state covers tools) works
    Resumed compacted thread, first turn yes (context rebuild sent as increment, tools in stripped prefix) fails
    Warm post-compaction turn n/a — prefix mismatch forces full resend works
    Fork of compacted thread (even in-process) yes (new session → new prewarm baseline) fails
    Same history over HTTP n/a works

    Design claim

    The tool table should not live inside a truncatable history prefix. Either:

    • carry tool declarations in a field the delta path never elides, or
    • have the delta path unconditionally retain ResponseItem::AdditionalTools, or
    • disable the delta path for any request whose input contains a compaction item, since such a request is a context rebuild rather than an extension.

    Workaround

    For custom providers, disable the WebSocket transport:

    [model_providers.<id>]
    supports_websockets = false

    responses_websocket_enabled() (codex-rs/core/src/client.rs:949-957) then returns false and every request goes over HTTP, where ResponsesApiRequest has no previous_response_id field at all (:918-934) and the tool table is rebuilt per request (:862-891). Confirmed to make the full repro matrix pass — and notably, already-broken compacted threads heal immediately on the next turn, no thread recreation needed: rollout restoration is intact (the tools are in the session config), only the WS turn frame omits them.

  3. XhinLiang commented on Sep 2, 2026

    @XhinLiang

    Additional independent reproduction on stable codex-cli 0.151.0 (sanitized).

    Environment

    • Linux x86_64
    • codex app-server JSON-RPC over stdio, with one app-server process per loaded thread
    • Model: gpt-5.6-sol, reasoning effort medium
    • approvalPolicy=never, sandbox=danger-full-access
    • The cwd was trusted, and both shell_tool and unified_exec were enabled

    Reproduction observed in three independent long-running threads

    The same sequence occurred in all three threads:

    1. A contextCompaction item and a persisted compacted record with replacement_history appeared.
    2. Structured exec calls continued to succeed later in that same live turn, so the tool surface was still healthy immediately after compaction.
    3. The app-server process then exited normally after the host idle timeout.
    4. A fresh app-server process initialized successfully, and thread/resume returned success with the same model, cwd, approval, and sandbox overrides.
    5. The first successor turn emitted no structured commandExecution or custom_tool_call. The model either reported that no command executor was available or rendered intended calls such as to=tools.exec_command as ordinary assistant text.
    6. Additional turns on the resumed thread remained tool-dead.

    One thread provided a tight sequence:

    • compaction occurred;
    • structured exec calls continued for about six minutes;
    • the process idle-exited about four minutes later;
    • a fresh-process resume happened about three minutes after that;
    • the first response after resume had zero structured tool calls.

    Impact

    One affected automation thread accumulated 47 completed retry runs and 5 failed retry runs after the transition, all without a structured tool call. The host treated the successful thread/resume as healthy, so its existing fresh-thread recovery path was never activated.

    This matches the mechanism already documented in this issue and extends the confirmed affected range from the earlier 0.147/0.148 builds to stable 0.151.0. It also reproduces outside the IDE UI through a third-party app-server stdio integration.

    I did not capture an upstream Responses HTTP versus WebSocket A/B, so I cannot independently confirm the transport-level mechanism. The compaction plus fresh-process resume boundary and the loss of native exec are deterministic across the three observed threads.

    I have not yet replayed the fixture on 0.152.1.

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

    app-serverIssues involving app server protocol or interfacesbugSomething isn't workingcontextIssues related to context management (including compaction)extensionIssues related to the VS Code extensiontool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions