Skip to content

Background process polling wastes tokens: each write_stdin poll triggers full API turn with complete history #13733

Open

Description

@jitlabs-sg

Problem

When a background process is running (e.g. cargo build, cargo test), Codex enters a polling loop where each status check triggers a full API round-trip with the entire conversation history. This burns tokens/credits proportional to history size × poll count, even though no meaningful work is being done.

Root Cause (from source analysis)

The turn loop in codex.rs:4869 works as follows:

  1. Model issues exec_command → local process spawns → partial output returned
  2. needs_follow_up = true (stream_events_utils.rs:133)
  3. Loop back → clone_history().for_prompt() sends full history to API (codex.rs:4901-4905)
  4. Model sees process still running → issues write_stdin with empty input to poll
  5. process_manager.rs waits MIN_EMPTY_YIELD_TIME_MS (5s) then returns "(no new output)"
  6. needs_follow_up = true again → goto step 3

Each poll = 1 full API request with complete conversation history. A 60-second cargo build generates ~12 polling turns. With 250+ items in history, this is extremely wasteful.

The model also "reasons" about whether to wait or do something else during each poll, consuming additional output tokens for no useful purpose.

Observed Behavior

From proxy debug logs, a single Codex session shows items growing: 237 → 239 → 242 → ... → 300+ over routine tool calls. Each turn re-transmits the entire history. During background waits, the cadence is ~10s per poll with no substantive new content.

Confirmed by Community

Issue #10957 reporter noted:

"jobs are running in background but not sure implementation is very 'token' friendly as it keep polling and reasoning about it instead of just waiting"

Proposed Solutions

Option A: Local wait before API turn (preferred)

When write_stdin returns no new output and the process hasn't exited, do not set needs_follow_up = true. Instead, wait locally (30-60s configurable) and only re-poll the API when:

  • The process produces new output, OR
  • The process exits, OR
  • A timeout expires

This could be implemented in the tool handler layer — if write_stdin response has no output and no exit code, sleep locally before returning, rather than bouncing back to the model.

Option B: Increase MIN_EMPTY_YIELD_TIME_MS

Change MIN_EMPTY_YIELD_TIME_MS from 5s to 30-60s for truly empty polls. This is a minimal change but still wastes one API turn per interval.

Option C: Batch tool results

When the model issues write_stdin for a process that has no new output, return the tool result without setting needs_follow_up. The model only gets called again when the process finishes or produces output (via a notification/callback mechanism).

Environment

  • Codex CLI via Responses API proxy
  • Observed on both macOS and Windows (Windows worse due to slower builds)
  • History sizes: 200-300+ items typical for medium sessions
  • GAC (proxy) bills per payload KB, making this especially costly

Related Issues

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    tool-callsIssues related to tool calling
    rate-limitsIssues related to rate limits, quotas, and token usage reporting
    on Mar 6, 2026
  2. github-actions commented on Mar 6, 2026

    @github-actions
    Contributor

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

    Powered by Codex Action

  3. vjain419 commented on Mar 6, 2026

    @vjain419
    Contributor

    Please multiple team member sees this issue best workaround I found is use a subagent to not mess up with main agent context.

  4. happyme531 commented on Mar 6, 2026

    @happyme531

    I completely agree with this issue, but the token waste is only half the problem. The biggest dealbreaker here is that the AI literally "loses patience" and abandons long-running tasks.

    While the token burn is awful, the current polling behavior completely breaks down on heavy, time-consuming commands (like a large compilation or a massive test suite that takes 20-30 minutes).

    Here is what happens:

    • The agent starts the command in the background.
    • It polls write_stdin 10 to 15 times over the span of a few minutes.
    • Because the process is still running and returning nothing new, the AI agent gets "impatient" (likely hitting a maximum iteration limit or hallucinating that the process is hung).
    • It abruptly stops checking, assumes the task failed, or just drops the context entirely.

    It is incredibly frustrating to wait for a 30-minute build, only to realize the AI gave up on monitoring it 5 minutes in.

    For single, synchronous, or known long-running tasks, the agent should stop abusing the background terminal feature, and simply execute them in the foreground.

  5. miraclebakelaser commented on Mar 6, 2026

    @miraclebakelaser
    Contributor

    @jitlabs-sg can't you already do option B with background_terminal_max_timeout in config.toml?

  6. kcosr commented on Mar 7, 2026

    @kcosr
    • It polls write_stdin 10 to 15 times over the span of a few minutes.

    You're lucky. It's polling every second (yield_time_ms=1000) by default for my tasks. I can't use it for background tasks that don't produce frequent output. I've even added heartbeat output and the model ignores it and aborts because "it's taking too long to get the final output."

  7. robertmsale commented on Mar 7, 2026

    @robertmsale

    All of the reasons everyone put in this issue are extremely valid and relevant, but I would like to add something to this.

    I found a workaround by using MCP servers for long-running commands, but the problem is you have to develop your own sandboxing and agent thread bookkeeping. Basically, the only way to ensure long-running commands run until completion is by routing them through an MCP server that can be abused or avoided, or just create a bunch of friction.

    They should at least allow a config to raise the minimum yield time so that we can raise it to whatever we want, such as 2 billion years. That would be better than the way it currently works.

  8. robertmsale commented on Mar 8, 2026

    @robertmsale

    You guys are gonna get a kick out of this.

    Codex searches your PATH for the shell you're using, and assigns that path to the PTY that the agent will use for the rest of the program. There is no configurable way to change the polling frequency, and in unified_exec=false mode there is no way to raise the command execution cap beyond 10 minutes (also the agent can set arbitrarily low timeouts, which it does fairly often, and runs the command multiple times up to the 10 min cap before giving up or deciding the timeout is a codebase issue and tries to fix non-problems). However, you can trick Codex (the application) into loading a shell that produces a job ID with every command execution that the agent sees, and with the MCP server they can pass that job ID and wait until that command completes. On mac the process looks like this:

    1. Create a folder somewhere (e.g. /opt/homebrew/shim on my system)
    2. Symbolically link the launch-job script, essentially a passthrough to zsh with recursion-safe job ID wrapper into that folder as zsh
    3. Launch codex with env PATH="/path/to/shim:$PATH" codex

    The skill instructions tell them that all commands produce a job ID whether they like it or not, and if the command is not finished they use the MCP to wait until the command finishes. Set the tool call timeout to some absurdly high value and that's all she wrote!

    Past couple days have been using this with extremely good results. Wasting tokens is not the biggest issue, as others have pointed out, it's the decisions the agent makes when a process runs for what they perceive as "too long", and no amount of prompt engineering seems to prevent them from eventually terminating a good process (besides tediously steering the agent). Also this prevents the spinlock/reawakening so it really does save tokens.

    Anyways, figured I'd share this little workaround incase OpenAI decides to close this as not planned (lack of votes or otherwise), or it just takes months to land on an agreed solution + implementation. Not practical, preserved for historical reference only

    Looks like this in practice:
    Image

  9. SproutSeeds commented on Mar 16, 2026

    @SproutSeeds

    I pushed on the narrowest tool-side fix locally, and my read is that this issue is real.

    One important detail from the current code: background_terminal_max_timeout does not currently do what people in this thread seem to expect for empty polls. In the write_stdin path, empty polls still effectively use the short fixed window because the requested yield is clamped into the 5s .. background_terminal_max_timeout range. So when the model asks for something like 1000ms, the actual empty-poll wait is still 5000ms, not the configured background timeout.

    Locally, I patched the unified-exec path so that:

    • empty write_stdin polls wait up to background_terminal_max_timeout
    • empty polls return as soon as fresh output arrives or the process exits
    • non-empty writes keep the shorter interactive clamp

    That means a single empty poll can bridge a long silent period without bouncing back to the model every 5 seconds, but it still yields promptly once there is something useful to send back.

    I also added a regression that crosses the old 5-second ceiling (sleep 5.5 && echo ...) and verifies that one empty poll captures the delayed output. Targeted codex-core tests passed locally.

    I ran a full workspace cargo test too. It was not fully green on this machine, but the new unified-exec regression passed in the full codex_core run, and several non-unified failures from that workspace run passed when rerun in isolation. The only still-reproducible blocker I hit separately was the existing seatbelt test pair.

    So my current read is:

    • the issue is real
    • background_terminal_max_timeout is currently only an upper bound, not the actual empty-poll wait window people expect
    • a narrow tool-side fix appears viable without changing the overall follow-up loop
  10. added
    agentIssues related to the core agent loop
    and removed
    CLIIssues related to the Codex CLI
    on Mar 16, 2026
  11. chernistry commented on Apr 6, 2026

    @chernistry

    This is fundamentally an architecture problem, not a tuning problem. The core issue is that scheduling decisions (should I wait for this process? how long?) are being routed through the LLM, which means every "should I keep waiting?" check costs a full API round-trip with the entire conversation history re-serialized. Adjusting MIN_EMPTY_YIELD_TIME_MS from 5s to 30s (Option B) just makes the bleeding slower — you still pay for reasoning tokens the model burns "deciding" to keep waiting, which is a foregone conclusion.

    We hit this exact class of problem when building our multi-agent orchestrator. The solution was separating the control plane from the inference plane completely. The orchestrator is deterministic Python code — it never calls an LLM. It spawns agents as subprocesses, then checks liveness with proc.poll() (literally a non-blocking syscall that returns the exit code or None). The tick loop runs every ~30 seconds, calls check_alive() on each agent session, and reaps dead ones. Zero tokens consumed for scheduling. Zero API calls for "is my build done yet?"

    The key architectural insight: process lifecycle management is not an LLM problem. An LLM cannot wait more efficiently than a select() call or a PID check. When you route "is process X still running?" through the model, you're paying for the model to parse the question, "reason" about it (consuming output tokens for a trivially deterministic answer), and emit a tool call — all while re-transmitting the full conversation context. With 250+ history items at ~10s cadence during a 60-second build, that's ~6 full-context API calls doing nothing.

    Option C in the issue description is closest to the right fix. But I'd go further: the tool handler layer should own the entire wait lifecycle. When exec_command spawns a background process, the local runtime should:

    1. Register the PID in a local process table
    2. Return immediately to the model with "process started, PID N"
    3. Watch for exit locally (epoll/kqueue, not polling the API)
    4. Only re-enter the model when there's actual new information: process exited (with exit code + final output), or meaningful stdout was produced

    This is effectively an event-driven architecture instead of a polling one. The model gets called when there's something to think about, not to ask "anything new?"

    Related patterns that help in practice:

    Per-task model routing. We assign a complexity tier to each task and route to an appropriate model. A trivial formatting fix doesn't need the same model or token budget as a multi-file refactor. This prevents the secondary waste where an expensive model is burning tokens on wait-poll cycles for work that shouldn't have used that model in the first place.

    Token growth monitoring. We sample cumulative token consumption per agent every 30 seconds and detect quadratic growth patterns — when the per-interval token delta doubles across consecutive windows, it signals unbounded context accumulation (exactly what the polling loop produces). Agents that exceed a token threshold with zero file changes get auto-killed. A circuit breaker guards against infinite compaction loops when context windows fill up.

    Cost anomaly detection. Per-task cost ceilings relative to the complexity tier median, burn-rate tracking against budget, output/input token ratio alerting, and retry-spiral detection (cumulative retry cost exceeding a multiplier of the first attempt). These catch exactly the scenario described here — a routine build-and-test cycle silently burning 10x the expected tokens.

    The proxy billing model (per payload KB) makes this even worse since you're paying quadratically: each poll re-sends the growing history, so poll cost increases over the session lifetime. With N polls and history growing by ~3 items per poll, total payload is roughly proportional to N^2.

    For anyone dealing with this now: if you can intercept at the proxy layer, you could short-circuit write_stdin tool calls that return "(no new output)" by caching and replaying the response without forwarding to the API. Ugly, but it stops the bleeding while the proper fix ships.

    We've been iterating on these patterns in Bernstein (https://github.com/chernistry/bernstein) — the token monitor and cost anomaly detector specifically exist because LLM-mediated polling is a recurring footgun in agent orchestration.

  12. 30 remaining items

  13. PFCLEEAI commented on Jul 20, 2026

    @PFCLEEAI

    I have a quantified multi-session reproduction that extends this issue from single-process polling to coordinator/worker orchestration.

    Environment:

    • codex-cli 0.144.6
    • gpt-5.6-sol
    • 258,400-token context window
    • macOS
    • Three sessions: coordinator, completed worker reused as supervisor, and active Unity worker

    I parsed the local JSONL logs using:

    • response_item as a response packet
    • payload.type == reasoning as a model reasoning step
    • SHA/manifest-related tool requests by matching shasum, SHA-256, artifact hashes, candidate manifests, and hash manifests

    Adjacent-window comparison:

    Functional code/test window:

    • 510 response packets
    • 170 reasoning steps
    • 149 tool requests
    • 23.15M total_tokens

    Five minutes after the worker explicitly froze source changes and Unity runs:

    • 203 response packets
    • 61 reasoning steps
    • 66 tool requests
    • 13 SHA/manifest-related tool requests
    • 9.02M total_tokens
    • 8.82M cached input tokens (97.8%)
    • 64.5% of those raw tokens came from the coordinator and already-completed supervisor, rather than the active worker

    Important correction: reasoning steps per packet did not increase (33.3% -> 30.0%). Instead:

    • response packet rate increased 11.1%
    • tool-request rate increased 23.6%
    • SHA/manifest tool share increased from 0.67% to 19.70% (29.3x)
    • total tokens per reasoning step increased 8.6%

    This suggests the primary amplification is repeated full-history/cache replay caused by tool polling and cross-session supervision, not additional useful reasoning.

    Expected behavior:

    1. Worker completion should notify the coordinator without model-driven polling.
    2. Completed workers should become idle and not supervise replacement workers.
    3. Status/wait operations should have a no-model or lightweight-context path.
    4. Codex should expose per-lane poll, packet, and cached-token metrics.
    5. Idle/finished workers should have an automatic lifecycle stop.

    Affected local thread IDs can be uploaded with /feedback:

    • coordinator: 019f7d0e-3f38-7301-a471-5ea0bd2c7db1
    • completed supervisor: 019f7ea2-fd97-7401-b7ec-847e61af721b
    • runtime worker: 019f7edd-a5f5-7952-9c44-2c440960900c
  14. robertmsale commented on Jul 21, 2026

    @robertmsale

    I found a pretty elegant combination of solutions that doesn't involve janky MCPs or custom builds.

    Then each project has a $build-expectations skill. You go through all the different build/test commands, time them, and record approximately how long they take, and tell the agent the initial poll should be the recorded minimum, and remaining polls can be every 30s or whatever. This setup is codex version agnostic and in my testing I've observed ~27% fewer input tokens on average (not a huge win but better than nothing) from tool calls. Since the agent can choose the polling time, the solution is making them aware of expectations in advance.

    I'm satisfied with this solution. It's made unified_exec very cost effective and usable for me. But one thing that might make it more powerful is if the PreToolUse hook allowed for "yield_time_ms" to be rewritable and can match write_stdin tool calls. That way the agent doesn't need to be aware of the initial poll period. You can set it up so certain commands in the project have a minimum first poll time, and remaining polls can use max(yield_time_ms, project_min_ms) defined by a hook or some other durable source. I think hooks are the key affordance for keeping the underlying implementation general purpose while users can tune behavior themselves.

  15. rbjarnason commented on Jul 28, 2026

    @rbjarnason

    While this is manageable with a human using Codex, then for our AI scientist project that uses Codex App Server in a larger agent harness, then this was not sustainable. Here is a opinionated patch that we use for our project that prevents Codex busy-polling. I'm sharing this here in case it would be useful for someone, this is not a general solution and I hope OpenAI fixes this soon. https://gist.github.com/rbjarnason/5014b19e9326bdea782be4c994b2dc60

  16. rogersau commented on Jul 28, 2026

    @rogersau

    Confirmed on Codex CLI 0.145.0, running through T3 Code via Codex app-server on Linux.

    I inspected one rollout lasting approximately 103 minutes:

    • 23,680,005 total recorded tokens
    • 23,623,380 input tokens
    • 22,827,776 cached input tokens (96.6%)
    • 51 write_stdin calls, all sending empty input
    • 46 calls used yield_time_ms: 1000
    • 5 calls used yield_time_ms: 30000
    • Model turns associated with write_stdin processed approximately 8.05M input tokens—about 34% of the session's input

    The one-second calls were not interacting with the process; they were repeatedly checking whether it had produced output or completed. Each check returned control to the model and caused another large-context inference.

    This confirms the issue remains present in 0.145.0 and also affects third-party clients using Codex app-server, rather than only the native CLI/Desktop UI.

    Expected behaviour: an empty wait should remain inside the runtime until output, process completion, user interruption, or a reasonably long timeout. An unchanged process state should not require another model turn every second.

  17. naowalrahman commented on Aug 3, 2026

    @naowalrahman

    I built an MCP server to address part of this issue. In particular, my workflow involves monitoring output on an HPC cluster for long periods, and Claude Code's monitor tool is really useful for that, so I tried to replicate it. The code is actually somewhat irrelevant -- it's really just a fancy wait. But in ~/.codex/config.toml you can define a custom timeout length for an MCP server's tool calls with tool_timeout_sec and that is separate from yield_time_ms for the exec tool.

    I suppose another workaround for long-running shell commands could be just having a simple bash MCP server that bypasses the regular exec tool.

  18. majiayu000 commented on Aug 9, 2026

    @majiayu000

    Additional quantified evidence from a macOS workstation, collected from all local Codex session JSONL records over a continuous 17-hour window (2026-08-08 19:53 to 2026-08-09 12:53, Asia/Shanghai).

    Environment:

    • macOS
    • GPT-5.6 Sol, mostly high reasoning effort
    • 18 sessions across 6 working directories
    • 10,932 model/API requests analyzed request by request

    Results:

    Metric All requests Empty polling requests Polling share
    Request count 10,932 3,100 28%
    Input tokens 1.513B 457M 30%
    Uncached input tokens 27.75M 5.15M 19%
    Output tokens 2.40M 0.23M 10%

    The narrower five-session sample that contained the long-running repository jobs showed 3,078 empty polls among 6,358 tool calls (48%). The two percentages use different denominators: 28% is all API requests across all 18 sessions; 48% is tool calls in the five long-running sessions.

    Worst observed cases:

    • One command was polled 127 times at roughly 30-second intervals, lasting about 64 minutes.
    • Another session issued 164 polls with yield_time_ms=1000.
    • Across the full sample, 1,619 polls requested 30 seconds, 671 requested 10 seconds, and 406 requested 55 seconds.

    An important configuration observation: background_terminal_max_timeout was already set to 1,800,000 ms (30 minutes) during this run. The model still explicitly requested 1-second, 10-second, and 30-second waits. This supports the point raised above that a maximum timeout alone does not solve the problem; it neither supplies nor enforces a useful empty-poll default/minimum.

    The practical local mitigation is now:

    • reset background_terminal_max_timeout to the documented 300,000 ms default;
    • prohibit 1-second and 10-second empty polls through global agent guidance;
    • use 55-60 seconds for interactive polling;
    • stop polling after two empty results and do independent work;
    • disable proactive multi-agent execution and move broad verification to CI where possible.

    These are only mitigations. The desired fix remains runtime-owned, event-driven completion: keep process liveness and waiting in the local control plane, and re-enter the model only when stdout/stderr changes, the process exits, or a real deadline is reached. This would remove the API/model turn from the deterministic question "is the process done yet?" and should also reduce compaction pressure and premature abandonment of healthy long-running commands.

  19. naipi11 commented on Aug 18, 2026

    @naipi11

    Empty write_stdin polls are billed as full model turns with the whole history. A long cargo build / test run can spend a large share of the session on "(no new output)" checks.

    Method:

    1. Do not let the agent watch a long command through repeated empty write_stdin polls.
    2. Run the long job in a normal terminal, or have the agent write output to a file and return.
    3. If the agent must wait, tell it to use one long wait (tens of seconds) instead of 1s polls, then read the file / exit code once.
    4. After a long job, start a fresh thread before more work so later turns do not keep resending that history.

    Evidence: current-thread measurements include a 103-minute rollout with 51 empty write_stdin polls and millions of input tokens, plus a 17-hour local sample where empty polls were ~28% of requests / ~30% of input tokens.

    Independent community workaround; not an official OpenAI fix.

  20. weathon commented on Aug 24, 2026

    @weathon

    Yes, I really want something like Claude Code where it can start a process and keep it running in background and get notified when it finishes. I am running LLM training and it takes weeks or months, I do not need it keep checking it every 30s. I already put it in AGENTS.md that it should not use it but it doesn't work. For tasks with long logs, this also make it very expensive, every time it read 10k token log, I think this is the reason my usage is draining so fast.

    I now ask it to run it in tmux but it just started two tmux one for running another one for monitoring

    Edit: Somehow i get it to run in background without blocking the conversation and no polling

    Image Codex said it is executed with exec_command and when the time passes yield_time_ms
  21. tiago-peixoto commented on Sep 6, 2026

    @tiago-peixoto

    Thanks for tracing the model-turn cost here and for the maintainer discussion about a runtime-owned watcher. I have a current, version-pinned reproduction that adds two details: the documented long empty-poll window is not taking effect in this build, and increasing the caller's requested timeout did not reduce the production polling rate.

    Environment: codex-cli 0.153.4 on macOS.

    Measured tool calls:

    Call Requested Actually yielded after Returned
    exec_command({cmd: "sleep 200", yield_time_ms: 300000}) 300,000 ms 30.002 s empty output; command still running
    write_stdin({chars: "", yield_time_ms: 290000}) 290,000 ms 31.0 s nothing; command still running
    code-mode wait({cell_id, yield_time_ms: 60000}) 60,000 ms 61.0 s nothing; command still running

    The first probe cost 2 model requests. The second cost 6 model requests for one 200-second wait.

    Minimal reproduction:

    1. Start sleep 200 with exec_command, requesting yield_time_ms: 300000.
    2. When it yields after about 30 seconds, poll the returned session with empty chars and yield_time_ms: 290000.
    3. Observe that the empty poll returns after about 31 seconds with no output and no exit status, rather than remaining local for the requested interval.
    4. In code mode, run the same command through exec and resume its returned cell with wait({cell_id, yield_time_ms: 60000}); that path yields after about 61 seconds.

    This appears inconsistent with the current configuration reference, which documents background_terminal_max_timeout as the maximum empty-write_stdin poll window, with a 300,000 ms default and no stated upper bound. That default was in force. I also could not find documented [code_mode] yield settings.

    The impact is visible beyond the synthetic probe. Production runs already used 30-minute checkpoints and still spent 60 to 63 model requests per quiet hour. Across 26 isolated idle stretches (10.8 idle hours, 1,227 requests), the rate was 113.4 monitoring-driven model requests per idle hour. Over two days, 6,753 requests were spent, about 26.7 percent of estimated cost, on model turns that waited and received no new information. Moving the same workload to a runtime where waiting is not model-driven changed the monitoring share from 30.3 percent of all requests to 0.1 percent.

    This seems to reinforce the runtime-owned, event-driven watcher direction discussed above rather than another timeout-only mitigation. Is that still the approach maintainers want contributors to pursue? If so, I am happy to help with an implementation after the intended API and ownership boundary are confirmed.

  22. chernistry commented on Sep 9, 2026

    @chernistry

    The three rows in @tiago-peixoto's table do not have the same cause, and separating them narrows where to look. Reading the code at tag rust-v0.153.4, the version measured:

    Row 1 is what the code does by design. background_terminal_max_timeout is not consulted on the exec_command path at all. exec_command puts its requested yield through clamp_yield_time():

    // codex-rs/core/src/unified_exec/mod.rs
    pub(crate) const MIN_YIELD_TIME_MS: u64 = 250;
    pub(crate) const MAX_YIELD_TIME_MS: u64 = 30_000;
    
    pub(crate) fn clamp_yield_time(yield_time_ms: u64) -> u64 { ...
        yield_time_ms.clamp(MIN_YIELD_TIME_MS, MAX_YIELD_TIME_MS)
    }

    called from process_manager.rs::exec_command. So the first yield of any background command is capped at 30s regardless of what is requested or configured, which matches the measured 30.002s exactly. That reads as a documentation mismatch rather than a bug: the config reference describes background_terminal_max_timeout without noting that it does not govern exec_command.

    Row 2 is a different thing. On the empty-write_stdin path the same tag does honour the setting:

    // codex-rs/core/src/unified_exec/process_manager.rs
    let time_ms = request.yield_time_ms.max(MIN_YIELD_TIME_MS);
    if request.input.is_empty() {
        time_ms.clamp(MIN_EMPTY_YIELD_TIME_MS, self.max_write_stdin_yield_time_ms)
    } else {
        time_ms.min(MAX_YIELD_TIME_MS)
    }

    max_write_stdin_yield_time_ms is set once per session in session.rs from UnifiedExecProcessManager::new(config.background_terminal_max_timeout), and the write_stdin handler passes chars straight through as input. With the documented 300,000ms default and a 290,000ms request, that expression evaluates to 290,000. The measured 31.0s is instead within a second of MAX_YIELD_TIME_MS, which is the ceiling on the other branch. So on that build either the empty branch was not taken or the manager was constructed with a smaller bound than the documented default.

    That distinction matters for the question being asked here, because row 2 decides whether the empty-poll case needs a new mechanism or is a wiring problem in an existing one. A cheap way to tell them apart without patching anything: run the same empty poll on a command that produces no output, and check whether the ~30s ceiling holds when background_terminal_max_timeout is set explicitly in config.toml rather than left to the default. If it still returns at ~30s with an explicit value, the configured bound is not reaching the process manager.

    Row 1 is separable from the larger design discussion and cheap either way: exec_command could take the same bound as the empty-poll branch instead of the fixed 30s cap, or the config reference could state the current limit. Today the first yield of every background start costs a model turn that no configuration can avoid.

  23. LawrentChen commented on Sep 21, 2026

    @LawrentChen

    Hi guys. I am not a professional developer, but I made my tiny solution (a skill) for this polling issue in long-run tasks.

    https://github.com/LawrentChen/codex_callback

    For me it works pretty well. It allows the thread to be smoothly awakened and take over each time(can be more than once) the long-run task encounters a problem, and after completing phased tasks, it smoothly transitions to downstream docking tasks, requiring no polling or human intervention.

    I use codex app and ssh to my developing server. It just works fine.

    Take a look if you are interested.

  24. LawrentChen commented on Sep 22, 2026

    @LawrentChen

    #14110 (comment)
    /goal can be a better way

  25. PHPCraftdream commented on Oct 1, 2026

    @PHPCraftdream

    The separation @chernistry made between the initial exec yield and the empty-poll window is important: increasing a timeout is not the same intervention as removing model turns from waiting. @naowalrahman's suggestion of using an MCP-owned wait is the direction we used for an interim command runner.

    I maintain cc-arch-hands; its cli-run integration takes the polling question out of the model's path for commands launched through that tool. A detached Node worker owns the actual child processes and writes combined stdout/stderr plus structured exit results to disk. In the default queue delivery, the launch call returns a task name and UID, the parent turn can end, and one codex queue message is attempted after the whole run reaches completion. The worker does not periodically ask the model whether the commands are still running. The model can inspect saved results after notification rather than repeatedly consuming empty write_stdin responses.

    The packaged option is available now:

    npx cc-arch-hands@0.15.1 install --codex-skills

    It installs a Codex-only skill bundle, MCP configuration, and a marked usage-guidance section; restart Codex afterward. The source and coverage table are here.

    For this issue, I would distinguish the architectural effect from a cost claim. We have not run a controlled before/after token-cost benchmark, so I am not claiming a particular percentage reduction or applying the measurements above to our package. The narrower claim is that this worker's wait does not require a stream of model-mediated status checks. Users should not reintroduce that stream by polling the provided status tool; it is for requested inspection or recovering a failed notification.

    Limitations remain: native exec_command waits, other tools, and autonomous goal continuations are unchanged. Queue access to the target thread is required, and an unavailable thread can leave delivery unsuccessful/pending. The alternative inline mode holds the MCP call open until results are ready, so it has different timeout/lifecycle constraints and is not an idle wakeup.

    This is also not a transparent sandboxed substitute for exec_command: MCP jobs run outside Codex's per-turn filesystem sandbox, and arbitrary command execution needs user approval or explicit trust. A runtime-owned wait in upstream Codex would remove both this integration burden and the native polling overhead; that remains the fix I would want.

  26. renesugar commented on Oct 2, 2026

    @renesugar

    I ran a deterministic 61m30s local sleeper to measure this terminal-polling behavior against ChatGPT subscription telemetry. The harness and analysis with individual reports are now public.

    The following Codex trials verified both host and CLI as 0.160.0 and used the default service tier:

    Condition 5-hour remaining Weekly remaining Estimated pre-completion parent rounds
    Astra/low baseline 60% → 45% (15 pp drain) 87% → 85% (2 pp) 26
    Astra/low deterministic wait mitigation 98% → 95% (3 pp) 84% → 83% (1 pp) 1
    GPT-6.1 Sol/max baseline 100% → 97% (3 pp) 82% → 81% (1 pp) 26

    Each target completed 3690 seconds without restart, duplicate, or fail-safe. Trials were sequential and no subscription reset crossed an individual measurement. The mitigation kept Astra as the parent: JavaScript re-armed 13 terminal waits inside one Code Mode execution, rather than returning to the model after every terminal timeout. No monitoring subagent was used in these Codex trials.

    The baseline sampling records occurred strictly before the sleeper's completion marker. This supports unnecessary model-mediated waiting as an identified client efficiency problem, rather than inference being inferred solely from quota movement. Sol reduced observed quota drain while the same estimated number of model rounds remained.

    These are subscriber-window snapshots, not API-dollar estimates or a per-request server ledger. Round counts are best effort. Model/effort and starting headroom differed; a reset occurred between the repeated Astra trials. Each Codex trial included one host-required hourly status message and short snapshot boundary intervals. The analysis documents these limits, pinned source references, additional trials, and the distinction between local tool execution and remote inference.

    Related: #35259, #42981, #32188.

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

    bugSomething isn't workingrate-limitsIssues related to rate limits, quotas, and token usage reportingsessionIssues involving session (thread) management, resuming, forking, naming, archivingtool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions