Repository navigation
[Codex Desktop] Nested functions.exec wait_agent polling causes repeated parent turns and token usage #35108
Description
Summary
When multi_agent_v1__wait_agent is invoked inside Codex Desktop's generic functions.exec code-mode wrapper, one logical wait can be split into multiple parent-visible tool boundaries:
- the outer
functions.execyields withScript running with cell ID ...; - the parent then invokes
functions.wait; - the wait returns
timed_out: true; - the parent model is sampled again after each boundary.
This appears to consume additional model turns and append low-value wrapper metadata while the child agent is still simply running. The issue is not that the model continuously generates tokens during the blocked interval; the problem is that each yield/timeout returns control to the parent model and may trigger another full-context inference.
This is related to, but distinct from, the 30-second default timeout reported in #18394: the reproduction below explicitly passes timeout_ms: 60000.
Environment
- Surface: Codex Desktop
- Platform: Windows 11 x64
- Observed package version: 26.721.3404.0
- Tool path: nested
multi_agent_v1__wait_agentthrough genericfunctions.exec - Multi-agent version: v1
Reproduction
- Spawn a subagent and keep its work running longer than the outer code-mode yield interval.
- In the parent, invoke the wait through a generic
functions.execwrapper:
const result = await tools.multi_agent_v1__wait_agent({
targets: [agentId],
timeout_ms: 60000,
});
text(result);- In the observed run:
- the nested
wait_agentstarted withtimeout_ms: 60000; - after approximately 11 seconds, the outer call returned
Script running with cell ID 29; - the parent then called
functions.waitfor the cell; - approximately 50 seconds later,
functions.waitreturned{"timed_out":true}.
- the nested
Observed usage evidence
The local rollout recorded two consecutive usage events around this single logical wait:
- first event: input 163,952; cached input 162,560; output 126; total 164,078;
- second event: input 164,106; cached input 163,584; output 31; total 164,137.
These raw totals are mostly cached input and should not automatically be interpreted as billable tokens. They do show that the parent turn resumed twice and that the second resume reprocessed an almost equally large context.
The important symptom is the repeated parent/model boundary:
multi_agent_v1__wait_agent(timeout_ms=60000)
-> functions.exec: Script running with cell ID ...
-> functions.wait(...)
-> timed_out: true
-> another parent/model continuation
Expected behavior
- A parent waiting for a subagent should remain in a runtime-managed wait until the child completes, fails, or the requested timeout expires.
- An intermediate wait yield should not require a new full parent-model inference.
- If an internal cap exists, the runtime should re-arm the wait internally rather than return to the model for every interval.
- Status/heartbeat metadata should not be repeatedly appended to the model-visible conversation history.
timeout_msshould bound the complete multi-agent wait operation, including the outer tool wrapper.
Actual behavior
- One logical
wait_agentoperation is exposed as multiple code-mode/tool polling steps. - Each step returns control to the parent model.
- The parent receives verbose wrapper output such as
Script running with cell ID ...and then a separate timeout result. - With a large parent context, this can create substantial repeated input processing and token/quota consumption.
- The behavior remains present even when
wait_agentreceives an explicit 60-second timeout.
Related issues
- Make wait_agent default timeout configurable #18394 — default
wait_agenttimeout is hardcoded to approximately 30 seconds and causes polling loops. - Built-in
waittool capped at ~50s causes MASSIVE token burn on long waits (multi_agent_v2 re-samples every 50s) #32640 — the built-inwaittool is capped at approximately 50 seconds and re-samples the full context on each expiry. - [URGENT] Stable IDE extension ships a prerelease CLI with under-development "Code mode" silently active, breaking any long-running MCP calls and burning tokens while waiting #29122 — long-running nested tools routed through
functions.execconsume tokens while the model repeatedly waits. - Background process polling wastes tokens: each write_stdin poll triggers full API turn with complete history #13733 — background polling sends a full API turn with the complete history for each poll.
- multi_agent wait_agent/spawn_agent can block for ~7.5h; wait_agent timeout_ms not enforced during runtime stall #24951 —
wait_agent.timeout_msmay not bound the complete tool call.
Those issues cover adjacent generic wait, code-mode, or multi-agent behavior. I did not find a report specifically covering multi_agent_v1__wait_agent nested inside Codex Desktop's functions.exec wrapper and producing multiple parent usage events for one logical wait.
Suggested fix direction
- Keep multi-agent waits as direct runtime-managed operations instead of wrapping them in a model-polled code-mode cell.
- If a wrapper is unavoidable, keep the parent turn suspended until the child reaches a terminal state or the full timeout expires.
- Re-arm internal timers without a new model request.
- Avoid adding repeated cell IDs, wall times, and unchanged wait metadata to the model-visible history.
- Add telemetry that distinguishes one logical wait from internal runtime polling and reports the number of parent model resumptions.
Activity
github-actions commentedon Jul 24, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
nos1609 commentedon Aug 22, 2026
Independent cross-platform reproduction: the same nested V1 wait behavior occurs on Rocky Linux with VS Code Web, not only in Codex Desktop on Windows.
Environment:
functions.exec -> multi_agent_v1__wait_agent -> functions.waitIn one task family during a fixed 24-hour window:
multi_agent_v1__wait_agentoperations;functions.execcalls yieldedScript running with cell ID ...;functions.waitpolls;timed_out: true;The model-visible boundaries associated with this V1 wait path processed approximately 74.17M tokens:
Approximately 98.9% of the input was cached. These figures are local rollout telemetry, not a claim about OpenAI's private billing ledger. They show repeated full-context model re-entry around one logical wait.
A second independent root on the same host showed the same mechanism at smaller scale:
This confirms that the defect is not specific to the original Windows Desktop surface or to one task. The generic code-mode wrapper exposes internal waiting as repeated parent/model boundaries.
Expected behavior remains one runtime-managed wait operation. Internal yields and timer re-arming must not return control to the model when no agent state changed. The requested timeout should cover the whole logical operation, including the outer wrapper.
No prompts, task identifiers, repository names, local paths, account data, or credentials are included.
lunara69-ctrl commentedon Aug 24, 2026
I can confirm this behavior from an independent Linux Codex CLI audit.
In one long-running session I measured 58 WAIT/POLL continuations that produced no new useful work. Those turns accounted for 6,787,563 input tokens in total, or about 117k input tokens per wait on average.
The important observation is the same as in this report: the expensive part is not the wall-clock waiting itself. Each timeout/yield returns control to the parent model and causes another inference over the accumulated parent context.
In our traces, the cost therefore grew with session/context size even though the semantic operation was effectively just “is the worker finished yet?”.
This makes wait orchestration a context-lifecycle issue, not merely a timeout-duration issue.
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
tompassarelli commentedon Aug 31, 2026
Downstream root-fix evidence for the invalid wait-surface part of this issue (no PR).
On the 0.151.0 line, the code-mode
waittool is model-visible even when no yielded exec cell exists, and itscell_idschema accepts any string. In long supervisor sessions this lets the model select exec-cell waiting for agent lifecycle waits and invent or reuse invalid handles; one retained matched episode contained 304 invalid-handle wait calls.Minimal tool-plan reproduction:
waitis still visible.wait.cell_idremains an unconstrained string.waitremains visible.Downstream root repair:
waittool when the live yielded-cell registry is empty;cell_idas an enum containing only the exact live cell IDs;Downstream commit:
2e2a266d2987574fb63d991a06a543170d73d262(based on the collaboration-routing repair06b0cc29d668f787bc1fe856f44091e2bf44a1fe).Focused evidence: the exact schema regression and the absent/present/absent lifecycle regression each passed once. The patched binary is activated locally. A fresh model-visible canary is not yet evidence because local account authentication failed before inference; no behavioral claim is made from that attempt.
This complements keeping
wait_agentas a direct runtime control operation. It removes the invalid genericwaitchoice before handler invocation instead of relying on prompt guidance or retry policy.Downstream retirement condition: an upstream Codex release containing equivalent construction-time omission plus exact live-cell ID enumeration, with the same absent/present/absent regression coverage.
fix(code-mode): stop direct-tool routing loops
rezzminator commentedon Sep 5, 2026
This needs to be treated as a runtime orchestration defect with a token-consumption impact, not dismissed as cosmetic progress-message noise. A healthy child doing its job should not require the parent to repeatedly run inference just to announce that nothing has changed. That is excessive token consumption for zero useful work, and users should not have to reverse-engineer the harness to stop it.
I investigated an adjacent direct v2 wait path on Codex CLI 0.153.3 and inspected source at
ddf04ad26789d040f9ef6a96736f76602e35a6cc. This is additional evidence for the parent-resampling problem, not a claim that I reproduced this issue's exact nested v1 wrapper path.Observed evidence:
developer_instructionsexception and v2 wait configuration, one requested 750,000 ms wait returned on child completion after 187.005 seconds, withtimed_out: false. There were no parent status queries in that corrected run. A separate fresh-chat check using only the global configuration also received the final notification automatically.activity_rx.changed()undertimeout_at). The wasted inference is at the model-visible timeout/retry boundary; the underlying wait does not need to busy-poll.Every unnecessary parent continuation processes the accumulated context again. This creates excessive token consumption, context/history growth, and avoidable latency for what should be a suspended runtime operation. Other comments here quantify that repeated processing. My experiment did not measure billing or establish a dollar amount, and cached input should not be presented as uncached billing.
The local mitigation used:
It also required a developer-level exception allowing mailbox-interruptible waits to exceed the general 60-second wait/commentary guidance. Increasing the configured default alone does not help when the model explicitly requests a short timeout. This remains a finite, instruction-dependent mitigation, not a runtime fix.
Please assign an owner and ship a regression-tested fix that:
“Just tell the model to wait longer” is not an adequate product fix. The runtime already has event notification machinery. Users should not be paying the context-processing cost of repeatedly asking whether a notification has arrived.
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops
fix(code-mode): stop direct-tool routing loops