Skip to content

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

Open

Description

@czwaxm

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:
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:

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

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

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    windows-osIssues related to Codex on Windows systems
    subagentIssues involving subagents or multi-agent features
    tool-callsIssues related to tool calling
    on Jul 24, 2026
  2. github-actions commented on Jul 24, 2026

    @github-actions
    Contributor
  3. nos1609 commented on Aug 22, 2026

    @nos1609

    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.

  4. lunara69-ctrl commented on Aug 24, 2026

    @lunara69-ctrl

    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.

  5. tompassarelli commented on Aug 31, 2026

    @tompassarelli

    Downstream root-fix evidence for the invalid wait-surface part of this issue (no PR).

    On the 0.151.0 line, the code-mode wait tool is model-visible even when no yielded exec cell exists, and its cell_id schema 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:

    1. Build a code-mode tool router with zero live exec cells: wait is still visible.
    2. Mark one exec cell ready: wait.cell_id remains an unconstrained string.
    3. Finish the cell and rebuild the router: wait remains visible.

    Downstream root repair:

    • omit the code-mode wait tool when the live yielded-cell registry is empty;
    • when it is present, define cell_id as an enum containing only the exact live cell IDs;
    • rebuild that surface from current runtime state, giving absent -> present -> absent behavior across the cell lifecycle.

    Downstream commit: 2e2a266d2987574fb63d991a06a543170d73d262 (based on the collaboration-routing repair 06b0cc29d668f787bc1fe856f44091e2bf44a1fe).

    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_agent as a direct runtime control operation. It removes the invalid generic wait choice 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.

  6. rezzminator commented on Sep 5, 2026

    @rezzminator

    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:

    • The original parent repeatedly requested 60,000 ms waits while its child remained active, interleaving status checks/messages.
    • In a controlled AGENTS.md-only experiment, the parent explicitly cited the higher-priority developer instruction to avoid blocking waits longer than 60 seconds. It requested 60,000 ms, timed out after 60.105 seconds, and ended its initial turn. An instruction to use a long mailbox wait in AGENTS.md did not override that conflict.
    • With a targeted developer_instructions exception and v2 wait configuration, one requested 750,000 ms wait returned on child completion after 187.005 seconds, with timed_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.
    • The v2 wait handler already waits on a Tokio watch notification (activity_rx.changed() under timeout_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:

    [features.multi_agent_v2]
    wait_agent_enabled = true
    min_wait_timeout_ms = 150000
    default_wait_timeout_ms = 750000
    max_wait_timeout_ms = 1500000

    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:

    1. Keeps a healthy child's parent suspended across internal yields/heartbeat intervals, without invoking the parent model merely because an internal timer expired.
    2. Resumes on actual mailbox activity, completion/failure, user interruption, or the user's requested overall deadline; an ordinary internal wait expiry must not be interpreted as child failure or a reason to steer the child.
    3. Removes the conflict between mailbox-interruptible waits and the generic 60-second wait/commentary instructions.
    4. Tests the direct v2 path and the nested v1/code-mode path separately, asserting the number of parent model requests while no child state has changed.

    “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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appbugSomething isn't workingperformancesubagentIssues involving subagents or multi-agent featurestool-callsIssues related to tool callingwindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions