IDE: resumed compacted thread loses exec/code-mode tools on Responses WebSocket
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?
- Use a long VS Code Codex thread until its rollout contains
compacted.payload.replacement_history. - Close/reopen VS Code, or navigate away and reopen the old thread.
- Ask the resumed thread to run
printf "TOOL_OK\n". - Observe that it reports no terminal execution tool is available and emits no command execution.
- Create a fresh thread in the same workspace and repeat. The command executes.
- 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:
- Create a thread with compacted replacement history.
- Resume it through app-server.
- Use Responses WebSocket startup prewarm.
- Run a turn that calls public
execand thenexec_command. - Assert command execution succeeds.
- Compare the effective tool/
additional_toolsstate with the HTTP request for the same stored history.
Related but distinct reports:
- #34719: post-compaction deferred app/MCP tools disappear, but shell/core tools remain.
- #31894: fresh
codex execwith Responses Lite does not expose code-mode tools; this report is resume/compaction and WebSocket-specific, with fresh WebSocket and same-history HTTP controls succeeding. - #35298: Remote reconnect removes app connector tools; this report loses native exec/file-edit tools and is tied to compacted-history WebSocket replay.
- #25990: older threads miss newly introduced dynamic tools; this report loses tools that remain available in a fresh thread on the same runtime.
Raw rollout files, provider configuration, and transcripts are private. Sanitized request-shape traces can be provided.
1 Comment
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/forkof 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 acompactionitem — a context-replacement directive — the upstream rebuilds context from the delta alone, with zero tool declarations (noadditional_toolsinput item, no top-leveltoolsfield). 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
compactioninput 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)
thread/startwithdynamicTools(flat form andtype: "namespace"form both reproduce).thread/compact/start.codex app-serverprocess.thread/resumein a fresh process (threadIdonly — restoration fromsession_metais expected and does work; the loss is not in rollout reconstruction).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)codex-rs/core/src/client.rs:884:``
rust
``input.splice(0..0, prefix); // prefix = [AdditionalTools, developer instructions]
codex-rs/core/src/client.rs:1188-1226:``
rust
``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.
codex-rs/core/src/client.rs:1651-1660:``
rust
``let ws_payload = ResponseCreateWsRequest {
previous_response_id,
input: incremental_items.as_deref().unwrap_or(&request.input),
generate: if warmup { Some(false) } else { None },
...
:1771sendswarmup = true, stored aslast_requestat:1685; the first real turn follows at:1830).The prewarm input is exactly
[additional_tools, developer_instructions], soprevious_items_len == 2and the first real turn's delta isinput[2..]— the entire conversation minus the tool table.Observed frames (RUST_LOG=trace, tungstenite)
Prewarm (
generate: false), identical in working and failing runs:Warm post-compaction turn — prefix comparison fails (last request was the compaction trigger), so a full request is sent and the tool call succeeds:
First turn after a fresh-process resume — delta path engages and the tool call fails:
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:
ResponseItem::AdditionalTools, orcompactionitem, since such a request is a context rebuild rather than an extension.Workaround
For custom providers, disable the WebSocket transport:
responses_websocket_enabled()(codex-rs/core/src/client.rs:949-957) then returns false and every request goes over HTTP, whereResponsesApiRequesthas noprevious_response_idfield 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.