Symptom
Crash recovery treats a pending/queued node with a stale childSessionId as a restart orphan. If cancelling that old child Session fails, recovery swallows the error and immediately makes the node eligible for spawnReady, allowing the old and replacement executions to overlap.
Reproduction
Baseline: dev@3642796de
cd packages/opencode
bun test test/dag/dag-recovery.test.ts --test-name-pattern "aborts recovery when a stale restart-orphan session cannot be cancelled"
Observed deterministic red result: Exit.isFailure(exit) was false; the cancellation error was discarded.
Root cause
The restart-orphan branch in reconcileWorkflow runs cancelSession(...).pipe(Effect.catch(() => Effect.void)) and then continues. Its own comment requires the old child to be cancelled before re-scheduling, but the implementation turns a failed cancellation into successful reconciliation.
recoverWorkflow already catches a reconciliation failure and abandons adoption, so propagating this error prevents spawnReady until a later recovery attempt can establish single execution ownership.
Expected contract
A restart orphan may re-enter scheduling only after the stale child Session cancellation succeeds. A cancellation failure must abort this recovery attempt without inventing a node terminal event.
Acceptance criteria
- Retain a red test at the
reconcileWorkflow seam.
- Propagate stale-child cancellation failure for
pending and queued restart orphans.
- Preserve best-effort cancellation for already-running ownership-loss nodes, because those nodes are terminalized and cannot be re-spawned.
- Publish no node event for the aborted restart-orphan recovery.
- Run the full recovery test file, OpenCode DAG suite, and package typecheck.
Symptom
Crash recovery treats a
pending/queuednode with a stalechildSessionIdas a restart orphan. If cancelling that old child Session fails, recovery swallows the error and immediately makes the node eligible forspawnReady, allowing the old and replacement executions to overlap.Reproduction
Baseline:
dev@3642796deObserved deterministic red result:
Exit.isFailure(exit)wasfalse; the cancellation error was discarded.Root cause
The restart-orphan branch in
reconcileWorkflowrunscancelSession(...).pipe(Effect.catch(() => Effect.void))and then continues. Its own comment requires the old child to be cancelled before re-scheduling, but the implementation turns a failed cancellation into successful reconciliation.recoverWorkflowalready catches a reconciliation failure and abandons adoption, so propagating this error preventsspawnReadyuntil a later recovery attempt can establish single execution ownership.Expected contract
A restart orphan may re-enter scheduling only after the stale child Session cancellation succeeds. A cancellation failure must abort this recovery attempt without inventing a node terminal event.
Acceptance criteria
reconcileWorkflowseam.pendingandqueuedrestart orphans.