From 60a0acb396bc75f7db703fcce0581f55f3e5b6da Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:00:10 +0800 Subject: [PATCH] fix(cutter): retain completion-read failures during cleanup retry --- docs/reverse-engineering-tools.md | 3 + src/cutter/CutterBridgeService.ts | 10 ++- .../providers/CutterBridgeService.test.ts | 87 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/reverse-engineering-tools.md b/docs/reverse-engineering-tools.md index ce3ac5d3c6..1c50b0d890 100644 --- a/docs/reverse-engineering-tools.md +++ b/docs/reverse-engineering-tools.md @@ -135,6 +135,9 @@ block new commands until they can be retried successfully. Status proof failures identify `cutter_command` and retain `status` in `details.diagnostics.auxiliary_operation`; malformed proof still blocks dispatch. Descriptor failures likewise retain the requesting operation through cleanup. +If a completion-proof read fails while an earlier command is unresolved, retry +admission preserves both errors and their independently owned resources. Releasing +one owner does not settle the other, and the command is not replayed to recover proof. Command admission reports typed cancellation and treats the caller's abort reason as opaque metadata, without inferring cleanup ownership from it. A generation mismatch is an input correction only when the authenticated reply confirms diff --git a/src/cutter/CutterBridgeService.ts b/src/cutter/CutterBridgeService.ts index 4509f31f28..f02e2a58d0 100644 --- a/src/cutter/CutterBridgeService.ts +++ b/src/cutter/CutterBridgeService.ts @@ -73,7 +73,15 @@ export class CutterBridgeService { requestId, pending.completionToken, ); - if (!receipt.ok || !receipt.value) return err(pending.failure); + if (!receipt.ok) + return err( + analysisErrorWithCleanupFailure( + pending.failure, + receipt.error, + "cutter_command", + ).bindCleanupDependencies([pending.failure, receipt.error]), + ); + if (!receipt.value) return err(pending.failure); const { failure } = pending; failure.resolveCleanup(); this.#pendingCleanup.delete(requestId); diff --git a/tests/boundary/providers/CutterBridgeService.test.ts b/tests/boundary/providers/CutterBridgeService.test.ts index f7a7771761..b46f9dca32 100644 --- a/tests/boundary/providers/CutterBridgeService.test.ts +++ b/tests/boundary/providers/CutterBridgeService.test.ts @@ -13,6 +13,93 @@ const cutterInput = { command: "pd 4", }; +it.each(["descriptor-first", "command-first"] as const)( + "retains receipt-read cleanup alongside pending command ownership: %s", + async (order) => { + const descriptor = new ProviderCleanupError( + "cutter", + ["descriptor-fixture"], + { reason: "controlled receipt descriptor close failure" }, + { operation: "cutter_command" }, + ); + let readFails = true; + let terminal = false; + const execute = vi.fn(async () => ({ + requestId: "4b18ce4d-bf31-456e-ae31-e8c547b2f9db", + output: null, + currentFile: null, + documentGeneration: 7, + identityStatus: "partial" as const, + cutterVersion: null, + executionState: "unknown" as const, + error: "missing-reply", + message: "completion remains unknown", + outputTruncated: false, + })); + const service = new CutterBridgeService({ + listSessions: async () => ({ + sessions: [], + bridge_directory: "/fixture", + discovery_status: "no_live_bridge_found", + bridge_directory_security: "private_verified", + }), + confirmCommandCompletion: async () => + readFails + ? { ok: false, error: descriptor } + : { ok: true, value: terminal }, + execute, + }); + const dispatched = await service.execute(cutterInput); + if (dispatched.ok) throw new Error("Expected pending completion"); + const retried = await service.retryCleanup(); + if (retried.ok) throw new Error("Expected receipt-read failure"); + expect(retried.error.cleanupResources).toEqual( + expect.arrayContaining([ + "descriptor-fixture", + `cutter-session:${cutterInput.session_id}`, + ]), + ); + expect(retried.error.partialObservation).toBe( + dispatched.error.partialObservation, + ); + expect(retried.error.diagnostics).toMatchObject({ + cleanup_error: { + details: { + diagnostics: { + reason: "controlled receipt descriptor close failure", + }, + }, + }, + }); + if (order === "descriptor-first") { + descriptor.resolveCleanup(); + expect(retried.error.cleanupResources).toEqual([ + `cutter-session:${cutterInput.session_id}`, + ]); + readFails = false; + expect(await service.retryCleanup()).toEqual({ + ok: false, + error: dispatched.error, + }); + expect(retried.error.cleanupIncomplete).toBe(true); + terminal = true; + expect(await service.retryCleanup()).toEqual({ ok: true, value: null }); + } else { + readFails = false; + terminal = true; + expect(await service.retryCleanup()).toEqual({ ok: true, value: null }); + expect(dispatched.error.cleanupIncomplete).toBe(false); + expect(retried.error.cleanupIncomplete).toBe(true); + expect(retried.error.cleanupResources).toEqual(["descriptor-fixture"]); + descriptor.resolveCleanup(); + } + expect(retried.error.cleanupIncomplete).toBe(false); + expect(retried.error.cleanupResources).toEqual([]); + expect(execute).toHaveBeenCalledTimes(1); + await service.close(); + }, +); + it("retains uncertain command completion as Evidence without encouraging a retry", async () => { const service = new CutterBridgeService({ listSessions: async () => ({