From 1c0b9fb1597865f82812b101283e7aca01b0c03f Mon Sep 17 00:00:00 2001 From: lex Date: Sun, 9 Aug 2026 15:33:52 +0800 Subject: [PATCH] fix(dag): block unsafe recovery restart --- packages/opencode/src/dag/runtime/recovery.ts | 2 +- .../opencode/test/dag/dag-recovery.test.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/dag/runtime/recovery.ts b/packages/opencode/src/dag/runtime/recovery.ts index 799234d50a..a9bbc0848a 100644 --- a/packages/opencode/src/dag/runtime/recovery.ts +++ b/packages/opencode/src/dag/runtime/recovery.ts @@ -65,7 +65,7 @@ export function reconcileWorkflow( // never revisit it if the workflow is about to become terminal. if (node.status === "pending" || node.status === "queued") { if (node.childSessionId && cancelSession) { - yield* cancelSession(node.childSessionId).pipe(Effect.catch(() => Effect.void)) + yield* cancelSession(node.childSessionId) } continue } diff --git a/packages/opencode/test/dag/dag-recovery.test.ts b/packages/opencode/test/dag/dag-recovery.test.ts index e1537d604e..9a6ae584e4 100644 --- a/packages/opencode/test/dag/dag-recovery.test.ts +++ b/packages/opencode/test/dag/dag-recovery.test.ts @@ -171,6 +171,24 @@ describe("reconcileWorkflow", () => { expect(result).toEqual({ reconciled: 0, ownershipLost: 0 }) }) + it("aborts recovery when a stale restart-orphan session cannot be cancelled", async () => { + const events: TrackedEvent[] = [] + const nodes = [makeNodeRow({ id: "n1", status: "queued", childSessionId: "ses_stale" })] + const dagLayer = makeDagLayer(nodes, events) + const checkStatus = () => Effect.succeed("active" as const) + const cancelSession = () => Effect.fail(new Error("cancel unavailable")) + + const exit = await Effect.runPromise( + reconcileWorkflow("wf-1", checkStatus, cancelSession).pipe( + Effect.provide(dagLayer), + Effect.exit, + ), + ) + + expect(Exit.isFailure(exit)).toBe(true) + expect(events).toEqual([]) + }) + it("cancels and fails a zero-message child classified as unknown exactly once", async () => { const events: TrackedEvent[] = [] const cancelled: string[] = []