Repository navigation
Codex CLI repeatedly wakes xhigh to poll deterministic long-running jobs, exhausting finite weekly usage before task completion #45974
Description
What version of Codex CLI is running?
0.154.0
What subscription do you have?
Pro-Light
Which model were you using?
Astra-xhigh
What platform is your computer?
Darwin 25.6.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Apple Terminal (Terminal.app)
Codex doctor report
{
"schemaVersion": 1,
"generatedAt": "1789575158s since unix epoch",
"overallStatus": "ok",
"codexVersion": "0.154.0",
"checks": {
"app_server.status": {
"id": "app_server.status",
"category": "app-server",
"status": "ok",
"summary": "background server is not running",
"details": {
"control socket": "/Users/joshmcclure/.codex/app-server-control/app-server-control.sock",
"daemon state dir": "/Users/joshmcclure/.codex/app-server-daemon",
"mode": "ephemeral",
"pid file": "/Users/joshmcclure/.codex/app-server-daemon/app-server.pid (missing)",
"settings": "/Users/joshmcclure/.codex/app-server-daemon/settings.json (missing)",
"status": "not running",
"update-loop pid file": "/Users/joshmcclure/.codex/app-server-daemon/app-server-updater.pid (missing)"
},
"remediation": null,
"durationMs": 0
},
"auth.credentials": {
"id": "auth.credentials",
"category": "auth",
"status": "ok",
"summary": "auth is configured",
"details": {
"auth file": "/Users/joshmcclure/.codex/auth.json",
"auth storage mode": "File",
"stored API key": "false",
"stored ChatGPT tokens": "true",
"stored agent identity": "false",
"stored auth mode": "chatgpt"
},
"remediation": null,
"durationMs": 0
},
"config.load": {
"id": "config.load",
"category": "config",
"status": "ok",
"summary": "config loaded",
"details": {
"CODEX_HOME": "/Users/joshmcclure/.codex",
"active thread overrides": "not inspected",
"config.toml": "/Users/joshmcclure/.codex/config.toml",
"config.toml parse": "ok",
"configuration load ms": "3",
"configuration scope": "invocation config, including cloud-managed policy",
"cwd": "/Users/joshmcclure",
"enabled feature flags": "shell_tool, view_image, sleep_tool, unified_exec, unified_exec_tty, unified_exec_zsh_fork, shell_snapshot, content_item_kinds, code_mode_host, terminal_resize_reflow, sqlite, hooks, enable_request_compression, unbounded_connection_retries, multi_agent, apps, tool_search_always_defer_mcp_tools, tool_suggest, plugins, in_app_browser, in_app_chat, in_app_dictation, in_app_local_automation, in_app_updates, browser_use, browser_use_full_cdp_access, browser_use_external, computer_use, remote_plugin, plugin_sharing, image_generation, resize_all_images, item_ids, skill_mcp_dependency_install, skill_search, mentions_v2, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, auth_elicitation, personality, fast_mode, tui_app_server, remote_compaction_v2, compaction_image_budget, workspace_dependencies",
"feature flag overrides": "none",
"feature flags enabled": "48",
"log dir": "/Users/joshmcclure/.codex/log",
"mcp servers": "2",
"model": "gpt-6-astra",
"model provider": "openai",
"sqlite home": "/Users/joshmcclure/.codex"
},
"remediation": null,
"durationMs": 0
},
"desktop.app.version": {
"id": "desktop.app.version",
"category": "desktop",
"status": "ok",
"summary": "the desktop application is installed",
"details": {
"log directory": "$HOME/Library/Logs/com.openai.codex",
"running": "true",
"version": "26.908.40834"
},
"remediation": null,
"durationMs": 0
},
"desktop.app_server.handshake": {
"id": "desktop.app_server.handshake",
"category": "desktop",
"status": "ok",
"summary": "no desktop app-server handshake was recorded",
"details": {},
"remediation": null,
"durationMs": 0
},
"desktop.security.enforcement": {
"id": "desktop.security.enforcement",
"category": "desktop",
"status": "ok",
"summary": "the desktop application passed available macos security assessments",
"details": {
"gatekeeper": "accepted"
},
"remediation": null,
"durationMs": 0
},
"git.environment": {
"id": "git.environment",
"category": "git",
"status": "ok",
"summary": "git executable found; execution not verified",
"details": {
"PATH git #1": "/usr/bin/git",
"PATH git entries": "1",
"git execution": "not inspected (PATH helpers are not executed)",
"repo detected": "false",
"selected git": "/usr/bin/git"
},
"remediation": null,
"durationMs": 0
},
"installation": {
"id": "installation",
"category": "install",
"status": "ok",
"summary": "installation looks consistent",
"details": {
"PATH codex #1": "/opt/homebrew/bin/codex",
"current executable": "/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin/codex",
"install context": "npm (package /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin, bin /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin, resources /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-resources, path /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path)",
"managed by Vite+": "false",
"managed by bun": "false",
"managed by npm": "true",
"managed by pnpm": "false",
"managed package root": "/opt/homebrew/lib/node_modules/@openai/codex",
"npm update target": "not inspected (PATH helpers are not executed)"
},
"remediation": null,
"durationMs": 0
},
"mcp.config": {
"id": "mcp.config",
"category": "mcp",
"status": "ok",
"summary": "MCP configuration is locally consistent",
"details": {
"configured servers": "2",
"disabled servers": "1",
"stdio servers": "2"
},
"remediation": null,
"durationMs": 0
},
"network.env": {
"id": "network.env",
"category": "network",
"status": "ok",
"summary": "network-related environment looks readable",
"details": {
"managed proxy": "not configured",
"proxy env vars": "none",
"respect system proxy": "disabled",
"system proxy": "direct"
},
"remediation": null,
"durationMs": 0
},
"network.provider_reachability": {
"id": "network.provider_reachability",
"category": "reachability",
"status": "ok",
"summary": "active provider endpoints are reachable over HTTP",
"details": {
"ChatGPT inference URL": "https://chatgpt.com/backend-api/<redacted> reachable (HTTP 405)",
"desktop assets CDN": "https://chatgpt.com/backend-api/<redacted> reachable (HTTP 200)",
"reachability mode": "ChatGPT auth"
},
"remediation": null,
"durationMs": 320
},
"network.websocket_reachability": {
"id": "network.websocket_reachability",
"category": "websocket",
"status": "ok",
"summary": "Responses WebSocket handshake succeeded",
"details": {
"DNS": "2 IPv4, 2 IPv6, first IPv6",
"auth mode": "chatgpt",
"connect timeout": "15000 ms",
"endpoint": "wss://chatgpt.com/backend-api/<redacted>",
"handshake result": "HTTP 101 Switching Protocols",
"model provider": "openai",
"provider name": "OpenAI",
"proxy env vars": "none",
"reasoning header": "false",
"server model present": "false",
"supports websockets": "true",
"wire API": "responses"
},
"remediation": null,
"durationMs": 674
},
"runtime.provenance": {
"id": "runtime.provenance",
"category": "runtime",
"status": "ok",
"summary": "running npm on macos-aarch64",
"details": {
"commit": "unknown",
"current executable": "/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin/codex",
"install method": "npm (package /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin, bin /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin, resources /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-resources, path /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path)",
"platform": "macos-aarch64",
"version": "0.154.0"
},
"remediation": null,
"durationMs": 0
},
"runtime.search": {
"id": "runtime.search",
"category": "search",
"status": "ok",
"summary": "search command found (bundled); execution not verified",
"details": {
"search command": "/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path/rg",
"search command readiness": "file exists",
"search provider": "bundled"
},
"remediation": null,
"durationMs": 0
},
"sandbox.helpers": {
"id": "sandbox.helpers",
"category": "sandbox",
"status": "ok",
"summary": "sandbox configuration is readable",
"details": {
"approval policy": "OnRequest",
"codex-linux-sandbox helper": "none",
"denied-read glob rules": "0",
"denied-read rules": "0",
"execve wrapper helper": "/Users/joshmcclure/.codex/tmp/arg0/codex-arg0HwgccB/codex-execve-wrapper",
"filesystem sandbox": "restricted",
"glob scan max depth": "unbounded",
"managed filesystem source": "none",
"network sandbox": "restricted"
},
"remediation": null,
"durationMs": 0
},
"security.endpoint": {
"id": "security.endpoint",
"category": "security",
"status": "ok",
"summary": "no supported endpoint protection detected",
"details": {
"endpoint products": "none detected"
},
"remediation": null,
"durationMs": 12
},
"state.paths": {
"id": "state.paths",
"category": "state",
"status": "ok",
"summary": "state paths and databases are inspectable",
"details": {
"CODEX_HOME": "/Users/joshmcclure/.codex (dir)",
"active rollout files": "684 files, 3336772714 total bytes, 4878322 average bytes",
"archived rollout files": "0 files, 0 total bytes, 0 average bytes",
"goals DB": "/Users/joshmcclure/.codex/goals_1.sqlite (file)",
"goals DB integrity": "ok",
"log DB": "/Users/joshmcclure/.codex/logs_2.sqlite (file)",
"log DB integrity": "ok",
"log dir": "/Users/joshmcclure/.codex/log (dir)",
"memories DB": "/Users/joshmcclure/.codex/memories_1.sqlite (file)",
"memories DB integrity": "ok",
"queue DB": "/Users/joshmcclure/.codex/queue_1.sqlite (file)",
"queue DB integrity": "ok",
"sqlite home": "/Users/joshmcclure/.codex (dir)",
"state DB": "/Users/joshmcclure/.codex/state_5.sqlite (file)",
"state DB integrity": "ok",
"thread history DB": "/Users/joshmcclure/.codex/thread_history_1.sqlite (file)",
"thread history DB integrity": "ok"
},
"remediation": null,
"durationMs": 2816
},
"state.rollout_db_parity": {
"id": "state.rollout_db_parity",
"category": "threads",
"status": "ok",
"summary": "rollout files and state DB thread inventory agree",
"details": {
"default model provider": "openai",
"rollout DB active files": "684",
"rollout DB active rows": "684",
"rollout DB archive mismatches": "0",
"rollout DB archived files": "0",
"rollout DB archived rows": "0",
"rollout DB duplicate DB paths": "0",
"rollout DB duplicate rollout thread ids": "0",
"rollout DB malformed file names": "0",
"rollout DB missing active rows": "0",
"rollout DB missing archived rows": "0",
"rollout DB model providers": "openai=684",
"rollout DB rows": "684",
"rollout DB scan cap reached": "false",
"rollout DB scan errors": "0",
"rollout DB sources": "cli=526, subagent:other=110, subagent:review=36, subagent:thread_spawn=9, vscode=3",
"rollout DB stale rows": "0"
},
"remediation": null,
"durationMs": 395
},
"system.disk": {
"id": "system.disk",
"category": "disk",
"status": "ok",
"summary": "sufficient free disk space (13.6 GiB)",
"details": {
"CODEX_HOME available": "13.6 GiB",
"failure threshold": "1.0 GiB",
"warning threshold": "5.0 GiB",
"worktree available": "13.6 GiB"
},
"remediation": null,
"durationMs": 0
},
"system.environment": {
"id": "system.environment",
"category": "system",
"status": "ok",
"summary": "OS language en-US",
"details": {
"EDITOR": "not set",
"LANG": "en_US.UTF-8",
"VISUAL": "not set",
"os": "Mac OS 26.6.2 [64-bit]",
"os language": "en-US",
"os type": "Mac OS",
"os version": "26.6.2"
},
"remediation": null,
"durationMs": 2
},
"terminal.env": {
"id": "terminal.env",
"category": "terminal",
"status": "ok",
"summary": "terminal metadata was detected",
"details": {
"COLORTERM": "truecolor",
"TERM_PROGRAM": "Apple_Terminal",
"color output": "enabled",
"effective locale": "en_US.UTF-8",
"stderr is terminal": "true",
"stdin is terminal": "true",
"stdout is terminal": "true",
"terminal": "Apple Terminal",
"terminal size": "80x24",
"terminal version": "470.2"
},
"remediation": null,
"durationMs": 0
},
"terminal.title": {
"id": "terminal.title",
"category": "title",
"status": "ok",
"summary": "terminal title default",
"details": {
"terminal title activity": "true",
"terminal title items": "activity, project-name",
"terminal title project source": "cwd",
"terminal title project value": "joshmcclure",
"terminal title source": "default"
},
"remediation": null,
"durationMs": 0
},
"updates.status": {
"id": "updates.status",
"category": "updates",
"status": "ok",
"summary": "update configuration is locally consistent",
"details": {
"cached latest version": "0.154.0",
"check for update on startup": "true",
"last checked at": "2026-09-15T23:03:57.240967Z",
"latest version": "0.154.0",
"latest version status": "current version is not older",
"npm update target": "not inspected (PATH helpers are not executed)",
"update action": "npm install -g @openai/codex",
"version cache": "/Users/joshmcclure/.codex/version.json"
},
"remediation": null,
"durationMs": 448
}
}
}What issue are you seeing?
Codex CLI/Astra xhigh stayed in a model-mediated polling loop around long-running deterministic jobs and exhausted my main weekly Codex usage before the implementation completed.
This was one continuous WASDE data-engineering implementation session:
Uploaded thread: 01a098e5-0707-7530-9138-688b82a1e86e
Environment:
Codex CLI 0.154.0
Model: gpt-6-astra
Reasoning effort: xhigh
Fast mode off
Pro-Light
The difficult semantic work was legitimate, but once long-running parse/load/audit jobs were already running, Codex repeatedly resumed Astra/xhigh to poll progress rather than allowing the shell/process to own the wait and return only on completion or failure.
Representative late-run sequence:
2026-09-16T11:49:17.187Z
Astra: "The audited load has reached 333 reports. Both audits remain clear; no intervention is needed."
Codex then launched another status operation that waited on the live process for 60 seconds and reread progress JSON:
2026-09-16T11:49:22.228Z
exec:
await tools.write_stdin({
session_id:79012,
chars:"",
yield_time_ms:60000,
max_output_tokens:100
})
then read:
unit-and-meat-reparse-process.json
unit-and-meat-fact-diff/progress.json
It then issued another explicit wait:
2026-09-16T11:49:29.280Z
wait:
{"cell_id":"5590","yield_time_ms":60000,"max_tokens":500}
The result was only healthy incremental progress:
2026-09-16T11:50:22.319Z
status: running
expected: 688
completed: 576
errors: 0
unexpected_changes: 0
sources: 339
failures: []
Astra then summarized that status in prose and continued.
The autonomous goal context during this same period stated:
Continue working toward the active thread goal.
Budget:
- Token budget: none
- Tokens remaining: unbounded
No-progress check:
- A verified wait polls a specific process, session, job, or tool handle confirmed live now.
Near the final limit hit, Codex again waited on the same kind of healthy deterministic work:
2026-09-16T11:54:09.717Z
status: running
expected: 688
completed: 595
errors: 0
unexpected_changes: 0
sources: 354
failures: []
Immediately afterward the session terminated:
{
"timestamp": "2026-09-16T11:54:10.143Z",
"type": "event_msg",
"payload": {
"type": "task_complete",
"error": {
"message": "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Sep 22nd, 2026 1:06 PM.",
"codex_error_info": "usage_limit_exceeded"
}
}
}
Support has clarified that the nearby codex_bengalfox used_percent: 0.0 event refers to a separate Spark meter, not the exhausted main weekly Codex limit, so I am not reporting that meter as the bug.
The issue I am reporting is the execution behavior: xhigh remained in the loop to repeatedly poll deterministic background jobs even after Astra itself stated that no intervention was needed.
I had also repeatedly warned Astra earlier in the same retained session that inference usage was becoming a critical constraint and asked why it had not changed course.
Expected behavior:
For long-running deterministic work, Codex should launch the job, suspend model inference, and resume only when the process exits, fails, or emits an event requiring judgment. A shell-side waiter such as:
while kill -0 "$PID" 2>/dev/null; do
sleep 30
done
tail -n 25 "$LOG"
should not require repeated Astra/xhigh turns.
The complete uploaded session contains thousands of exec/tool interactions, including repeated wait/status loops. I have preserved the full rollout JSONL and can provide additional excerpts if useful.
What steps can reproduce the bug?
Uploaded thread: 01a098e5-0707-7530-9138-688b82a1e86e
What is the expected behavior?
Codex should treat long-running deterministic work as machine-owned execution, not as a reason to keep invoking the reasoning model.
Once Astra has made the necessary engineering decision and launched a parser, loader, verifier, database job, or other deterministic process, the expected behavior is:
Launch the job.
Allow the shell/process to block, wait, or monitor it without repeated model inference.
Suspend Astra/xhigh while no judgment is required.
Resume the model only when:
the process completes successfully;
the process fails;
an exception or unexpected state requires model judgment; or
the user intervenes.
Return terminal output, a structured receipt, failure bundle, or relevant log tail to the model when it resumes.
For example, this should be sufficient:
while kill -0 "$PID" 2>/dev/null; do
sleep 30
done
tail -n 25 "$LOG"
During the sleep loop, there should be no repeated Astra/xhigh turns merely to observe that the process is still running.
A persistent autonomous goal should also distinguish between:
reasoning progress — new information requiring model judgment; and
machine progress — a deterministic job continuing normally.
Machine progress should not require repeated model wakeups to count as progress.
If a long-running task materially changes in scope, expected duration, convergence risk, or resource consumption, Codex should surface an owner checkpoint rather than silently continuing the same execution strategy.
In short, the expected model is:
xhigh reasons
→ launches deterministic work
→ model goes dormant
→ machine finishes or fails
→ xhigh resumes on the result
rather than:
xhigh checks status
→ waits 60 seconds
→ xhigh checks status
→ waits 60 seconds
→ repeat
This is especially important when the user is operating under a finite weekly Codex usage limit.
Additional information
This occurred during a genuinely difficult multi-day data-engineering implementation, and Astra did substantial useful work. I am not reporting that xhigh was inherently too expensive or that the task should have been easy.
The issue is that the execution strategy did not scale down model involvement when the work became mechanical.
The affected main rollout contains approximately:
5,593 exec calls
230 explicit waits
67 context compactions
thousands of small diagnostic/tool interactions
Near the end, Astra had already determined the semantic corrections, frozen the relevant code path, identified the affected population, and launched deterministic reparse/load/audit jobs. At that point the machines owned the work. Nonetheless, Codex continued waking Astra/xhigh to monitor incremental progress.
I had also repeatedly told Astra earlier in the same retained session that Codex usage had become a critical constraint and asked why it had not changed course. Those warnings remained present in later compacted context, but the execution policy did not materially change.
There were also few natural owner waypoints. When the project materially changed in scope, duration, or convergence risk, Astra generally adapted internally and continued rather than pausing to surface:
what assumption had failed,
what had changed,
whether another expensive corpus cycle was justified,
what machine work versus model work remained, and
whether owner direction was needed.
I believe there are two related product issues worth examining:
Long-running deterministic jobs need an event-driven/blocking wait path that does not repeatedly invoke the model.
Persistent autonomous goals need better resource/convergence governance, including owner checkpoints when the task materially changes or becomes machine-bound.
The full session has already been uploaded through Codex CLI feedback:
Uploaded thread: 01a098e5-0707-7530-9138-688b82a1e86e
I also have the complete local rollout JSONL and a detailed postmortem if additional evidence is useful.
I have a separate OpenAI Support case handling the account-side usage review, so this GitHub issue is intended to document the engineering behavior rather than request account remediation here.
Activity
github-actions commentedon Sep 16, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
monitortool with first-class remote-host support #44855Powered by Codex Action
jmcclure8275 commentedon Sep 16, 2026
Additional timing/telemetry detail requested by OpenAI Support:
Affected uploaded thread: 01a098e5-0707-7530-9138-688b82a1e86e
This was one continuous Codex CLI session running:
model: gpt-6-astra
reasoning_effort: xhigh
The session hit usage_limit_exceeded three times as I applied banked resets:
2026-09-14 20:38:20 CDT
2026-09-15 13:04:28 CDT
2026-09-16 06:54:10 CDT
Equivalent UTC timestamps from the rollout:
2026-09-15T01:38:20.857Z
2026-09-15T18:04:28.963Z
2026-09-16T11:54:10.143Z
All of the additional capacity I applied during this run came from banked resets, not purchased instant resets and not normal weekly-window resets.
One diagnostic detail that may be important: although the active model was gpt-6-astra at reasoning_effort: xhigh, nearby token_count telemetry identifies:
limit_id: codex_bengalfox
limit_name: GPT-5.3-Codex-Spark
with that separate meter showing used_percent: 0.0.
OpenAI Support has clarified that codex_bengalfox / Codex Spark is a separate meter from the primary Codex weekly limit, so I am not claiming that the 0.0 → usage_limit_exceeded sequence by itself proves incorrect usage accounting.
I am including the mismatch because Support specifically identified it as useful diagnostic information: the session performing the repeated polling was gpt-6-astra / xhigh, while this telemetry line references the separate codex_bengalfox meter.
The underlying engineering issue remains the model-mediated polling behavior described in the issue: xhigh continued waking to supervise healthy deterministic jobs until the primary weekly limit was exhausted.
lucaschang2021 commentedon Sep 17, 2026
I dug into current
mainand the related reports (#44909, #32188). I think #45974 is best fixed at the runtime ownership boundary, not primarily with prompt tweaks or shorter polling intervals.What current
mainappears to doThere are two pieces that line up with the observed behavior:
Goal continuation wakes inference as soon as the thread goes idle.
GoalExtension::on_thread_idlecallsGoalRuntimeHandle::continue_if_idle(), andcontinue_if_idle()eventually callsstart_turn_if_idle(...)for an active goal. There is a continuation-deferral check, but it is not currently tied to a live deterministic process/subagent wait.Unified exec already knows when a background process exits, but that completion does not become an idle-thread wake.
unified_exec::async_watcher::spawn_exit_watcherwaits for process completion and emitsExecCommandEnd. That is the seam described in Event-driven wakeup when background exec sessions complete #32188: the machine can wait cheaply, but the idle agent is not resumed from that event.This also matches the maintainer note in #44909: the newer empty-response circuit breaker helps empty automatic turns, but repeated polling/tool calls still keep an active goal alive and reset the breaker.
Proposed behavior
For deterministic machine-owned work, I would make the lifecycle explicitly event-driven:
In other words: machine progress should satisfy goal liveness without requiring model progress.
Implementation shape
I would reuse/converge with #32188 rather than build a separate polling-specific mechanism:
exec_command/ subagent work (armed,completed,observedis a good state model).write_stdin/ explicit wait) first, mark it observed and suppress the later wake.Fallback for waits the runtime cannot classify
For cases where Codex still ends up in an unclassified poll-only loop, an exponential backoff/circuit breaker is still useful, but I would treat it as a fallback rather than the primary solution. The primary invariant should be: if the runtime already owns a live process/subagent handle, inference should not be used as the clock.
Regression tests I would want
write_stdinobserves completion first: no duplicate automatic wake.activeacross the machine wait and resumes with the completion result, rather than being paused/blocked just to avoid token burn.This would address the #45974 failure mode without sacrificing the intended “gritty” goal semantics: the goal stays alive, but expensive intelligence sleeps while cheap deterministic execution is making progress.
lucaschang2021 commentedon Sep 17, 2026
Follow-up after checking the latest #32188 discussion and current
main:There is now stronger evidence that this is an idle-wakeup / wait-ownership problem rather than a model-prompt problem.
New evidence
Event-driven completion has been implemented successfully in personal forks.
Event-driven wakeup when background exec sessions complete #32188 now includes a tested implementation where a yielded background command can finish after the assistant turn ends, enqueue a bounded completion notification, and resume the same thread exactly once — with no
write_stdinpolling loop required.codex queue --thread ... --message ...already proves the thread can be woken from an external completion event.That workaround is not sufficient as a product solution (it lacks native cancellation/dedup/capacity semantics), but it confirms the missing primitive is not “can Codex ever wake an idle thread?”; it is “can the runtime wire owned background work into that wake path safely?”
A useful benchmark proposed in Event-driven wakeup when background exec sessions complete #32188 is:
WAIT_MAIN_INPUT = total parent-model input consumed exclusively while waiting for external work to reach a terminal stateFor a runtime-managed wait, the target should be 0 until an actual completion/failure event exists.
Minimal invariant I think Codex should enforce
If the runtime owns a live deterministic handle, the model should not be used as the clock.
The important point is that this should apply to both:
unified_execprocesses; andOtherwise fixing only shell-process waits will still leave the same quota burn through
wait_agent/ task-status polling.Suggested integration point with Goals
Current
mainalready hasthread_goal_continuation_deferrals. I think that mechanism can be made semantic rather than incidental:start_turn_if_idle(...)to run once with the completion payload.That preserves the intended gritty behavior while removing inference-only liveness checks.
Additional regression criteria
Beyond the earlier list, I would add:
WAIT_MAIN_INPUT == 0during pure machine wait;I think this is the cleanest way to address #45974, #44909, and #32188 as one runtime scheduling problem: model reasoning should resume on new information, not on elapsed time alone.
lucaschang2021 commentedon Sep 17, 2026
Implementation refinement after checking current
mainmore closely (around8452164c): I would not use the existingthread_goal_continuation_deferralsrow as the machine-wait registry itself.The current schema is one row per thread:
That works as a thread-level boolean, but it cannot represent multiple concurrent waits. If a goal owns jobs A/B/C and A completes first, clearing a single boolean deferral at A's completion can re-enable automatic goal continuation while B and C are still running, recreating the same polling burn.
A more robust shape
I think the runtime needs a small keyed wait registry, e.g. conceptually:
Then goal deferral becomes a derived invariant:
rather than a boolean that one completion can clear prematurely.
Subagents are already very close to this
Current
AgentControl::maybe_start_completion_watcheralready does the expensive part correctly: it creates a detached watcher, subscribes to the child status, and waits until the child reaches a final state.For MA v2, the watcher then builds the completion message with:
and for the legacy path it calls:
So Codex already has machine-observable subagent completion; the completion is deliberately delivered as non-waking state.
Also,
send_inter_agent_communicationalready has atrigger_turnpath and performs the execution-capacity check before starting a turn. That means the subagent fix does not need another polling layer or another watcher. It needs an ownership rule that conditionally converts a completion from queue-only delivery into an exactly-once wake when the parent has an armed runtime wait for that child.Suggested lifecycle
The ordering in steps 6–8 matters: enqueue/commit the new information before reopening continuation, otherwise there is a race where the goal wakes and still has nothing new to reason about.
Multi-wait regression I would add
Parent goal launches A, B, C concurrently:
WAIT_MAIN_INPUT == 0;wait_agent/write_stdinobservation of any handle suppresses only that handle's pending automatic wake.This is the main correction I would make to my previous suggestion: reuse the existing goal-deferral gate, but back it with keyed wait ownership/multiplicity rather than treating the current thread-level boolean as the wait registry.
That would make process and subagent waits share the same scheduling invariant while preserving current capacity controls and the intended goal semantics.
ShaneBreazeale commentedon Sep 20, 2026
Additional evidence and a working interim pattern for the event-driven fix proposed here:
A recent independent report measured the polling overhead in two runs:
Source: https://www.reddit.com/r/codex/comments/1wlcy5q/comment/paxyixk/
I also validated the interim workaround on Codex CLI 0.155.1. Tool processes receive
CODEX_THREAD_IDandCODEX_SESSION_ID, so a detached launcher can capture the current thread, run a non-interactive command with output/status persisted to files, and invoke exactly once on exit:The originating model turn can end immediately. The queue message wakes the same thread after success, failure, or launch error, with no model calls while the command is merely alive.
One integration wrinkle is also reproducible: when the detached wrapper inherits a managed workspace sandbox, the later
codex queuecall can fail opening$CODEX_HOME/state_5.sqlitewithattempt to write a readonly database. Launching the wrapper with narrowly scoped escalated access works, but adds an approval/configuration burden. A native runtime completion wake would avoid requiring a background helper to mutate Codex state from inside the command sandbox.This makes the desired product behavior concrete, but it should be a native runtime contract rather than a user-maintained wrapper:
on_exit: wake(or an equivalent completion policy).The regression metric suggested above captures the intended invariant well:
WAIT_MAIN_INPUT == 0until a terminal event introduces new information.codex queueproves the wake path already exists; the missing part is wiring runtime-owned completion to it automatically and steering the model away from elapsed-time polling.docs: explain temporary cli-run workaround (openai/codex#42908, opena…
PHPCraftdream commentedon Oct 1, 2026
@lucaschang2021's distinction between a machine-owned wait and goal continuation, plus @ShaneBreazeale's queue/sandbox example, helps separate two problems here. A command runner can remove the need to ask xhigh whether a healthy job is still alive; it cannot by itself prevent an active goal from scheduling unrelated model turns. I have packaged a temporary workaround for the first part, not a fix for the whole goal-governance issue.
I maintain
cc-arch-hands, whosecli-runMCP integration is available in 0.15.1. Aruncall with queue delivery hands a named set of commands to a detached Node worker and acknowledges its UID. The worker owns process waiting, captures stdout/stderr and exit results on disk, and attempts one shortcodex queuecompletion message to the calling thread after all jobs finish. The caller can end its turn rather than repeatedly issuing emptywrite_stdincalls or rereading progress JSON. Later,statusandlogsallow inspection by task name or UID; they are not intended as a new polling loop.That installs the optional Codex skills, local stdio MCP registration, and marked instructions for routing long-running commands. Restart Codex to load them. Implementation and the coverage matrix.
The permission wrinkle in the previous comment is real and should not be hidden by an installation command. MCP workers do not inherit Codex's per-turn filesystem sandbox;
runcan execute arbitrary commands and therefore requires approval or an explicit user trust decision. We recently encountered Codex rejecting the MCP call before launch withMCP tool call requires approval, but approval policy is never. Installation does not silently override that policy. Version 0.15.1 preserves an explicitly selectedtools.run.approval_modeduring reinstall. A missing/unloaded target thread or queue-access error can still prevent notification; that failure is saved with the run state.This runner does not implement
thread_goal_continuation_deferrals, register native subagent wait handles, coalesce independent task completions at the scheduler level, or guaranteeWAIT_MAIN_INPUT == 0for a session with an active goal. We also have no controlled token-cost benchmark, so the savings reported earlier in this thread are not measurements of our tool.It is usable now for moving deterministic CLI waiting out of the model loop, while the upstream ownership invariant remains necessary: live machine work should defer inference, and terminal information should be committed before reopening goal continuation. I would keep this issue open for that runtime-level change rather than treat an external queue helper as its resolution.
lucaschang2021 commentedon Oct 1, 2026
Thank you for building this and, especially, for drawing the boundary so clearly.
I think your runner is a very useful validation of the distinction I was trying to make: deterministic machine-owned waiting can be moved out of the model loop today, while goal continuation still needs a runtime-level ownership rule.
The fact that you explicitly do not treat the external queue helper as a full resolution is important. The remaining invariant is still upstream: while live machine work owns progress, continuation inference should be deferred, and terminal state should be committed before the goal is reopened.
Also appreciate you documenting the approval/sandbox limitation instead of hiding it behind the installer. That makes the workaround much easier to reason about as an engineering component rather than as a substitute for native scheduling semantics.
Thanks again for implementing and testing this in practice — it gives the machine-wait half of the proposal a concrete reference point.