[Codex Desktop] Nested functions.exec wait_agent polling causes repeated parent turns and token usage
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:
~~~javascript
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}.
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:
~~~text
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
- #18394 — default
wait_agenttimeout is hardcoded to approximately 30 seconds and causes polling loops. - #32640 — the built-in
waittool is capped at approximately 50 seconds and re-samples the full context on each expiry. - #29122 — long-running nested tools routed through
functions.execconsume tokens while the model repeatedly waits. - #13733 — background polling sends a full API turn with the complete history for each poll.
- #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.
3 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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:
| Boundary class | Processed tokens |
|---|---:|
| Initial V1 exec yields | 18.37M |
| Outer wait returns | 31.41M |
| Immediate exec returns | 10.01M |
| Outer timeout returns | 14.38M |
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.
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.