Skip to content

Codex CLI repeatedly wakes xhigh to poll deterministic long-running jobs, exhausting finite weekly usage before task completion #45974

Open

Description

@jmcclure8275

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

  1. added
    CLIIssues related to the Codex CLI
    model-behaviorIssues related to behaviors exhibited by the model
    rate-limitsIssues related to rate limits, quotas, and token usage reporting
    on Sep 16, 2026
  2. github-actions commented on Sep 16, 2026

    @github-actions
    Contributor
  3. jmcclure8275 commented on Sep 16, 2026

    @jmcclure8275
    Author

    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.

  4. lucaschang2021 commented on Sep 17, 2026

    @lucaschang2021

    I dug into current main and 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 main appears to do

    There are two pieces that line up with the observed behavior:

    1. Goal continuation wakes inference as soon as the thread goes idle.
      GoalExtension::on_thread_idle calls GoalRuntimeHandle::continue_if_idle(), and continue_if_idle() eventually calls start_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.

    2. 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_watcher waits for process completion and emits ExecCommandEnd. 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:

    model reasons
      -> launches deterministic work
      -> runtime registers a machine-wait handle
      -> goal continuation is deferred
      -> model turn ends
      -> process/subagent runs with zero model inference
      -> completion/failure event arrives
      -> runtime enqueues one bounded completion event
      -> deferral is cleared
      -> exactly one continuation turn starts
    

    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:

    • Add a race-safe completion registration for yielded exec_command / subagent work (armed, completed, observed is a good state model).
    • When an active goal yields a known-live deterministic handle, persist/set goal-continuation deferral instead of immediately starting another goal turn.
    • Let the existing process/subagent watcher own the wait.
    • On normal completion/failure, enqueue a bounded completion fragment and schedule exactly one continuation.
    • If the completion is manually observed (write_stdin / explicit wait) first, mark it observed and suppress the later wake.
    • A kill/interrupt/session teardown must not create a phantom wake.
    • If the thread/session is at a concurrency/capacity ceiling, queue or degrade to current behavior rather than oversubscribing.
    • Multiple near-simultaneous completions should be batched/coalesced where possible.

    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

    1. Active goal starts a background process, turn becomes idle, process runs for N seconds: zero model continuations during N and exactly one wake on exit.
    2. Process exits before the wake registration is fully armed: completion is not lost.
    3. Manual write_stdin observes completion first: no duplicate automatic wake.
    4. Process is killed/aborted or session is torn down: no wake.
    5. Completion lands at the final-answer/idle boundary: exactly once.
    6. Several processes finish together: bounded/coalesced continuation behavior.
    7. Goal remains active across 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.

  5. lucaschang2021 commented on Sep 17, 2026

    @lucaschang2021

    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

    1. 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_stdin polling loop required.

    2. 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?”

    3. 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 state

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

    owned live handle exists
      => defer automatic goal continuation
      => no model wake just to observe liveness
      => watcher owns waiting
      => terminal event enqueues bounded completion state
      => clear deferral
      => schedule exactly one continuation
    

    The important point is that this should apply to both:

    • yielded unified_exec processes; and
    • subagent/task handles where completion is already machine-observable.

    Otherwise fixing only shell-process waits will still leave the same quota burn through wait_agent / task-status polling.

    Suggested integration point with Goals

    Current main already has thread_goal_continuation_deferrals. I think that mechanism can be made semantic rather than incidental:

    • set/persist deferral when the goal is active and there is at least one known-live runtime-owned wait handle;
    • do not classify the goal as paused/blocked merely because it is waiting;
    • clear deferral only when a relevant completion/failure/cancellation transition is committed;
    • then allow 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:

    • subagent parity: background subagent completion must satisfy the same zero-poll / exactly-once wake semantics as background process completion;
    • wait-cost metric: WAIT_MAIN_INPUT == 0 during pure machine wait;
    • durability boundary is explicit: if wake delivery is not durable across app/CLI restart, make that behavior explicit rather than silently losing responsibility;
    • no wake amplification: several completions close together should produce one bounded continuation batch where possible;
    • no double-observation: an explicit/manual consume path must invalidate the pending automatic wake atomically.

    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.

  6. lucaschang2021 commented on Sep 17, 2026

    @lucaschang2021

    Implementation refinement after checking current main more closely (around 8452164c): I would not use the existing thread_goal_continuation_deferrals row as the machine-wait registry itself.

    The current schema is one row per thread:

    CREATE TABLE thread_goal_continuation_deferrals (
        thread_id TEXT PRIMARY KEY NOT NULL REFERENCES thread_goals(thread_id) ON DELETE CASCADE
    );

    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:

    MachineWait {
      owner_thread_id,
      goal_id?,
      kind: Exec | SubAgent,
      handle_id,
      state: Armed | Completed | Observed | Cancelled,
      wake_policy,
    }
    

    Then goal deferral becomes a derived invariant:

    defer goal continuation
        iff EXISTS live runtime-owned wait for this goal/thread
    

    rather than a boolean that one completion can clear prematurely.

    Subagents are already very close to this

    Current AgentControl::maybe_start_completion_watcher already 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:

    InterAgentCommunication::new(..., /*trigger_turn*/ false)

    and for the legacy path it calls:

    inject_fragment_without_turn(...)

    So Codex already has machine-observable subagent completion; the completion is deliberately delivered as non-waking state.

    Also, send_inter_agent_communication already has a trigger_turn path 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

    1. exec/subagent yields a live handle
    2. register MachineWait before exposing the yielded state
    3. active goal sees live wait => continuation remains deferred
    4. parent model turn ends
    5. watcher waits with zero inference
    6. terminal event atomically transitions that wait
    7. enqueue bounded completion payload
    8. if parent is idle, schedule one turn; if active, retain/queue
    9. explicit/manual observation marks the wait Observed and suppresses a later wake
    10. cancellation/kill/teardown removes the wait without a wake
    11. remaining live waits keep the goal deferred
    

    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:

    • while all three are running: WAIT_MAIN_INPUT == 0;
    • A completes: exactly one parent wake with A's completion;
    • after that turn, B and C still count as live machine waits, so the goal returns to idle without polling;
    • B/C complete later and wake the parent independently or coalesce if their completions race;
    • manual wait_agent / write_stdin observation 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.

  7. ShaneBreazeale commented on Sep 20, 2026

    @ShaneBreazeale

    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:

    • 19 short sleep/check calls accounted for 44% of the orchestrator's estimated cost.
    • With Astra High orchestrating Luna Max, the polling version made 41 orchestrator responses and cost about $1.20 during the worker phase. The completion-driven version needed one response and cost about $0.04. Both passed the same 244 tests.

    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_ID and CODEX_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:

    codex queue --thread "$CODEX_THREAD_ID" --message "Job finished; inspect <status> and <output>, verify, and continue."
    

    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 queue call can fail opening $CODEX_HOME/state_5.sqlite with attempt 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:

    1. A yielded exec/subagent handle registers on_exit: wake (or an equivalent completion policy).
    2. The runtime owns the wait and goal continuation remains deferred while relevant handles are live.
    3. Completion commits a bounded result before scheduling one continuation.
    4. Manual observation atomically suppresses the pending wake; cancellation and teardown do not create one.
    5. Concurrent completions coalesce where practical.

    The regression metric suggested above captures the intended invariant well: WAIT_MAIN_INPUT == 0 until a terminal event introduces new information. codex queue proves 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.

  8. PHPCraftdream commented on Oct 1, 2026

    @PHPCraftdream

    @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, whose cli-run MCP integration is available in 0.15.1. A run call 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 short codex queue completion message to the calling thread after all jobs finish. The caller can end its turn rather than repeatedly issuing empty write_stdin calls or rereading progress JSON. Later, status and logs allow inspection by task name or UID; they are not intended as a new polling loop.

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

    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; run can execute arbitrary commands and therefore requires approval or an explicit user trust decision. We recently encountered Codex rejecting the MCP call before launch with MCP tool call requires approval, but approval policy is never. Installation does not silently override that policy. Version 0.15.1 preserves an explicitly selected tools.run.approval_mode during 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 guarantee WAIT_MAIN_INPUT == 0 for 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.

  9. lucaschang2021 commented on Oct 1, 2026

    @lucaschang2021

    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.

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

    CLIIssues related to the Codex CLIbugSomething isn't workingmodel-behaviorIssues related to behaviors exhibited by the modelrate-limitsIssues related to rate limits, quotas, and token usage reporting

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions