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] The board showed three dead runs as "completed" and kept "Needs you" empty while all three waited on a human — a coding card has no needs-human status and follows the session, not the run #553
Split out of #541, which is about why those runs died. This one is about why the board said the
opposite of what happened.
Observed, production, 2026-08-13
Three Pilot runs on instance a1d3522f (AIPA coder) died on 2026-08-12 with stuck not resolved in time — each after opening a human handoff and waiting 15 minutes for an
answer (#541). Here is that instance's board right now (instance_board):
Grep-verified as complete: upsertCodingSessionCard is only ever called with "running"
(coding-store.ts:472), and closeCodingSessionCards only with "cancelled" (:511) or status === "error" ? "failed" : "completed" (:576). And:
Its two peer workflows both have it — workflows/job-apply.ts parks a task in needs_human on
handoff, and lib/agent-loop.ts:204-207 maps escalated → needs_human. The Pilot is the only
autonomous workflow whose human handoff is invisible to the board.
2. The card's status is a function of the session's lifecycle, not the run's outcome. coding-store.ts:576:
The workflow does pass "error" for a failed run (coding-session.ts:754), but only inside the
branch gated on shouldEndSessionAfterRun({ openedByRun: … }) (:743). A session the human
opened survives the run, so that branch is skipped and the card is never updated at all — which
is csess_22d08431, still "running" 16 hours after its run died.
I did not fully trace why the other two read completed rather than failed. The consistent
explanation is that the session had already left active by some other path (the End button, the
orphan reaper, or a --force takeover — endSession defaults to "ended" → completed), so the
workflow's own endSession(…, "error") matched no row, changed was false, and no card write
happened. Stated as the likely cause, not a verified one; the structural claim above does not
depend on it.
Why it matters more than a wrong badge
The owner's stated read of these runs is "they keep dying and I retype Retry". The board is where he
would look to see that a run wanted him — and it is the one surface that actively told him
everything was fine. The push notification did fire for all four handoffs (pushed_at set on
every row, verified), so the information existed; it was in the tray and nowhere durable.
What to do — cheapest first
(a) Add needs_human to CodingCardStatus and set it when the handoff opens. The handoff block
already exists at coding-session.ts:620-640 and already calls notifyUser; one upsertCodingSessionCard(…, status: "needs_human") beside it, and one back to running on resume
(:659). This alone puts the card in the column that already renders.
(b) Make a failed run mark its card failed even when the session survives. Move the card write
out of the shouldEndSessionAfterRun branch — the card is about the work, and #271 deliberately
decoupled session lifetime from run lifetime. A session outliving its run is now normal, so a card
keyed only to session lifetime is structurally wrong, not just occasionally stale.
(c) A reconciler for stale running cards.sweepStaleRuns (lib/run-sweeper.ts) already
closes agent_loop_runs rows whose driver died after STALE_RUN_MS; it does not touch board cards. csess_22d08431 is 16h stale. Cheapest version: have the existing sweep close the matching card.
Drop the coding card and let the run's delegation card carry it. Rejected for the same reason,
plus these three runs had taskId: null (verified in their error_log context) — they were not
delegated, so there is no delegation card. The coding card is the only one they have.
Only fix the notification. Rejected: the notification already worked. The defect is that the
durable surface disagreed with it.
Acceptance criteria
While a Pilot run is parked in a handoff, its card is in Needs you; on resume it returns to Running.
A run that ends failed leaves its card in Failed, whether or not the session survives.
No card sits in Running more than STALE_RUN_MS after its run reached a terminal state.
(b) touches the one place a session leaves active (coding-store.ts:562), whose comment warns
that every terminal path routes through it so the card "can reliably follow". Moving the write out
must not create a second writer that can disagree — prefer calling upsertCodingSessionCard/closeCodingSessionCards explicitly from the workflow's end step and
leaving endSession's write for the paths that have no run.
(a) adds a status the console's board grouping must map to a column. defaultBoardColumns(["coding"])
currently maps running/completed/cancelled/failed with no declaration (coding-board.ts:19-20) —
check needs_human lands in "Needs you" and not in an "other" bucket before shipping.
Test that would catch a regression: assert board state at three points of one Pilot run
(start → handoff → terminal) rather than only at the end. No current test observes the mid-run
card, which is why this survived.
Cross-refs: #541 (why the runs died), #206 (why the card exists at all), #271 (why sessions outlive
runs), #349 (the same "handoff notification lands somewhere other than the control" defect, fixed
for apply and browser-task — this is the coding instance of it).
Split out of #541, which is about why those runs died. This one is about why the board said the
opposite of what happened.
Observed, production, 2026-08-13
Three Pilot runs on instance
a1d3522f(AIPA coder) died on 2026-08-12 withstuck not resolved in time— each after opening a human handoff and waiting 15 minutes for ananswer (#541). Here is that instance's board right now (
instance_board):csess_2dd3124cis run82739cb6, which failed. Board says completed.csess_302422b7is run1dbb3e54, which failed. Board says completed.csess_22d08431is runebc70ac1, which failed at 2026-08-12T22:56:23Z. Board saysrunning, ~16 hours later.
The board is the surface with a column literally named for this state, and it showed three
successes and one in-flight job for four dead runs.
Mechanism — the card tracks the SESSION, the run outcome never reaches it
1. There is no needs-human status a coding card can hold.
lib/coding-board.ts:21:Grep-verified as complete:
upsertCodingSessionCardis only ever called with"running"(
coding-store.ts:472), andcloseCodingSessionCardsonly with"cancelled"(:511) orstatus === "error" ? "failed" : "completed"(:576). And:Its two peer workflows both have it —
workflows/job-apply.tsparks a task inneeds_humanonhandoff, and
lib/agent-loop.ts:204-207mapsescalated → needs_human. The Pilot is the onlyautonomous workflow whose human handoff is invisible to the board.
2. The card's status is a function of the session's lifecycle, not the run's outcome.
coding-store.ts:576:The workflow does pass
"error"for a failed run (coding-session.ts:754), but only inside thebranch gated on
shouldEndSessionAfterRun({ openedByRun: … })(:743). A session the humanopened survives the run, so that branch is skipped and the card is never updated at all — which
is
csess_22d08431, still "running" 16 hours after its run died.I did not fully trace why the other two read
completedrather thanfailed. The consistentexplanation is that the session had already left
activeby some other path (the End button, theorphan reaper, or a
--forcetakeover —endSessiondefaults to"ended"→completed), so theworkflow's own
endSession(…, "error")matched no row,changedwas false, and no card writehappened. Stated as the likely cause, not a verified one; the structural claim above does not
depend on it.
Why it matters more than a wrong badge
The owner's stated read of these runs is "they keep dying and I retype Retry". The board is where he
would look to see that a run wanted him — and it is the one surface that actively told him
everything was fine. The push notification did fire for all four handoffs (
pushed_atset onevery row, verified), so the information existed; it was in the tray and nowhere durable.
What to do — cheapest first
(a) Add
needs_humantoCodingCardStatusand set it when the handoff opens. The handoff blockalready exists at
coding-session.ts:620-640and already callsnotifyUser; oneupsertCodingSessionCard(…, status: "needs_human")beside it, and one back torunningon resume(
:659). This alone puts the card in the column that already renders.(b) Make a failed run mark its card failed even when the session survives. Move the card write
out of the
shouldEndSessionAfterRunbranch — the card is about the work, and #271 deliberatelydecoupled session lifetime from run lifetime. A session outliving its run is now normal, so a card
keyed only to session lifetime is structurally wrong, not just occasionally stale.
(c) A reconciler for stale
runningcards.sweepStaleRuns(lib/run-sweeper.ts) alreadycloses
agent_loop_runsrows whose driver died afterSTALE_RUN_MS; it does not touch board cards.csess_22d08431is 16h stale. Cheapest version: have the existing sweep close the matching card.Alternatives considered and rejected
agent_loop_runsinstead of the session. Rejected: [coordination] Ticket 2 — make coding work visible to a supervisor (domain writes) #206 put codingsessions on the board precisely so a human-driven session (no loop run at all) is visible to a
supervisor. Keying to the run would delete that, which is the capability [coordination] Ticket 2 — make coding work visible to a supervisor (domain writes) #206 was filed to add.
plus these three runs had
taskId: null(verified in theirerror_logcontext) — they were notdelegated, so there is no delegation card. The coding card is the only one they have.
durable surface disagreed with it.
Acceptance criteria
Running.
failedleaves its card in Failed, whether or not the session survives.STALE_RUN_MSafter its run reached a terminal state.Regression risk
active(coding-store.ts:562), whose comment warnsthat every terminal path routes through it so the card "can reliably follow". Moving the write out
must not create a second writer that can disagree — prefer calling
upsertCodingSessionCard/closeCodingSessionCardsexplicitly from the workflow'sendstep andleaving
endSession's write for the paths that have no run.defaultBoardColumns(["coding"])currently maps running/completed/cancelled/failed with no declaration (
coding-board.ts:19-20) —check
needs_humanlands in "Needs you" and not in an "other" bucket before shipping.(start → handoff → terminal) rather than only at the end. No current test observes the mid-run
card, which is why this survived.
Cross-refs: #541 (why the runs died), #206 (why the card exists at all), #271 (why sessions outlive
runs), #349 (the same "handoff notification lands somewhere other than the control" defect, fixed
for apply and browser-task — this is the coding instance of it).