Skip to content

[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

Description

@serge-ivo

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):

columns: Waiting | Running | Needs you | Failed | Blocked | Done | Cancelled
Running: csess_22d08431… "Coding: dev/aipa (codex)" status: running
Done: csess_43be1f0e… "Coding: dev/aipa (codex)" status: completed
csess_2dd3124c… "Coding: dev/aipa (claude)" status: completed
csess_302422b7… "Coding: dev/aipa (claude)" status: completed
Needs you: (empty)
Failed: (empty)
  • csess_2dd3124c is run 82739cb6, which failed. Board says completed.
  • csess_302422b7 is run 1dbb3e54, which failed. Board says completed.
  • csess_22d08431 is run ebc70ac1, which failed at 2026-08-12T22:56:23Z. Board says
    running, ~16 hours later.
  • "Needs you" was empty the entire time all three sat waiting on a human, and it is empty now.
  • "Failed" has never held any of them.

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:

exporttypeCodingCardStatus="running"|"completed"|"cancelled"|"failed";

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:

$ grep -c needs_human workers/api/src/workflows/coding-session.ts
0

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:

if(changed)awaitcloseCodingSessionCards(env,instanceId,userId,[sessionId],status==="error" ? "failed" : "completed");

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.

Alternatives considered and rejected

Acceptance criteria

  1. While a Pilot run is parked in a handoff, its card is in Needs you; on resume it returns to
    Running.
  2. A run that ends failed leaves its card in Failed, whether or not the session survives.
  3. No card sits in Running more than STALE_RUN_MS after its run reached a terminal state.
  4. A human-opened session with no loop run still shows as Running while live ([coordination] Ticket 2 — make coding work visible to a supervisor (domain writes) #206 preserved).

Regression risk

  • (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).

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions