Skip to content

The board shows attempts:1 for a job that ran 9 times, asks for help with no reason or timestamp, and offers takeover of ended sessions #592

Closed

Description

@serge-ivo

The board under-reports attempts by up to 9×, points "Needs you" at ended sessions, and carries neither reason nor timestamp

Measured live 2026-08-15 across 83 cards on 5 boards.

1. attempts is 1 on all 83 cards, contradicting the board's own note

The board states: "retries of the same job collapse into one; attempts = run count". Counter-evidence from check_instance_loop on the same instances:

instance card runs carrying that sessionId card
f8ddc272 csess-csess_c960d431… 9 (9f3cf5ab, 026d4255, f19d0654, 0dce25fb, 2ab928b6, 86a7ad7f, ea27fbfb, 0cba259e, c7a03659) attempts:1, completed
bd43f4de csess_c306e923 3 (one needs_human, one completed, one failed) attempts:1
e4d2d031 csess_9228b52f 2 attempts:1

One of the nine (9f3cf5ab) failed. The board shows a single clean success where nine runs happened, one of them a failure.

2. Four of five "Needs you" cards point at sessions that ended

instance card card status session ended
e4d2d031 csess-csess_88a4de4b… needs_human ended 09:29:44
f8ddc272 csess-csess_f7a6fcec… needs_human ended 09:27:41
e26f69d0 csess-csess_f6266368… needs_human ended 09:29:01
5219a03a csess-csess_e94eb26a… needs_human ended 09:28:22
bd43f4de csess-csess_c306e923… needs_human active (capture-verified) —

Both halves are true in isolation — the run needs a human, the session is over — but the card is keyed on the session (jobKey: csess-<id>) and its affordance is "take over" something closed. board.ts:334 is write-through with no read-time join; closeCodingSessionCards' openOnly:true guard is correct for failed/completed and leaves needs_human pointing at a dead session.

3. The card that asks for help carries no reason and no timestamp

All 5 needs_human cards have detail: "" while the run carries the full explanation ("the limit does not reset until 2026-08-17 16:00 +10:00 … beyond the 6 hours a run may wait"). No card on any of the 5 boards carries a timestamp.

Concrete cost: e4d2d031's "Waiting"/queued card task_fbf2744e… is undated on the board; instance_task_events dates it 2026-08-15T08:55:25.916Z — queued 1h13m before the audit and, given the engine limit, unable to run until Aug 17. Neither fact appears where the owner looks.

Why this misleads into a wrong action

The board is the "does anything want me?" surface (#553). It currently asks for attention without saying why, offers to take over something that no longer exists, and hides that a job was retried nine times with a failure among them.

Acceptance criteria

  1. attempts reflects the run count, or the note stops claiming it does. Verify against a card with a known multi-run session — csess_c960d431 has nine.
  2. A needs_human card whose session has ended either resolves, or states that the session is over and offers the action that is actually available.
  3. A card asking for human attention carries the run's reason and a timestamp.
  4. A guard asserts board card state agrees with the run/session it is keyed on, denominator stated per ADR 0002 over all cards on a fixture board — not one card.
  5. Red demonstrated on the nine-run case.

Verified vs inferred

  • Verified: all counts and card values read live; board.ts:334; the nine run ids; the task-event timestamp.
  • Inferred: that attempts is never incremented anywhere. It reads 1 on 83/83 and the note says otherwise, but the writer was not traced — that is the first thing to establish.

Activity

  1. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    Shipped in 37bb2ea (on main, CI + API/MCP/Host deploys all green).

    The traced answer to the attempts question

    The issue labelled "attempts is never incremented" as inferred and asked for the writer to be
    traced first. There is no writer, and nothing was ever incremented — attempts is not a counter.

    buildInstanceBoard groups the instance's instance_runtime_tasks by jobKeyForTask and reports
    arr.length (board.ts:335), so attempts counts card rows sharing a job key, not runs.

    • For an apply card those are the same number: each attempt inserts its own task row and they
      collapse on the normalized URL. board-config.test.ts:295 already pinned that at 2, so the note
      was true for that family all along.
    • For a coding card it is pinned at 1 by construction, and correctly so at the layer that does
      it: codingCardId is csess-<sessionId> (coding-board.ts:52) and upsertWorkCard is
      ON CONFLICT(id) DO UPDATE (work-card.ts:22) — one session is one row forever. That dedup is
      the feature ("so every transition upserts the SAME row rather than piling up").

    A coding session's attempts are its agent_loop_runs, in a different table, which the board never
    read. So the fix is not a write — it is the read-time join board.ts:334 never had, which is also
    the only form that can be correct: a session leaves active in five places (enumerated in
    coding-board.ts) and a write-through would have to be added to all five and kept there.

    What changed

    lib/board-runs.ts (new) joins each coding card to its runs and its session at read time, per
    coding-board.ts's own rule that the run is the authority (#553).

    1. AC1 — attempts is the runs on that session, newest first. A hand-driven session with no
      loop run keeps its single row-derived attempt ("one session, no automated runs" is the truth
      there). The MCP note now says attempts = the runs behind it, which is finally accurate for
      both card families.
    2. AC2 — a needs_human card whose session is over stops asking. Where the run has since
      reached a verdict, the card adopts it. Where the run is also still parked, nobody ever
      answered, and the card becomes failed — never completed, which is the exact overwrite
      closeCodingSessionCards' openOnly guard exists to prevent. The fifth card (csess_c306e923,
      capture-verified active) is untouched, as is a card whose session row is missing: an absence is
      not a death. Cards also carry sessionEnded: true so a reader can say the takeover is gone
      without inferring it from a status. No console change was needed — leaving needs_human is what
      withdraws the takeover affordance.
    3. AC3 — the card carries the run's reason (all five had detail: "" because
      setCodingSessionCardStatus patches only $.status); a live progress line (#207B) is never
      overwritten. The missing timestamp was an MCP projection loss, not a board one:
      BoardItemView.updatedAt existed all along and groupBoard's fixed card list dropped it. Added.
    4. AC4 — board-runs.test.ts asserts the invariant over a 9-session fixture covering every
      combination observed live, with the denominator asserted and printed per ADR 0002 (G1/G2) and
      unresolvable cards reported rather than skipped (G3).
    5. AC5 — red demonstrated (G4) with the join disabled: attempts 1 ≠ 9 runs on
      csess_c960d431, four needs_human on a ended session, and the nine-run card returning one
      attempt.

    No MCP_SERVER_VERSION bump: the surface fingerprint excludes descriptions and covers no response
    body, and neither tool declares an outputSchema.

    Verified / not verified

    Verified: full suite green; the new guard red-then-green on the nine-run case; typecheck,
    biome, file-size, bare-catch, surface-lock, migrations, test-isolation, design-tokens,
    agents-allowlist, qa-config, docs-drift; Playwright 405 passed (chromium + webkit); CI and all
    three deploys green; api.proagentstore.online/health and mcp.proagentstore.online/health live.

    NOT verified in production: I hold no session token, and both the board API and the MCP surface
    are auth-gated, so I could not read csess_c960d431 live to confirm it now reports nine attempts,
    nor confirm the four needs_human cards have left the column. Leaving this open for that one
    check
    — it needs an authenticated read (instance_board on f8ddc272, e4d2d031, e26f69d0,
    5219a03a, bd43f4de), which the pags-live agent can do.

  2. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    Verified live — the check 37bb2ea could not make without a session token

    Read from production 2026-08-15 on f8ddc272 (Heartfull), 24 cards:

    defect before after
    attempts on csess_c960d431 1 9 — the exact nine runs
    attempts on csess_e4775bf7 1 2
    cards carrying a timestamp 0 of 83 every card has updatedAt
    needs_human on an ended session offered "take over" moved to Failed: "The session ended while this was waiting for you — there is nothing left to take over."
    detail on the help-asking card "" full run reason, including the Aug-17 reset instant

    sessionEnded: true is now on the card, and the board note documents it. All three defects in this issue are closed on live data.

    One residual contradiction the new data exposes

    Two cards sit in Done with status: "completed" and a detail that says the run failed:

    • csess_c960d431 — status: completed, runStatus: completed, attempts: 9, detail: "outcome: failed — run error: UserAiProviderError: The AI provider stopped sending mid-reply…"
    • csess_e4775bf7 — status: completed, runStatus: completed, attempts: 2, detail: "outcome: failed — stuck not resolved in time"

    With attempts: 9 this may be correct in substance — eight runs succeeded and the last one failed — but the card presents a column and a status saying success beside prose saying failure, and a reader cannot tell which run the detail describes.

    The audit established that one of csess_c960d431's nine (9f3cf5ab) failed. So the open question is narrow and worth answering explicitly rather than by inspection: when a job has N runs with mixed outcomes, which one does detail describe, and should status reflect the last run, the best, or the fact that outcomes were mixed?

    This is the same class this issue fixed — a summary that cannot express what actually happened — one level in. Recommend it stays open on that point, or is split out if the answer is a design decision rather than a fix.

    Verified vs inferred

    • Verified: every value above read from the deployed board; the nine run ids from the earlier audit.
    • Inferred: that eight of the nine succeeded. Only 9f3cf5ab was confirmed failed; the other eight outcomes were not individually re-checked, so "mixed" is established but the ratio is not.
  3. added
    P1: blocks external usersMust be true before someone who is not the owner can run an agent (#68)
    bugSomething isn't working
    backendBackend / Worker / API work
    frontendFrontend / UI work
    coderThe Coder wedge agent (#68) — Engine, Pilot, Co-pilot, Loop, Overseer
    observabilityA displayed value the code cannot produce, or that means something other than its label
    on Aug 18, 2026
  4. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    ContributorAuthor

    Shipped in de46d2a7. CI green, API + MCP deploys green, /health 200 on both. Leaving this open for one authenticated read — see the bottom.

    The residual, answered

    The last comment left one question open: "when a job has N runs with mixed outcomes, which one does detail describe, and should status reflect the last run, the best, or the fact that outcomes were mixed?" It was a real contradiction — two cards in Done, status: "completed", carrying prose that said the run failed.

    The mechanism, traced rather than guessed. Both halves came out of reconcileCodingCard, from two different runs. runStatus is the card's own, written by the run that owns it; description was borrowed unconditionally from runs[0], the newest agent_loop_runs row. Nothing required them to describe the same run. And the card's own run is often not in that table at all: POST …/coding/sessions/:sid/run (routes/coding.ts:688) creates the Workflow with no loopRunId, so a session driven that way writes the card and leaves no run row. That is also why the obvious fix is wrong — "adopt the newest row's status" would let a stale historical run overwrite a card a live, row-less run had just written.

    The rule now:

    1. detail describes the run that AGREES with the card's status. A card quotes its own run or it quotes nothing, so the column and the prose cannot contradict each other. One exception, and it says so on the card: the run a needs_human card was waiting on when its session died — why it was waiting is the explanation for that card.
    2. status stays where it is, for the reason above.
    3. Mixedness is a COUNT — 9 runs: 8 completed, 1 failed — leading the detail, per card-detail.ts's rule from A board card's detail is cut at exactly 300 chars mid-word with no ellipsis — 13 of 15 irreversible acts vanish, and the full text is already on the same card #568 that the count goes first and is never what gets cut. Emitted only when the runs disagree; a tally on every card is noise on the many to serve the few.

    So csess_c960d431 can no longer read as one clean success, and it can no longer read as a failure either: it reads as nine runs, eight of them completed and one failed, with its own run's account beside them.

    Verified / not verified

    Verified: 9,827 tests; typecheck (11 projects); biome; check-file-size, check-migrations --require-history, check-surface-lock --require-history, check-mcp-parity, check-bare-catch, check-test-isolation, check-design-tokens, check-agents-allowlist, check-qa-config; docs:drift; docs:build. Red demonstrated with the join removed — 6 failing, including the fixture guard. The board invariant now measures rule 4 over the whole fixture and states its own denominator per ADR 0002 (4 cards stand for runs that ended differently, asserted, because that arm could otherwise pass over zero cards).

    NOT verified in production. The board is auth-gated (/v1/instances/…/board → 401 without a session) and confirming it live would mean reading — and in the failure case moving — the owner's real cards. So the two live cards this fix is written from were not re-read. pnpm test:e2e skipped: the diff touches no store/ or agents/coder/web.

    The one check left, for an authenticated reader (pags-live can do it): instance_board on f8ddc272, cards csess_c960d431 and csess_e4775bf7. Expected now — the detail no longer contains outcome: failed under a completed status, and leads with 9 runs: 8 completed, 1 failed / 2 runs: 1 completed, 1 failed. If that reads right, this can close.

    Verified vs inferred

    • Verified: routes/coding.ts:688 creates the workflow with no loopRunId; reconcileCodingCard's two sources; every gate above.
    • Inferred: that the row-less /run path is what produced the specific completed on csess_c960d431. It is a path that produces exactly this shape; which of the nine runs wrote that status was not established, and the fix does not depend on it — the invariant is that the two halves agree, whichever writer won.
  5. serge-ivo commented on Aug 23, 2026

    @serge-ivo
    ContributorAuthor

    DELIVERED — closing. Verified in code at HEAD and re-measured live on the same board this ticket was filed from.

    Commits: 37bb2ea1 (the read-time join) and de46d2a7 (one card, one run's voice).

    Each acceptance criterion, against main

    AC Evidence
    1. attempts reflects the run count, or the note stops claiming it does workers/api/src/lib/board-runs.ts:1-20 states the finding this ticket asked for — "attempts was never a counter and nothing ever incremented it … buildInstanceBoard groups the instance's instance_runtime_tasks by jobKeyForTask and reports arr.length" — and replaces it with the read-time join at board.ts:375 (reconcileCodingCard). Pinned by board-runs.test.ts:44 "reports nine runs for the nine-run session, with the failure among them (#592 case 1)"
    2. a needs_human card whose session ended resolves, or says the session is over board-runs.ts:106 /** Said when the session died under a card that was still asking for a human. */; tests at board-runs.test.ts:90,100,114, and the control at :124 — "leaves a card asking while its session is still active — that one really does want you"
    3. a card asking for attention carries the run's reason and a timestamp reason: board-runs.test.ts:144 "fills an empty detail from the latest run (#592 case 3)", with :154 guarding that a live progress line is never overwritten (#207B). Timestamp: board-runs.ts:193 — runs.map((r) => ({ id: r.runId, status: r.status, updatedAt: new Date(r.at).toISOString() }))
    4. a guard asserts card state agrees with the run/session, denominator stated board-runs.test.ts:369 describe("board invariant — every coding card agrees with the run and session it is keyed on") → :370 "holds over the whole fixture board, and says how many cards it measured"
    5. red demonstrated on the nine-run case board-runs.test.ts:448 "the nine-run card reports nine attempts and its failure, end to end"; :456 "the four ended 'Needs you' cards leave the column; the active one stays"

    Measured live today, not inferred

    GET /v1/instances/f8ddc272-0390-4826-8812-94989e3d2ebd/board — the same instance and the same session csess_c960d431:

    status: completed | attempts: 9
    description: "9 runs: 8 completed, 1 failed | outcome: done — The keyboard flash-and-hide bug fix …"
      9f3cf5ab-1b46-4528-980a-f220d32807ae  failed     2026-08-12T07:18:48.186Z
      026d4255-26b9-4ac3-8072-b502d62246d8  completed  2026-08-12T04:56:23.709Z
      f19d0654-c413-44f2-ac1a-c90afb4bb22f  completed  2026-08-12T02:57:07.864Z
      0dce25fb-e1c3-4ee6-8fb6-8f14f27c33d3  completed  2026-08-12T00:06:46.081Z
      2ab928b6-cb89-4b82-80a9-3dddbe47e536  completed  2026-08-11T23:11:24.601Z
      86a7ad7f-58f7-4224-b064-98cbf405e995  completed  2026-08-11T22:37:35.496Z
      ea27fbfb-c2bd-4bcb-8ffd-b17dca6eeb36  completed  2026-08-11T22:19:49.096Z
      0cba259e-6d26-45ba-a2c4-1b84489419e7  completed  2026-08-11T21:39:03.861Z
      c7a03659-5aef-4033-8238-5418fa45f0be  completed  2026-08-11T21:33:17.124Z
    

    All nine run ids are the ones named in the ticket's own table, the failure is among them and named, and every attempt carries a timestamp. This is the exact case the title was written from — "attempts:1 for a job that ran 9 times" — and it now reads 9.

    Case 2 is also live on the same board: card csess-csess_f7a6fcec-… now opens with "The session ended while this was waiting for you — there is nothing left to take over."

    Verified: every file:line above; the live board response quoted verbatim. Inferred: nothing — both halves of the finding were re-measured against production rather than read off the test suite.

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

    P1: blocks external usersMust be true before someone who is not the owner can run an agent (#68)backendBackend / Worker / API workbugSomething isn't workingcoderThe Coder wedge agent (#68) — Engine, Pilot, Co-pilot, Loop, OverseerfrontendFrontend / UI workobservabilityA displayed value the code cannot produce, or that means something other than its label

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions