v5.17.5 — a decision that was both running and completed
Both running and completed. A decision that reported its outcome through the durable-finality endpoint stayed running in the ledger forever, so Decision Replay rendered a red RUNNING beside a green Completed badge on the same line. Reported by the maintainer reading a decision detail page.
Fixed
- The outcome POST never closed the lifecycle.
action_recordscarries two columns —status(lifecycle) andoutcome_status(durable finality) — closed by two different paths, and the reconciliation only ran one way. A terminalPATCH /api/actions/:idimplicitly advancesoutcome_status, butPOST /api/actions/:id/outcomewrote only theoutcome_*columns and leftstatus='running'.setActionOutcomenow closes the lifecycle in the same UPDATE (completed→completed,partial/failed→failed,lost_confirmation→unknown) and stampstimestamp_endthroughCOALESCE, so a caller-supplied end time still wins. Only a still-open lifecycle (running/pending/ NULL) flips: an already-terminal status is preserved, andpending_approvalis left to the approvals expiry sweep. No new lifecycle value is introduced — all three targets were already written by the stale-outcome sweep. - Nothing could heal an affected row. Both of the sweep's UPDATEs gate on
outcome_status(the primary on'pending', the backfill on'lost_confirmation'), so a row that reportedcompletedwas unreachable by either and stuck atrunningpermanently. It also counted as in-flight in the operations stats and tripped the doctor's "Zombie running actions" check, whose fix hint — opening/decisionstriggers the reconciliation sweep — could never work for that row. The backfill now covers every terminaloutcome_statusand maps each to the same lifecycle value as the write path, so rows already stuck reconcile on the next sweep: cron, or the lazy trigger on the actions list. No migration.
Verification
The repository's unit tests mock the SQL client, so they pin the statement text but cannot show that Postgres accepts it — a real gap here, because timestamp_end is text in the drizzle schema and timestamptz on some migrated instances. The change was additionally run against a live Postgres in a throwaway org: the reported case, the partial mapping, an already-terminal lifecycle being preserved, a caller-supplied timestamp_end surviving, and a pre-existing stuck row being backfilled. All passed.
Platform only — no Node or Python SDK source change, so the SDKs are intentionally not republished.
Maintained by Claude (an AI) under the delegation in MAINTAINER.md. Session notes: docs/maintainer-log.md.