From 1a9aa1296b71866072dca20fd4a3275912bd7d89 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:46:30 +0800 Subject: [PATCH] fix(kuna): recover overflow when project can answer (#8747) * fix(kuna): recover overflow when project can answer * fix(kuna): provide actionable CLI overflow recovery * test(kuna): cover shared overflow cleanup * test(kuna): cover cancellation during overflow cleanup * fix(kuna): make overflow recovery usable after handoff --- src/kuna/KunaSessionClient.ts | 35 ++++- .../providers/kuna/kunaProvider.test.ts | 136 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 04baa3eaa6..fe10751660 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -1207,7 +1207,40 @@ export class KunaSessionClient implements AnalysisClient { options.signal, ); if (outcome === EXPORT_READY) continue; - if (!outcome.ok) return outcome; + if (!outcome.ok) { + const failure = outcome.error; + if ( + failure instanceof AnalysisResourceConstraintError && + failure.resource === "memory" && + failure.reportedLimits?.output_limit_bytes === + KUNA_FUNCTION_TEXT_BYTES && + !failure.cleanupIncomplete && + this.#exportFailure === undefined && + this.#project !== undefined + ) + return err( + new AnalysisResourceConstraintError( + operation, + failure.resource, + failure.reason, + failure.reportedLimits, + { + cause: failure, + capturedOutput: failure.capturedOutput, + diagnostics: failure.diagnostics, + partialObservation: failure.partialObservation, + retryable: true, + remediationAction: + "The standalone output exceeds REA's fixed buffer. Run rea function for this target with Kuna to build the project, then retry this function. If the project cannot supply pseudocode, consider another provider.", + continuingCallerGuidance: { + remediationAction: + "The standalone output exceeds REA's fixed buffer. If the original session is open, wait for its project export, for example by requesting procedure_assembly. Otherwise reopen the target and wait for its project. Then retry the function. If the project cannot supply pseudocode, consider another provider.", + }, + }, + ), + ); + return outcome; + } const exportFailure = await this.#settledExportFailure(); return ok({ result: outcome.value, diff --git a/tests/boundary/providers/kuna/kunaProvider.test.ts b/tests/boundary/providers/kuna/kunaProvider.test.ts index 4bcc83bfd6..499e0c3a46 100644 --- a/tests/boundary/providers/kuna/kunaProvider.test.ts +++ b/tests/boundary/providers/kuna/kunaProvider.test.ts @@ -552,6 +552,90 @@ it("rejects oversized cached text before buffering while retaining its full expo } }); +it("can recover a standalone output overflow from a completed project", async () => { + const { open, kuna } = await session("rea-kuna-overflow-project-", { + delayMs: 3000, + decompileDelayMs: 200, + decompilePadding: KUNA_FUNCTION_TEXT_BYTES + 1, + }); + const client = open(); + const failures = await Promise.all( + ["main", "0x1000"].map((procedure) => + client.execute("procedure_pseudo_code", { procedure }), + ), + ); + for (const failure of failures) { + expect(failure.ok).toBe(false); + if (failure.ok) continue; + expect(projectAnalysisError(failure.error)).toMatchObject({ + code: "resource_constraint", + retryable: true, + remediation: { action: expect.stringContaining("reopen the target") }, + details: { + reported_limits: { output_limit_bytes: KUNA_FUNCTION_TEXT_BYTES }, + }, + }); + const invocation = projectAnalysisError(failure.error, "invocation"); + expect(invocation.remediation.action).toContain("rea function"); + expect(invocation.remediation.action).not.toContain("procedure_assembly"); + } + expect( + (await fakeKunaCalls(kuna)).filter(({ argv }) => argv[0] === "decompile"), + ).toHaveLength(1); + await run(client, "procedure_assembly", { procedure: "main" }); + const later = await run(client, "procedure_pseudo_code", { + procedure: "main", + }); + expect(later.rawResult).toMatchObject({ source: { kind: "project-export" } }); + await client.close(); + + const failed = await session("rea-kuna-overflow-failed-project-", { + project: "exit-failure", + decompilePadding: KUNA_FUNCTION_TEXT_BYTES + 1, + }); + const failedClient = failed.open(); + expect( + (await failedClient.execute("procedure_assembly", { procedure: "main" })) + .ok, + ).toBe(false); + const overflow = await failedClient.execute("procedure_pseudo_code", { + procedure: "main", + }); + expect(overflow.ok).toBe(false); + if (!overflow.ok) + expect(projectAnalysisError(overflow.error)).toMatchObject({ + code: "resource_constraint", + retryable: false, + remediation: { action: expect.stringContaining("Use another provider") }, + }); + await failedClient.close(); + + const unproven = unprovenCleanupLauncher("decompile"); + const retained = await session( + "rea-kuna-overflow-retained-cleanup-", + { project: "hang", decompilePadding: KUNA_FUNCTION_TEXT_BYTES + 1 }, + {}, + unproven.launcher, + ); + const retainedClient = retained.open(); + try { + const result = await retainedClient.execute("procedure_pseudo_code", { + procedure: "main", + }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.cleanupIncomplete).toBe(true); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "cleanup_incomplete", + retryable: false, + }); + } + } finally { + unproven.release(); + expect(await retainedClient.close()).toEqual({ ok: true, value: null }); + } +}); + it("keeps answering pseudocode one function at a time when the export fails", async () => { const { open } = await session("rea-kuna-export-failed-", { project: "exit-failure", @@ -585,6 +669,58 @@ it("keeps answering pseudocode one function at a time when the export fails", as await client.close(); }); +it("keeps an overflow as the primary failure when cancellation arrives during final cleanup", async () => { + let enteredCleanup!: () => void; + let releaseCleanup!: () => void; + const entered = new Promise((resolve) => (enteredCleanup = resolve)); + const released = new Promise((resolve) => (releaseCleanup = resolve)); + const launcher: KunaProcessLauncher = async (options) => { + const spawned = await spawnOwnedProviderProcess(options); + if (options.arguments[0] !== "decompile") return spawned; + return { + ...spawned, + cleanup: async () => { + const result = await cleanupOwnedProcessGroup(spawned.ownership); + enteredCleanup(); + await released; + return result; + }, + }; + }; + const fixture = await session( + "rea-kuna-overflow-cancel-finalization-", + { project: "hang", decompilePadding: KUNA_FUNCTION_TEXT_BYTES + 1 }, + {}, + launcher, + ); + const client = fixture.open(); + const caller = new AbortController(); + const pending = client.execute( + "procedure_pseudo_code", + { procedure: "main" }, + { signal: caller.signal }, + ); + try { + await entered; + caller.abort("cancel during overflow cleanup"); + releaseCleanup(); + const result = await pending; + expect(result.ok).toBe(false); + if (!result.ok) + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "resource_constraint", + details: { + reported_limits: { output_limit_bytes: KUNA_FUNCTION_TEXT_BYTES }, + }, + }); + } finally { + releaseCleanup(); + caller.abort(); + await pending; + expect(await client.close()).toEqual({ ok: true, value: null }); + } +}); + it("stops waiting for the export when the caller cancels, without abandoning it", async () => { const { open } = await session("rea-kuna-wait-cancel-", { delayMs: 1500 }); const client = open();