Skip to content

fix(workflows): status reconstruction reads current node rows, never ledger history - #733

Open
rcbevans wants to merge 4 commits into
mainfrom
fix/wf-status-reconstruct-identity
Open

rcbevans wants to merge 4 commits into
mainfrom
fix/wf-status-reconstruct-identity

Conversation

@rcbevans

Copy link
Copy Markdown
Contributor

Fixes the intermittent derived failed, not complete march failure on main by correcting reconstruct_workflow_status.

Defect

reconstruct_workflow_status folded wf_step_ledger terminal rows by step_key only, with no ORDER BY, and projected them onto pending nodes. Two consequences, both reproduced on real PostgreSQL 18.6:

  • One failed retry of a single sources.item map child was inherited by its still-live pending siblings, so a live run reconstructed failed while the product's own root writer kept __flow__ running.
  • The "latest terminal" overwrite depended on heap/insertion order, so ANALYZE could flip a verdict between premature complete and failed for identical logical rows.

Ruling and fix

An architecture ruling established the invariant: a ledger terminal never applies to a currently live node. The current committed jobs rows are the current-node record; wf_step_ledger is claim/attempt history and replay data. A node terminal and its own ledger terminal commit in one _run_tx1 transaction, so a live row beside ledger history is a deliberate live shape — a retry, a hold, an inner claim — never a partial commit.

So the fix deletes the overlay rather than re-keying it: _LEDGER_TERMINALS_SQL, both ledger maps, the ordered-overwrite loop and the pending-node projection are gone. Reconstruction builds NodeView directly from WORKFLOW_NODES_SQL and is now two statements with zero wf_step_ledger reads.

An earlier attempt keyed the ledger by job_id + attempt; review demonstrated it collapsed a node's own claim with its ctx.substep and loop-iteration claims, reconstructing held or killed live nodes as complete/failed. That approach is discarded, not refined.

Controls

Eight control families plus a query inventory, each asserting the exact seeded tuples, then the verdict, then the real WORKFLOW_ROOT_MAINTAIN_SQL root: failed and succeeded map siblings (with a NULL-vs-0 map-index check), held step and held loop with inner substep/iteration terminals in both directions, killed/reclaimed child isolated and joined, real ladder repend_attempt + scheduled→pending sweep, real manual retry/reopen with run_epoch, terminal node rows winning over their own contradictory ledger history in both directions, the two-statement/no-ledger pin at two populations, and heap-motion + ANALYZE byte-identity.

Non-vacuity, independently reproduced by the reviewer:

  • base 4441dd642: map-failed, map-succeeded, ladder-repend, manual-retry and heap-motion red;
  • the attempted job_id fix: six isolated reds;
  • its no-ORDER BY mutant: the same six;
  • the terminal-facts control also kills a naive own-job overlay (counting assert) and a counter-evading overlay (behavioural assert 'complete' == 'failed').

Verification

  • Frozen full CI root selection: 14,001 passed, 0 failed, 15 skipped (32m06s, HEAD and git status proven unchanged across the run)
  • PG18 system-e2e at CI shape: 48 passed
  • March e2e: passed, measured=305.00s bound=866.00s
  • Status/schema 39, pins 17, probes 20, focused 246
  • Ruff check and format clean (1,885 files); tree-pinned Pyright 0/0; ty 49/49
  • EXPLAIN (ANALYZE, BUFFERS) at 25,601 jobs / 1.2M ledger rows: workflow_nodes 19.3 ms over jobs/wf_edge, _NEVER_GRANTED_SQL 0.023 ms over jobs — no ledger relation, no sequential scan, no sort growth

Reviews

  • DeepSeek: 2cafeaa8… (REVISE: format, stale prose, vacuous terminal-facts control), 90e2e34f… (confirmations plus five remaining prose sites), 901c75b4… (CLEAN prose: 0 non-prose lines, AST-identical test module), b638db0b… (attribution: the nine evidence-door reds were a HEAD-mutation race in our own lane, not code)
  • Kimi: 6208b82e… (CLEAN, merge gated on the heavy lanes, which now pass)

Scope

One product file (_status.py), its controls, and prose corrections in five files. No schema, migration, ledger-writer, retry-writer or root-maintenance change. Pre-existing absorption and live-work-ordering disagreements were deliberately left out of scope by the ruling.

rcbevans and others added 4 commits November 13, 2026 23:50
…laim identity

reconstruct_workflow_status collapsed the step ledger's terminal rows by
step_key alone, so one map child's failed retry was inherited by its
still-live pending siblings: derive_workflow_status returned 'failed' for
a run whose own authoritative root writer (WORKFLOW_ROOT_MAINTAIN_SQL)
kept it running — the intermittent march e2e red. The 'latest terminal'
overwrite also relied on physical arrival order (no ORDER BY), so the
verdict flipped between samples after a tuple move + ANALYZE.

The terminal read now carries the full claim identity (job_id, step_key,
map_index, attempt) in a deterministic (attempt, created_at, id) order —
the memoized replay's own latest-attempt convention — and the pending-row
remap fires only for the row's OWN current claim's terminal: a
historical attempt's terminal cannot relabel the node whose claim has
moved past it, and a sibling's terminal can never answer for it.

Red-first controls (tests/test_wf_status_pins.py), both red on the
unmodified base for the defect's exact reason and green after:
- test_reconstruction_map_sibling_never_inherits_a_siblings_terminal
- test_reconstruction_latest_terminal_is_order_deterministic
…obs rows are the current-node record (the ruling's rework)

The ruling (RULING_STATUS_IDENTITY.md) rejects the (job_id, attempt)
fix and every exact-tuple latest-terminal projection: a jobs row that is
currently pending/scheduled/running is LIVE current state, and no
wf_step_ledger terminal may relabel it. The node terminal and its own
ledger terminal commit in one transaction (_run_tx1), so a live row
beside ledger history is a deliberate live shape — a ladder re-pend, a
manual retry, a held/awaited claim, an inner substep/iteration claim —
attempt history, never a recoverable partial commit.

_status.py: _LEDGER_TERMINALS_SQL and the flow-wide ledger fetch are
DELETED (with their ORDER BY — there is no terminal-selection rule to
order); ledger_by_job/ledger_attempt_by_job, the ordered overwrite and
the pending-node remap are gone. NodeView is built directly from every
WORKFLOW_NODES_SQL row; derive_workflow_status, the terminal-crash
fold, the absorption handling, _NEVER_GRANTED_SQL and its append are
unchanged. Reconstruction is TWO statements and reads no ledger table.

Controls: the two aggregate-masked controls are replaced by the
ruling's families — failed and succeeded map siblings (exact pending,
join phase separate, NULL-vs-0 arbiter check), held step and held loop
with inner substep/iteration terminals in both directions, the
killed/reclaimed child isolated then joined (both directions), the REAL
ladder repend + scheduled→pending sweep, the REAL manual retry/reopen
(unchanged attempt, run_epoch advanced), and the terminal node rows'
win over missing/stale/contradictory ledger history — each asserting
the exact seeded tuples, then the exact reconstruction verdict, then
the real WORKFLOW_ROOT_MAINTAIN_SQL root. The query-count pin asserts
exactly two statements at both populations and NEITHER may read
wf_step_ledger; the heap-motion + ANALYZE control proves byte-identical
verdicts with the ordering concept gone.
…, the stale ledger-status docs, the own-claim contradictory-ledger control

1. ruff format on the pins module (the manual-retry assertion's wrap);
   ruff check . and ruff format --check . both clean (1885 files).
2. The stale two-source/ledger-status commentary corrected everywhere
   the ruling's same-edit requirement reached: _sql_status.py's module
   docstring ('the ledger IS the status store' — the rejected model),
   the G7 fixture's docstring (the assertion reads the committed node
   rows; no ledger), engine.py's skip fan-in comment (the ledger stays
   claim/attempt history; the status read is the node rows), the pins
   module's section header + the two-sources test's docstring (one
   source: the committed node rows; no ledger overlay), and the upgrade
   guide's current-behavior line (the reconstruction consults no
   ledger; a live row beside ledger history is a deliberate live
   shape). The tx1→tx2 crash-window prose and the unrelated two-source
   rules (env queues, deploy capacity) are untouched — they name other
   mechanisms.
3. The terminal-facts control now seeds every contradictory ledger row
   on the NODE'S OWN job id (the arbiter tuple the deleted overlay
   keyed), _assert_ledger asserts the owning job id explicitly, both
   directions are covered (failed node over its own succeeded ledger;
   succeeded node over its own failed AND crashed ledger), the
   overlay's absence is asserted in-family (a counting connection may
   issue no wf_step_ledger statement), and the real maintenance runs in
   the family body (the G7 fixture runs it again at teardown). The
   unrelated-job variant is kept, explicitly documented as unrelated
   history. Fixture coherence: the failed-map join phase is a real
   fan-in (deps_pending=2 + the two wf_edge rows); the held shapes
   carry the hold's real faces (the scheduled_at deadline + the
   status='held' wf_signals row). RED under the injected own-job
   overlay mutant (the overlay-absence assert fires, recording the
   shim's ledger SELECT); green on the real tree.
The rework2 review's five residual sites: the agent-fleet user guide's
current-behavior line (the step ledger's attempted terminals plus the
node rows → re-derived from the committed node rows alone, no ledger
read, the one-transaction terminal invariant), the two_sources test's
three body comments (the ledger carries it / the two-source rule's
second source / the ledger's truth → the node row's own terminal fact
and the never-granted read), and the fixture section header (the
reconstruction's ledger terminal identity → the one-source law: the
ledger is history, not state). The full docs/+tests/ sweep finds no
remaining claim that status reconstruction reads the ledger, overlays
terminals, or combines two sources for status; the other two-source/
crash-window mentions name unrelated mechanisms (queue-strict
evidence, emit backpressure, the tx1→tx2 propagation window, the
attempt-chain wait analytics) and stand.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant