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
{{ message }}
Repository navigation
[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
HeadlessSession.runState() infers idle for raw engines from three timers
(packages/browser-runner/src/coding/headless.ts:278-288). Rule 1 is "produced output, then went
quiet for 1.5s → settled", and its comment explains it was written for a persistent interactive
CLI, where there is no other signal to read.
Raw engines are no longer persistent. :298 states it outright — "Raw engines run ONE-SHOT PER
TURN, not as a persistent interactive process" — oneShot is mode === "raw" (:313), each turn
is its own spawn (runOneShot, :412), and the close handler sets this.run = "idle"
(:455-457). So for exactly the engines rule 1 governs, process exit is an exact, already-wired
end-of-turn signal, and a timer can only ever fire before it.
The failure is not theoretical, and the codebase already documents both halves:
headless.test.ts:412-419 records the consequence — "a long build gets judged idle and the brain
sends turn 2 while turn 1 is still running".
runOneShot (:417-427) then kills the in-flight process, with a comment naming the same
cause: "the raw idle heuristic declares idle after a >1.5s output pause — so a long build could
be judged idle, the brain sends turn 2, and TWO engine processes edit the same repo at once."
That kill was the right fix for concurrent writers, but it converts the symptom from two processes
racing into work silently destroyed: codex exec running a test suite or a build goes quiet for
more than 1.5s, is judged idle, and is killed mid-turn by its own replacement. The Pilot then reasons
about a turn that never finished.
Rules 2 and 3 have the same property for one-shot engines — an 8s silent-turn rule and a 15-minute
backstop are both guesses about a process whose exit is observable.
Suggested fix
Make exit authoritative when oneShot is true: runState() returns "thinking" while procAlive,
and idle is set by close (which it already does). The timer rules stay for any genuinely persistent
non-Claude engine, if one is ever added — but they should be gated on that, not on mode === "raw",
which today means the opposite.
Worth checking as part of it whether runOneShot's pre-emptive kill is still needed once idle cannot
be reported early; if it is, it should say what it killed rather than dying silently, per the same
reasoning that put a non-zero exit code into the transcript at :445.
What this does not claim
It does not claim the 15-minute backstop is useless — a wedged process still needs a ceiling, and
removing it without a replacement would trade an early kill for a permanent hang. It also does not
touch the stream-json path, where a result event already ends the turn as a fact.
Found while assessing #249 (whether tmux should return as a Coder engine backend); the memo's point
was that this specific defect is the one people would blame on "no PTY", and it is not PTY-shaped.
Verified against main at d0e4095 by reading the file, not by reproducing a run.
Verified already delivered — closing, with one caveat about who is actually running it.
1aa1722 is on origin/main. Checked against packages/browser-runner/src/coding/headless.ts rather than the commit message:
Exit is authoritative (:293): if (this.oneShot) return this.procAlive ? "thinking" : "idle"; returns before the timer rules. Idle is set by the close handler, as the issue asked.
The timer rules are gated on the right thing. They are now unreachable for a one-shot engine because of the early return above, and the surviving mode === "raw" branch is explicitly documented as the persistent-engine case. The issue's point — that mode === "raw" today means the opposite of the condition rules 1-3 were written for — is honoured.
The 15-minute backstop was replaced, not deleted.MAX_ONE_SHOT_TURN_MS (:121) arms a real setTimeout in runOneShot (:487) that SIGTERMs the process and writes [<engine> turn ended after 15m — the engine never exited, so the session was unwedged] to the transcript. That is the right shape: it ends the turn rather than relabelling a live one as idle, which the issue correctly identified as the same mistake in slower form. It is unref'd, so it cannot hold the runner open, and cleared before the staleness guard.
The pre-emptive kill now says what it destroyed (:462): [<engine> turn aborted — a new instruction arrived while the previous one was still running]. The issue asked for exactly this, by analogy with the non-zero exit code at :445.
Caveat — shipped is not the same as running. This is runner-side, so it reaches a user only through the published CLI bundle. It went out in @proagentstore/cli@0.4.41 (npm latest is now 0.4.43, so it is published and current). But both of the owner's machines are behind it: Mac on 0.4.40 — one version short, i.e. the last build without this fix — and Mac-mini on 0.4.22. So on those two machines a codex exec running a long build is still judged idle after 1.5s and killed mid-turn.
npm i -g @proagentstore/cli on both is what makes this real. I have not run it and cannot confirm the fixed behaviour on hardware; the code and the publish are what I verified.
Activity
fix(runner): a one-shot engine's turn ends when its process exits, no…
serge-ivo commentedon Aug 8, 2026
Verified already delivered — closing, with one caveat about who is actually running it.
1aa1722is onorigin/main. Checked againstpackages/browser-runner/src/coding/headless.tsrather than the commit message::293):if (this.oneShot) return this.procAlive ? "thinking" : "idle";returns before the timer rules. Idle is set by theclosehandler, as the issue asked.mode === "raw"branch is explicitly documented as the persistent-engine case. The issue's point — thatmode === "raw"today means the opposite of the condition rules 1-3 were written for — is honoured.MAX_ONE_SHOT_TURN_MS(:121) arms a realsetTimeoutinrunOneShot(:487) that SIGTERMs the process and writes[<engine> turn ended after 15m — the engine never exited, so the session was unwedged]to the transcript. That is the right shape: it ends the turn rather than relabelling a live one as idle, which the issue correctly identified as the same mistake in slower form. It isunref'd, so it cannot hold the runner open, and cleared before the staleness guard.:462):[<engine> turn aborted — a new instruction arrived while the previous one was still running]. The issue asked for exactly this, by analogy with the non-zero exit code at:445.Caveat — shipped is not the same as running. This is runner-side, so it reaches a user only through the published CLI bundle. It went out in
@proagentstore/cli@0.4.41(npmlatestis now 0.4.43, so it is published and current). But both of the owner's machines are behind it:Macon 0.4.40 — one version short, i.e. the last build without this fix — andMac-minion 0.4.22. So on those two machines acodex execrunning a long build is still judged idle after 1.5s and killed mid-turn.npm i -g @proagentstore/clion both is what makes this real. I have not run it and cannot confirm the fixed behaviour on hardware; the code and the publish are what I verified.fix(coding): a keystroke sent to a coding session is refused at the b…
feat(runner): a coding turn reports how it ENDED, so a non-zero engin…
fix(coding): the Pilot is told when the engine refused a turn, and st…
feat(admin-perimeter): the Access gate tells a service token from a p…
fix(ci): name the Data tab's record page type; unrace two runner tests