Dynamic tool calls can emit item/started without item/completed when a turn is interrupted
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- rust
- Domain
- api, backend-api-design, testing
Research direction
Start with the named regression test, interrupting_pending_dynamic_tool_emits_terminal_item, and run it with just test -p codex-app-server interrupting_pending_dynamic_tool_emits_terminal_item. Trace DynamicToolHandler in codex-rs/core/src/tools/handlers/dynamic.rs alongside cancellation handling in registry.rs and parallel.rs; done means the interrupted pending call emits exactly one item/completed before turn/completed, while the normal round-trip test still passes.
Written by the indexing model from the issue text.
Description
What issue are you seeing?
When an App Server v2 dynamic tool call is waiting for the client response and the turn is interrupted, the server can emit item/started for the dynamicToolCall but never emit the matching item/completed.
The observed lifecycle is:
turn/started
item/started(type=dynamicToolCall, id=dyn-call-interrupt-1, status=inProgress)
item/tool/call(callId=dyn-call-interrupt-1) # client response remains pending
turn/interrupt -> {}
turn/completed(status=interrupted)
No item/completed for dyn-call-interrupt-1 is sent before the turn completes. A client that treats item/completed as the authoritative terminal state can therefore keep rendering this item as in progress after the turn has already become interrupted.
This appears inconsistent with the documented contract that the per-item lifecycle is always item/started followed by zero or more deltas and item/completed:
Environment:
openai/codex commit: 56395bddaf26eb2829387ca6a417bf9128e5b239
platform: macOS 26.3.2 arm64 (Darwin 25.3.0)
rustc: 1.94.0
App Server experimental API: enabled
I reproduced this in three independent test runs on that commit. The local nextest profile retried each failure once, so the same missing-terminal-item behavior was observed 6/6 times.
What steps can reproduce the bug?
- Initialize App Server v2 with
capabilities.experimentalApi = true. - Start a thread with a top-level function in
dynamicTools. - Start a turn whose mocked model response invokes that dynamic tool.
- Wait for both the
dynamicToolCallitem/startednotification and the matchingitem/tool/callserver request. - Keep the
item/tool/callrequest pending; do not send aDynamicToolCallResponse. - Send
turn/interruptwith the active thread and turn IDs. - Continue reading notifications through
turn/completed. - Check whether an
item/completedfor the same dynamic-tool call ID arrived before the interrupted turn completed.
I added a local App Server integration test named:
interrupting_pending_dynamic_tool_emits_terminal_item
and ran:
just test -p codex-app-server interrupting_pending_dynamic_tool_emits_terminal_item
Each run fails with the same assertion:
Error: missing item/completed for interrupted dynamic tool call
As a control, the normal response path passes on the same source tree:
just test -p codex-app-server dynamic_tool_call_round_trip_sends_text_content_items_to_model
What is the expected behavior?
Once a dynamic-tool item has emitted item/started, it should receive exactly one terminal item/completed before the corresponding turn/completed, including when the turn is interrupted while the client response is pending.
Based on the existing item model, I would expect the interrupted item to be terminal—likely status = "failed", success = false, with a cancellation error—followed by turn/completed(status = "interrupted"). If interruption is intended to be an exception to the documented item lifecycle, the protocol documentation should state that explicitly so clients can reconcile orphaned in-progress items.
Additional information
Root-cause hypothesis
This is a code-path hypothesis rather than a maintainer-confirmed root cause:
DynamicToolHandleruses the default runtime cancellation behavior.- The handler emits
item/started, directly awaits the client response channel, and only emitsitem/completedafter that await returns. waits_for_runtime_cancellation()defaults tofalse.- The outer runtime aborts handlers on cancellation when that flag is false.
This can abort the dynamic handler while it is awaiting the client response, before its failed terminal item is constructed and emitted.
Possible direction
One focused direction would be to let the dynamic handler observe invocation cancellation, opt into completing runtime cancellation teardown, emit one failed terminal item, and then return. A broader alternative would be for the App Server projection layer to synthesize terminal items for every started-but-unfinished item before turn/completed, but that has greater duplicate-completion and state-consistency risk.
It would be useful to confirm whether cancellation completion should be owned by the dynamic tool handler/core lifecycle or by the App Server projection layer, and what terminal status/error semantics are intended when a response and interrupt race.
I searched open and closed Issues and PRs for dynamicToolCall, pending dynamic tool interruption, and missing item/completed behavior and did not find an equivalent report.
I have traced the relevant cancellation and item-lifecycle paths and have a deterministic local regression test for this behavior. If this is considered a bug and the focused direction matches the intended cancellation semantics, I would be happy to implement the fix. Would the Codex team be willing to invite me to open a PR for it?
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.5k
- Avg merge
- 1m
- Merged PRs (30d)
- 1k
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openai/codex
-
enhancement remote
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
bug CLI windows-os
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
macOS sandbox blocks hw.optional.arm64 sysctl, causing Flutter to misdetect Apple Silicon as x64 Openbug CLI sandbox
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug CLI TUI
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
CLI config enhancement skills
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
kwakseongjae/auto-hwp#319 ·
-
area:cli bug filter-quality good first issue priority:medium
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 1/5 Under an hour Newbie friendliness 72/100
bevyengine/bevy#25861 ·
-
comp-datalake
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/ClickHouse#121222 ·
-
A-linter
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
oxc-project/oxc#26863 ·