fix(ship): resume pipeline ownership after restarts - #2133
Conversation
There was a problem hiding this comment.
Changes requested: A stale child snapshot can rehost a dead pipeline parent indefinitely, and the updated behavior contradicts an existing run-history criterion.
Warning
Changes requested · head 7dca33b · 2 findings: 1 major, 1 minor
| Severity | Finding | Where |
|---|---|---|
| major | F1 Admission snapshot can immortalize a parent after its child closes | src/core/boot.ts:242 |
| minor | F2 Spec contradiction — docs/reference/specs/run-history.md item 36: past-deadline parents now sometimes rehost instead of always closing | docs/reference/specs/run-history.md:111 |
Full review
F1 — childrenOf treats every child in the pre-classification ledger snapshot as live. If a past-deadline parent and a finishing or non-resumable child are reclaimed together, the parent rehosts before that child closes. The launcher then adopts and heartbeats the parent indefinitely even when the Workflow is terminal, suppressing the required interruption notice. Determine parent liveness from children that will actually resume, or re-evaluate the parent after child classification.
F2 — Item 36 still states that every hosted parent past its deadline closes interrupted, while the new criterion and implementation rehost one when its Workflow or child remains live. Update the earlier criterion so the behavioral contract has one rule.
7dca33b to
0a5727d
Compare
There was a problem hiding this comment.
Changes requested: The prior findings are fixed, but a transient child-classification failure can still close its live parent and emit re-issue guidance.
Warning
Changes requested · head 0a5727d · 1 finding: 1 major
| Severity | Finding | Where |
|---|---|---|
| major | F3 A failed child classification can orphan the child and close its parent | src/core/boot.ts:251 |
Full review
F3 — If classifying a reclaimed child throws—for example, readEvents transiently fails—the catch leaves its row live in outcome.failed, but childrenOf excludes it because it never entered resumingIds. A past-deadline parent can then close and post “re-issue” while that child remains live and owned by this generation; periodic sweeps will not retry a self-owned row. Treat failed-but-still-live children as parent liveness, or defer the parent when any child classification fails.
b9f9712 to
d96058c
Compare
There was a problem hiding this comment.
Changes requested: F3's reported throw path is fixed, but a non-throwing child finish refusal can still orphan the live child from its parent.
Warning
Changes requested · head d96058c · 1 finding: 1 major
| Severity | Finding | Where |
|---|---|---|
| major | F4 A refused child finish is still omitted from parent liveness | src/core/boot.ts:372 |
Full review
F4 — When closeReclaimed returns a non-throwing refusal such as fenced, the child is added to outcome.failed but not failedIds. If another generation now owns that still-live child, childrenOf excludes it and the past-deadline parent can close with re-issue guidance. Add refused-but-live child classifications to the parent-liveness facts and cover the refusal path.
Co-Authored-By: coreplane-switchboard[bot] <318072483+coreplane-switchboard[bot]@users.noreply.github.com>
d96058c to
4afc77b
Compare
There was a problem hiding this comment.
LGTM: F4 is fixed: fenced child-finish refusals now remain parent-liveness facts, with regression coverage for the foreign-live-child path.
Note
Approved · head 4afc77b · no findings
Full review
F4 is resolved: fenced child-finish refusals now preserve the child as a parent-liveness fact, and the regression covers the foreign-owner path. No findings remain.
Restarts now retain a ship parent for every child that remains live, including a child whose finish is fenced by a newer generation. This prevents ownerless work and false re-issue guidance while recovery resumes the pipeline.
Why: Issue #2116 exposed child work resuming after a roll while its parent closed; F4 on PR #2133 found the same risk when child finish is refused. Record 0046 makes recovery a fresh admission, so ownership must remain durable across generations.
Where to look
Feedback wanted: Please scrutinize the fenced-finish liveness inference, terminal-child exclusion, and whether the one-admission card reconstruction selects the correct child.
Risk: 649 changed lines across 14 files. Splitting resumed tuning from ownership recovery was considered, but both shape one restart admission. Wrong liveness can orphan work or retain a host key; rollback is the single commit.
Verified: Four focused Vitest files passed 569 tests; fast gates passed. Pushed CI has 27 completed checks green/skipped with CodeQL pending; a live restart remains human-gated.
Decisions (4)
unknown-runmeans no durable row remains. Afencedresult atomically proves another generation owns the row, so only that refusal joins failed-but-live liveness facts.Validation (9 criteria)
npx vitest run src/core/boot.test.tsfailed the new regression because the parent closed interrupted; after the fix it passed 26/26.npx vitest run src/core/boot.test.ts src/core/dispatch/resolve.test.ts src/core/dispatcher.test.ts src/core/resumeLaunch.test.tspassed 4 files and 569 tests.NODE_OPTIONS=--max-old-space-size=6144 npx tsc --noEmit -p tsconfig.jsonpassed with exit 0.npx prettier --checkon the 10 changed TypeScript files passed.npm run hygiene:checkpassed: 9 files, 37 listed hits, 138 allowed lines.npm run specs:checkpassed: 50 specs and 5565 proof references checked.npm run check:pr-title -- "fix(ship): resume pipeline ownership after restarts"passed.For agents
Pushed head 4afc77b is one coherent commit rebased onto origin/main at f95ad1a. F4 is covered by the fenced-finish regression in boot.test.ts; it failed before the fix because the past-deadline parent closed and passed afterward. The rebase conflict in routing-and-config.md preserved main's newly merged completed-target facts and this PR's fresh-segment configuration sentence. No generated files changed. CI owns full verification.
Requested by @justinhelmer in slack:C0BRRHKFLCB
🤖 Generated with Claude Code