Description
Written-by: Triage
Split from #1507 — the pinned half of its triage comment (2026-10-28T02:22Z). #1507 keeps the shape decision (group retries vs mark superseded); this one does not depend on it and ships regardless.
What happens
With no run active and a failed last attempt, Runs shows that same attempt twice: a phantom Running row plus its own Failed history row.
plugins/workflow/client.js on main 0e6dfc0d:
liveStageRuns(:627-633) counts a run as live whenrun.live === trueand its status isrunningorfailed.runHistoryRows(:645-658) paints every row fromliveStageRunsasstatus: 'running'(:650-654), then appends everyworkflow.historyrow (:656). A failed-live attempt is therefore listed once asRunningand once asFailed.
Expected
One row per attempt. With no active run and a failed last attempt: exactly one row, labelled Failed, zero Running rows. A genuine retry still shows two distinguishable attempts — the retry is the live/current one, the earlier one stays Failed.
Scope (display only)
plugins/workflow/client.js — runHistoryRows (and liveStageRuns if the fix needs the run's own status). No src/service.js change for this half: lane.history already carries at, and that plus task.id is enough to tell a live failed attempt from its history row. No new state, no new key.
Red first
plugins/workflow/test/client.test.js already covers runHistoryRows (:906, :922) but only with running live runs. Add the failed case:
const task = { id: '#1422', title: 'PR 1422' }
const workflow = {
activeCount: 0,
queue: [],
runs: [{ id: 'run-a', live: true, status: 'failed', task }],
run: { id: 'run-a', live: true, status: 'failed', task },
history: [{ task, status: 'failed', at: '2026-10-28T02:00:00Z' }],
}
const rows = client.runHistoryRows(workflow)
assert.equal(rows.length, 1)
assert.equal(rows[0].status, 'failed')
assert.equal(rows.filter(row => row.status === 'running').length, 0)Red on base 0e6dfc0d (today it returns two rows, one running).
Acceptance
- The new case green; the existing
runHistoryRowscases (:906, :922) andthe runs tab shows one running row per concurrent live run(:934) stay green, so N concurrent live runs still render N rows. - UI check on a workflow whose last attempt failed:
Runsshows oneFailedrow and noRunningphantom; a real retry then shows two distinguishable attempts with the newest current.
Risk
Client-only display fix inside plugins/workflow; no secrets and no permission surface. The controller decides the tier.
Activity
Birfy commentedon Oct 28, 2026
Written-by: Triage
agent-ready— split from #1507 so the pinned half can start without waiting on that issue's shape decision. The defect (a failed-live run paintedrunninginrunHistoryRowsnext to its ownFailedhistory row), the red-first case, the files and the acceptance are in the body above; verified againstplugins/workflow/client.jsatmain0e6dfc0d.Birfy commentedon Oct 28, 2026
Written-by: Claim d573
Claimed — taking this to
dev/1583-runs-dedup-failed-live(branched frommain0e6dfc0d).First step: the red-first failed-live case in
plugins/workflow/test/client.test.js, thenrunHistoryRowsinplugins/workflow/client.jspaints one row per attempt and skips a history row that is already the live attempt.Scope stays the issue body: client-only display fix, no
src/service.jschange, no new state.fix(workflow): list a failed live attempt once in Runs (ThinkFlowLab#…