Found against a live codex-cli 0.153.2 under tmux, driven with CAO's own launch flags and read back through get_backend().get_history() — not a synthetic fixture.
CodexProvider.initialize() never concludes that Codex has started, and fails with TimeoutError("Codex initialization timed out after 60 seconds"), on a pane that is visibly idle and ready to accept input.
What the pane actually looks like
╭──────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.153.2) │
│ model: gpt-5.6-sol /model to change │
│ directory: /tmp/.../work │
│ permissions: YOLO mode │
╰──────────────────────────────────────────────────────────╯
Tip: Use /copy or press Ctrl+O to copy the latest agent response as Markdown.
• You have 1 usage limit reset available. Run /usage to use one.
› Ask Codex to do anything
gpt-5.6-sol default · /tmp/.../work
That is a fully started Codex sitting at its idle composer. Measured through CAO's own capture path:
_has_startup_idle_composer(real capture) -> False # initialize() polls until it times out
get_status(real capture) -> completed # before any task was ever submitted
Cause
Codex prints account/product notices as • bullets, and CAO cannot tell those from agent output.
1. The readiness veto fires on a notice. _has_startup_idle_composer rejects the frame when STARTUP_ACTIVITY_PATTERN (^\s*•[^\S\n]+\S) matches anywhere in the bottom STARTUP_PROMPT_BOTTOM_LINES. The matching text here is:
STARTUP_ACTIVITY_PATTERN vetoes readiness: True -> '\n• Y'
• You have 1 usage limit reset available. Run /usage to use one. — Codex telling the user about their plan, not activity. The other two vetoes (WAITING_PROMPT_PATTERN, STARTUP_BLOCKING_INPUT_PATTERN) are both False, so this is the sole reason readiness is refused. Every subsequent poll sees the same line, so the loop can only end in the 60s timeout.
2. The same line reads as a model reply. _find_assistant_marker returns that bullet, and with no user marker below the footer cutoff get_status takes the "long-running response whose › marker was evicted" branch and returns COMPLETED — for a worker that has never been given a task. The code's own comment calls a false COMPLETED "the dangerous case — it tells the conductor the agent is free."
The placeholder itself is fine: STARTUP_IDLE_PLACEHOLDER_PATTERN already lists Ask Codex to do anything, and TUI_FOOTER_PATTERN matches the model · path footer via its ·\s+[~/] alternative. Neither of those is the problem.
Impact
Any Codex account whose TUI shows a notice bullet within the bottom 15 lines cannot launch a Codex worker at all: initialize() raises after 60s, and on the deferred-init path _schedule_deferred_init then notifies the caller and tears the worker down. The trigger is account state (a usage-limit notice, a tip promoted to a bullet), so it will look intermittent across machines and accounts while being fully deterministic on any one of them.
Suggested direction
The readiness veto should key on evidence of work in progress, not on the bullet glyph. Codex marks actual activity with the progress spinner — TUI_PROGRESS_PATTERN, the (<n>s • esc to interrupt) shape — which is what "still starting" really looks like; a bare bullet with no spinner is a printed line, not a busy TUI. Narrowing STARTUP_ACTIVITY_PATTERN to that shape fixes readiness without loosening any of the blocking-dialog checks, which are matched separately.
The get_status half is the same root cause and wants the same discrimination: a notice bullet emitted before any user turn should not satisfy the assistant-marker search that produces COMPLETED.
I have the raw byte streams and rendered captures for the startup, pasted-unsubmitted, and accepted-turn phases if they would help whoever picks this up. Happy to take it myself — flagging first rather than sending a PR into an area with active review traffic.
Found against a live
codex-cli 0.153.2under tmux, driven with CAO's own launch flags and read back throughget_backend().get_history()— not a synthetic fixture.CodexProvider.initialize()never concludes that Codex has started, and fails withTimeoutError("Codex initialization timed out after 60 seconds"), on a pane that is visibly idle and ready to accept input.What the pane actually looks like
That is a fully started Codex sitting at its idle composer. Measured through CAO's own capture path:
Cause
Codex prints account/product notices as
•bullets, and CAO cannot tell those from agent output.1. The readiness veto fires on a notice.
_has_startup_idle_composerrejects the frame whenSTARTUP_ACTIVITY_PATTERN(^\s*•[^\S\n]+\S) matches anywhere in the bottomSTARTUP_PROMPT_BOTTOM_LINES. The matching text here is:• You have 1 usage limit reset available. Run /usage to use one.— Codex telling the user about their plan, not activity. The other two vetoes (WAITING_PROMPT_PATTERN,STARTUP_BLOCKING_INPUT_PATTERN) are both False, so this is the sole reason readiness is refused. Every subsequent poll sees the same line, so the loop can only end in the 60s timeout.2. The same line reads as a model reply.
_find_assistant_markerreturns that bullet, and with no user marker below the footer cutoffget_statustakes the "long-running response whose›marker was evicted" branch and returnsCOMPLETED— for a worker that has never been given a task. The code's own comment calls a false COMPLETED "the dangerous case — it tells the conductor the agent is free."The placeholder itself is fine:
STARTUP_IDLE_PLACEHOLDER_PATTERNalready listsAsk Codex to do anything, andTUI_FOOTER_PATTERNmatches themodel · pathfooter via its·\s+[~/]alternative. Neither of those is the problem.Impact
Any Codex account whose TUI shows a notice bullet within the bottom 15 lines cannot launch a Codex worker at all:
initialize()raises after 60s, and on the deferred-init path_schedule_deferred_initthen notifies the caller and tears the worker down. The trigger is account state (a usage-limit notice, a tip promoted to a bullet), so it will look intermittent across machines and accounts while being fully deterministic on any one of them.Suggested direction
The readiness veto should key on evidence of work in progress, not on the bullet glyph. Codex marks actual activity with the progress spinner —
TUI_PROGRESS_PATTERN, the(<n>s • esc to interrupt)shape — which is what "still starting" really looks like; a bare bullet with no spinner is a printed line, not a busy TUI. NarrowingSTARTUP_ACTIVITY_PATTERNto that shape fixes readiness without loosening any of the blocking-dialog checks, which are matched separately.The
get_statushalf is the same root cause and wants the same discrimination: a notice bullet emitted before any user turn should not satisfy the assistant-marker search that produces COMPLETED.I have the raw byte streams and rendered captures for the startup, pasted-unsubmitted, and accepted-turn phases if they would help whoever picks this up. Happy to take it myself — flagging first rather than sending a PR into an area with active review traffic.