[Codex Desktop] Nested functions.exec wait_agent polling causes repeated parent turns and token usage

Open 💬 3 comments Opened Jul 24, 2026 by czwaxm
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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:

  1. the outer functions.exec yields with Script running with cell ID ...;
  2. the parent then invokes functions.wait;
  3. the wait returns timed_out: true;
  4. 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_agent through generic functions.exec
  • Multi-agent version: v1

Reproduction

  1. Spawn a subagent and keep its work running longer than the outer code-mode yield interval.
  2. In the parent, invoke the wait through a generic functions.exec wrapper:

~~~javascript
const result = await tools.multi_agent_v1__wait_agent({
targets: [agentId],
timeout_ms: 60000,
});
text(result);
~~~

  1. In the observed run:
  • the nested wait_agent started with timeout_ms: 60000;
  • after approximately 11 seconds, the outer call returned Script running with cell ID 29;
  • the parent then called functions.wait for the cell;
  • approximately 50 seconds later, functions.wait returned {"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_ms should bound the complete multi-agent wait operation, including the outer tool wrapper.

Actual behavior

  • One logical wait_agent operation 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_agent receives an explicit 60-second timeout.

Related issues

  • #18394 — default wait_agent timeout is hardcoded to approximately 30 seconds and causes polling loops.
  • #32640 — the built-in wait tool is capped at approximately 50 seconds and re-samples the full context on each expiry.
  • #29122 — long-running nested tools routed through functions.exec consume 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_ms may 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.

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #34468

Powered by Codex Action

nos1609 · 6 days ago

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:

  • Rocky Linux 10.2, x86_64
  • VS Code Web with the OpenAI extension 26.818.32112
  • Codex CLI 0.149.0
  • Direct V1 path: functions.exec -> multi_agent_v1__wait_agent -> functions.wait

In one task family during a fixed 24-hour window:

  • 173 nested multi_agent_v1__wait_agent operations;
  • 111 outer functions.exec calls yielded Script running with cell ID ...;
  • those logical waits required 259 outer functions.wait polls;
  • 87 outer polls returned timed_out: true;
  • one logical wait required as many as 14 outer polls before resolution.

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:

  • 29 nested V1 waits;
  • 16 yielded cells;
  • 30 outer polls;
  • 11 timeout returns.

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 · 4 days ago

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.