You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[bug] Every Codex turn exited 1 and the session still reported alive/ready/idle — a non-zero engine exit is a line of prose in the pane and nothing else #545
Every Codex turn on this session exited 1, and the platform reports the session healthy
Feedback row 6a84f09a-821f-4ca2-9e8d-fbc8bae1d0fb (2026-08-12 12:16:52, trace eee50f1f,
instance a1d3522f-6b91-4cd9-bf7b-380e470192b7), filed by the agent through record_feedback:
"When starting Codex engine, it doesn't work — the Coding tab shows 'no output yet' and the engine
doesn't get started."
The owner's own words at 12:16:46: "There is nothing there yet, it just says no output yet, send
the engine an instruction to get started."
Codex does start. Every turn it runs exits 1, and nothing in the platform says so.
The pane, read live from production
GET /v1/instances/a1d3522f…/coding/sessions/csess_22d08431-591d-4936-bfd1-95c357667cd6/capture,
fetched 2026-08-12 23:14 UTC:
{"pane":"\n❯ [08:40:52] Run `git pull` in the repository at dev/aipa.\nReading additional input from stdin...\nNot inside a trusted directory and --skip-git-repo-check was not specified.\n[codex exited with code 1]\n\n❯ [08:40:56] Run the following shell command: cd dev/aipa && git pull\nReading additional input from stdin...\nNot inside a trusted directory and --skip-git-repo-check was not specified.\n[codex exited with code 1]\n\n❯ [08:41:00] Run the shell command: cd ~/dev/aipa && git pull\nReading additional input from stdin...\nNot inside a trusted directory and --skip-git-repo-check was not specified.\n[codex exited with code 1]",
"alive":true,"ready":true,"runState":"idle","engineRuntime":"child-process","runnerConnected":true}
Three turns. Three exit-1s. alive: true, ready: true, runState: "idle".
Reproduced locally, on the same machine, with the shipped preset
codex-cli 0.147.0, RLs-MacBook-Air (the node this session ran on):
$ mkdir /tmp/probe && cd /tmp/probe # NOT a git repo
$ codex exec --sandbox danger-full-access "say hi" </dev/null; echo "EXIT=$?"
Reading additional input from stdin...
Not inside a trusted directory and --skip-git-repo-check was not specified.
EXIT=1
$ git init -q . # the ONLY change
$ codex exec --sandbox danger-full-access "reply with the single word OK" </dev/null; echo "EXIT=$?"
…
codex
OK
EXIT=0
The failure is Codex's own gate: codex exec --help documents --skip-git-repo-check Allow running Codex outside a Git repository. The shipped preset does not
carry it —
(The directory in question is ~/dev/aipa, which has no .git — that half is #548. This issue is
about what the platform does when an engine turn fails, which would be the same for any non-zero
exit: a bad flag, an unauthenticated CLI, a wrong model name.)
The mechanism: a non-zero exit is prose, not a signal
packages/browser-runner/src/coding/headless.ts:528-543 (runOneShot's close handler):
proc.on("close",(code: number|null)=>{clearTimeout(ceiling);if(code)this.push(`[${this.config.clientType} exited with code ${code}]`);if(this.proc!==proc)return;this.run="idle";this.proc=null;});
The exit code is pushed into the transcript as a line of text and then discarded. It sets no
field. The only structured failure flag a one-shot session has is spawnFailed
(headless.ts:214,511), and that is set solely from the error event — a binary that is not on
PATH. A binary that runs and refuses is indistinguishable from a binary that runs and succeeds:
So the exit sets runState to idle — which is the loop's word for ready for the next
instruction — and alive stays true. The snapshot the cloud receives
(packages/browser-runner/src/coding/runtime.ts:245-265) carries pane, alive, ready, runState, authResolved, engineRuntime. There is no field for the outcome of the last turn.
What that costs the Pilot
runCodingLoop (workers/api/src/lib/coding-loop.ts:149-296) opens each step with two liveness
guards — snap.cancelled and !snap.alive — and neither fires. The brain is then handed the pane
as prose (decideCodingAction, :408-414) and left to work it out. In the production run it read
"Not inside a trusted directory" as a path problem and tried three path variants in eight
seconds (22:40:52 / 22:40:56 / 22:41:00), then called request_human. Nobody answered the handoff,
and 180 × 5s later (HANDOFF_WAIT_POLLS, workflows/coding-session.ts:54) the run closed failed — stuck not resolved in time at 22:56:23. Three BYOK-Claude decisions and 15 minutes spent
on a signal the runner had in hand at 22:40:52.
Note this is not#522: the three instructions differ, so repetitionVerdict correctly did not
fire, and 73aa1c7 had been on main for twelve hours when this ran.
And what it costs the console
agents/coder/web/src/CodingTab.tsx renders the same pane. A fresh one-shot session legitimately
shows "no output yet" until a turn arrives (headless.ts:389-392 — start() is a no-op for a
one-shot engine, by design). The owner opened the Coding tab on a fresh Codex session at 12:16
and read that message as "Codex is broken". He was wrong about that session and right about the
engine, and nothing on the page could tell the two apart.
What to do, cheapest first
1. Carry the last turn's exit code in the snapshot. One field, one line in runOneShot's close handler, one field in CodingSnapshot:
lastTurn?: { exitCode: number|null; at: number}
Everything below is a consumer of that one fact, and each is independently shippable.
2. Tell the Pilot, in the step log, not only in the pane.runCodingLoop already has the
channel: it feeds itself notes through actionLog for empty instructions (#504, :205-223) and
merge refusals (#314, :234-246), and the brain demonstrably reacts to them. Push the engine exited with code 1 and produced no result — this is the engine refusing to run, not an answer to your instruction and the third path-variant retry does not happen.
3. Bound consecutive engine failures, exactly as MAX_EMPTY_INSTRUCTIONS = 3 bounds the
mirror case: N non-zero exits in a row → outcome: "failed" with the engine's own last stderr
line as the detail. failed here, not stuck: no human takeover fixes a CLI that refuses on
every invocation, and 15 minutes of unanswered handoff is the worst available ending.
4. Say it in the console. A repo row / session header that reads "the engine exited with code 1
on its last 3 turns" instead of a pane the owner has to parse.
5. The preset question — decide it, do not default it. Should codex ship with --skip-git-repo-check? Arguments both ways, and it is genuinely the owner's call:
For: Claude Code's preset has no equivalent gate, so the two engines behave differently for
the same repo, which coding-engines.ts:238-246 explicitly calls a trap ("the trust level has
to match across presets or the choice is a trap").
Report alive: false after a non-zero exit. Rejected — it would resurrect the exact bug headless.ts:271-293 documents: a one-shot session has no process between turns, alive means
"can take a turn", and a failing turn does not make the session unable to take another. The next
turn might well succeed (different cwd, different instruction). Outcome and liveness are two
facts; conflating them once already killed every delegated goal on codex/grok/gemini at
iteration 0.
Acceptance criteria
CodingSnapshot carries the exit status of the last completed one-shot turn, and a runner test
asserts it is 1 after a command that exits 1.
runCodingLoop appends a note to actionLog on a failed turn, and a unit test asserts the note
reaches the next decide call's rendered step log.
N consecutive failed turns end the run as failed with the engine's own message, not as stuck
→ 15-minute handoff → failed.
Replaying this exact production pane through runCodingLoop produces at most one path-variant
retry, not three.
The Coding tab distinguishes "no output yet (nothing has been sent)" from "the engine failed on
its last turn".
Regression risk
A legitimately non-zero exit that is still an answer.codex exec can exit non-zero after
doing real work (a test suite it ran failed). Treating any non-zero exit as an engine failure
would end useful runs. Mitigate with the "consecutive, and no output produced" pair — the same
shape MAX_EMPTY_INSTRUCTIONS uses, and the reason it is documented as CONSECUTIVE.
Old runners. The field is absent from any bundle older than this change; the cloud must treat undefined as "unknown", never as "failed" — the same rule verdictFromCheck applies to checked !== true (lib/coding-workdir.ts:78).
Every Codex turn on this session exited 1, and the platform reports the session healthy
Feedback row
6a84f09a-821f-4ca2-9e8d-fbc8bae1d0fb(2026-08-12 12:16:52, traceeee50f1f,instance
a1d3522f-6b91-4cd9-bf7b-380e470192b7), filed by the agent throughrecord_feedback:The owner's own words at 12:16:46: "There is nothing there yet, it just says no output yet, send
the engine an instruction to get started."
Codex does start. Every turn it runs exits 1, and nothing in the platform says so.
The pane, read live from production
GET /v1/instances/a1d3522f…/coding/sessions/csess_22d08431-591d-4936-bfd1-95c357667cd6/capture,fetched 2026-08-12 23:14 UTC:
{"pane":"\n❯ [08:40:52] Run `git pull` in the repository at dev/aipa.\nReading additional input from stdin...\nNot inside a trusted directory and --skip-git-repo-check was not specified.\n[codex exited with code 1]\n\n❯ [08:40:56] Run the following shell command: cd dev/aipa && git pull\nReading additional input from stdin...\nNot inside a trusted directory and --skip-git-repo-check was not specified.\n[codex exited with code 1]\n\n❯ [08:41:00] Run the shell command: cd ~/dev/aipa && git pull\nReading additional input from stdin...\nNot inside a trusted directory and --skip-git-repo-check was not specified.\n[codex exited with code 1]", "alive":true,"ready":true,"runState":"idle","engineRuntime":"child-process","runnerConnected":true}Three turns. Three exit-1s.
alive: true,ready: true,runState: "idle".Reproduced locally, on the same machine, with the shipped preset
codex-cli 0.147.0,RLs-MacBook-Air(the node this session ran on):The failure is Codex's own gate:
codex exec --helpdocuments--skip-git-repo-check Allow running Codex outside a Git repository. The shipped preset does notcarry it —
workers/api/src/lib/coding-engines.ts:269and, grepped, the flag appears nowhere in the repository:
(The directory in question is
~/dev/aipa, which has no.git— that half is #548. This issue isabout what the platform does when an engine turn fails, which would be the same for any non-zero
exit: a bad flag, an unauthenticated CLI, a wrong model name.)
The mechanism: a non-zero exit is prose, not a signal
packages/browser-runner/src/coding/headless.ts:528-543(runOneShot'sclosehandler):The exit code is pushed into the transcript as a line of text and then discarded. It sets no
field. The only structured failure flag a one-shot session has is
spawnFailed(
headless.ts:214,511), and that is set solely from theerrorevent — a binary that is not onPATH. A binary that runs and refuses is indistinguishable from a binary that runs and succeeds:
headless.ts:294-297headless.ts:324So the exit sets
runStatetoidle— which is the loop's word for ready for the nextinstruction — and
alivestays true. The snapshot the cloud receives(
packages/browser-runner/src/coding/runtime.ts:245-265) carriespane,alive,ready,runState,authResolved,engineRuntime. There is no field for the outcome of the last turn.What that costs the Pilot
runCodingLoop(workers/api/src/lib/coding-loop.ts:149-296) opens each step with two livenessguards —
snap.cancelledand!snap.alive— and neither fires. The brain is then handed the paneas prose (
decideCodingAction, :408-414) and left to work it out. In the production run it read"Not inside a trusted directory" as a path problem and tried three path variants in eight
seconds (22:40:52 / 22:40:56 / 22:41:00), then called
request_human. Nobody answered the handoff,and 180 × 5s later (
HANDOFF_WAIT_POLLS,workflows/coding-session.ts:54) the run closedfailed — stuck not resolved in timeat 22:56:23. Three BYOK-Claude decisions and 15 minutes spenton a signal the runner had in hand at 22:40:52.
Note this is not #522: the three instructions differ, so
repetitionVerdictcorrectly did notfire, and
73aa1c7had been on main for twelve hours when this ran.And what it costs the console
agents/coder/web/src/CodingTab.tsxrenders the same pane. A fresh one-shot session legitimatelyshows "no output yet" until a turn arrives (
headless.ts:389-392—start()is a no-op for aone-shot engine, by design). The owner opened the Coding tab on a fresh Codex session at 12:16
and read that message as "Codex is broken". He was wrong about that session and right about the
engine, and nothing on the page could tell the two apart.
What to do, cheapest first
1. Carry the last turn's exit code in the snapshot. One field, one line in
runOneShot'sclosehandler, one field inCodingSnapshot:Everything below is a consumer of that one fact, and each is independently shippable.
2. Tell the Pilot, in the step log, not only in the pane.
runCodingLoopalready has thechannel: it feeds itself notes through
actionLogfor empty instructions (#504, :205-223) andmerge refusals (#314, :234-246), and the brain demonstrably reacts to them. Push
the engine exited with code 1 and produced no result — this is the engine refusing to run, not an answer to your instructionand the third path-variant retry does not happen.3. Bound consecutive engine failures, exactly as
MAX_EMPTY_INSTRUCTIONS = 3bounds themirror case: N non-zero exits in a row →
outcome: "failed"with the engine's own last stderrline as the detail.
failedhere, notstuck: no human takeover fixes a CLI that refuses onevery invocation, and 15 minutes of unanswered handoff is the worst available ending.
4. Say it in the console. A repo row / session header that reads "the engine exited with code 1
on its last 3 turns" instead of a pane the owner has to parse.
5. The preset question — decide it, do not default it. Should
codexship with--skip-git-repo-check? Arguments both ways, and it is genuinely the owner's call:the same repo, which
coding-engines.ts:238-246explicitly calls a trap ("the trust level hasto match across presets or the choice is a trap").
no version control and therefore no undo — and adding the flag would let an agent write into an
unversioned directory. I would not add it. Fixing [bug] A run is admitted onto a folder the platform has already judged "not a git working tree", and starting it overwrites that verdict with
ready— threegit pullruns died on it #548 (refuse the run when the checkout isnot a git work tree) removes the situation instead of suppressing the symptom, and preserves
Codex's guarantee.
Alternatives considered and rejected
[… exited with code N]. The string is already there and a regex wouldwork today. Rejected: it is a regex over an engine's prose, which is the class of guess [bug] A one-shot raw engine is judged idle by a 1.5s quiet timer and killed mid-turn — process exit is already the exact signal #391
removed from
runStatein favour of the process's own exit. The runner has the exit code; itshould carry it.
alive: falseafter a non-zero exit. Rejected — it would resurrect the exact bugheadless.ts:271-293documents: a one-shot session has no process between turns,alivemeans"can take a turn", and a failing turn does not make the session unable to take another. The next
turn might well succeed (different cwd, different instruction). Outcome and liveness are two
facts; conflating them once already killed every delegated goal on codex/grok/gemini at
iteration 0.
Acceptance criteria
CodingSnapshotcarries the exit status of the last completed one-shot turn, and a runner testasserts it is
1after a command that exits 1.runCodingLoopappends a note toactionLogon a failed turn, and a unit test asserts the notereaches the next
decidecall's rendered step log.failedwith the engine's own message, not asstuck→ 15-minute handoff →
failed.runCodingLoopproduces at most one path-variantretry, not three.
its last turn".
Regression risk
codex execcan exit non-zero afterdoing real work (a test suite it ran failed). Treating any non-zero exit as an engine failure
would end useful runs. Mitigate with the "consecutive, and no output produced" pair — the same
shape
MAX_EMPTY_INSTRUCTIONSuses, and the reason it is documented as CONSECUTIVE.undefinedas "unknown", never as "failed" — the same ruleverdictFromCheckapplies tochecked !== true(lib/coding-workdir.ts:78).alive/runStatesemantics for one-shot engines are load-bearing ([bug] A one-shot raw engine is judged idle by a 1.5s quiet timer and killed mid-turn — process exit is already the exact signal #391, [enhancement] A raw engine forgets every previous turn — one-shot spawn is the design, and no preset, panel or doc says so #449). Any changethere needs
headless.test.ts's one-shot suite green.Feedback row this closes
6a84f09a-821f-4ca2-9e8d-fbc8bae1d0fb(#514 capture, authoragent).