diff --git a/docs/kuna-provider.md b/docs/kuna-provider.md index 4acc290fad..d1adc2ca6e 100644 --- a/docs/kuna-provider.md +++ b/docs/kuna-provider.md @@ -83,7 +83,7 @@ The first text or assembly request in a session looks for this target's export i - Meanwhile, pseudocode comes from one `kuna decompile --addr` run. Name and address lookups, and listings, use the inventory the export takes with `kuna functions --json` before it decompiles. The session runs `kuna functions` itself, under `REA_KUNA_TIMEOUT_MS`, only when no export is building (an inventory-only request does not start one) or the export ends without an inventory. - Requests for the same function share one run; it stops when its last waiting request is cancelled. Each run analyzes the whole image again in its own process, so one REA process runs at most as many at once as the host has cores beyond `REA_KUNA_JOBS` (at least one). Further requests wait in order, and a waiting request is answered from the export if it finishes first. A run whose stop REA cannot prove keeps its slot until `close_binary` proves it stopped. - The session keeps each completed single-function text, up to 16 MiB in all with the least recently used dropped first, and answers repeats from it until the export can. After a failed export that lasts for the session. - - Assembly and data names wait for the export; progress notifications report the wait every ten seconds. A session does not rebuild a failed export: requests that need it fail with `retryable: false`, the export's failure, its captured Kuna output and what to change before reopening the target. A setting such as `REA_KUNA_PROJECT_TIMEOUT_MS` or `REA_KUNA_JOBS` is read when REA starts, so changing it also needs a restart. Pseudocode keeps using single-function runs. + - Assembly and data names wait for the export; progress notifications report the wait every ten seconds. A session does not rebuild a failed export: requests that need it fail with `retryable: false`, the export's failure, its captured Kuna output and what to change before reopening the target. REA does not expose per-function records from a failed project export, even if the retained Kuna diagnostic refers to them. Single-function fallback Evidence reports the failed state and this limitation. If cleanup remains incomplete, its error takes precedence and `close_binary` retries the retained resources. A setting such as `REA_KUNA_PROJECT_TIMEOUT_MS` or `REA_KUNA_JOBS` is read when REA starts, so changing it also needs a restart. Pseudocode keeps using single-function runs. - A single-function answer says so in its limitations. It lacks the export's whole-program prototypes, so a later request can return different text for the same function. Pseudocode, assembly, `analyze_function` and data-name answers record where their text came from in `raw_result.source`: `single-function`, or `project-export` with the cache directory. `procedure_pseudo_code` and `procedure_assembly` answers from the export also name the file, byte span and the file's SHA-256. Combined `analyze_function` answers retain both facet records in `raw_result.source.files`, each with its file, digest and byte span. Both texts come from one export; losing that project retries the complete report. Listings and procedure lookups carry no `raw_result`; omitted [Mach-O data entries](#mach-o-data-entries) are reported in their limitations. Agents can open or search the export's `.c` and `.asm` there directly when that is cheaper than calling a tool per function. Text read that way is target-derived, like any decompiler output, but it bypasses the untrusted-content marker that REA's tool results carry. diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 8424be370c..df5b0fe4d4 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -239,6 +239,9 @@ const descriptorCleanupError = ( return error; }; +const FAILED_PROJECT_RECORDS_GUIDANCE = + "REA does not expose per-function records from a failed project export."; + /** * How a request that needs the export reports the export's failure. A session * never rebuilds a failed export, so repeating the request cannot succeed; the @@ -276,11 +279,12 @@ const exportUnavailable = ( diagnostics: { export_error: failure._tag, export_code: exported.code, + failed_project_records: "not_exposed", ...(exported.details === undefined ? {} : { export_details: exported.details }), }, - userMessage: `Kuna's project export failed, so ${operation} is unavailable in this session: ${reason.replace(/\.?$/u, ".")} ${next}`, + userMessage: `Kuna's project export failed, so ${operation} is unavailable in this session: ${reason.replace(/\.?$/u, ".")} ${FAILED_PROJECT_RECORDS_GUIDANCE} Inspect any captured diagnostics. ${next}`, }, ); }; @@ -714,7 +718,11 @@ export class KunaSessionClient implements AnalysisClient { }; if (this.#project === undefined) return { state: "not-started" }; if (this.#exportFailure !== undefined) - return { state: "failed", reason: this.#exportFailure }; + return { + state: "failed", + reason: this.#exportFailure, + failure_records: "not_exposed", + }; return { state: "building", phase: this.#status?.phase ?? null, @@ -1272,6 +1280,7 @@ export class KunaSessionClient implements AnalysisClient { ? [] : [ `The project export failed, so later requests also use single-function runs: ${exportFailure}`, + FAILED_PROJECT_RECORDS_GUIDANCE, ]), ], }); diff --git a/tests/boundary/providers/kuna/kunaProvider.test.ts b/tests/boundary/providers/kuna/kunaProvider.test.ts index 32b20e9004..a051bc6947 100644 --- a/tests/boundary/providers/kuna/kunaProvider.test.ts +++ b/tests/boundary/providers/kuna/kunaProvider.test.ts @@ -655,6 +655,7 @@ it("keeps answering pseudocode one function at a time when the export fails", as userMessage: expect.stringContaining( "not in recognized object file format", ), + diagnostics: { failed_project_records: "not_exposed" }, }, }); const pseudocode = await run(client, "procedure_pseudo_code", { @@ -662,8 +663,14 @@ it("keeps answering pseudocode one function at a time when the export fails", as }); expect(pseudocode.rawResult).toMatchObject({ source: { kind: "single-function" }, + project_export: { state: "failed", failure_records: "not_exposed" }, }); - expect(pseudocode.limitations.at(-1)).toContain("The project export failed"); + expect(pseudocode.limitations).toEqual( + expect.arrayContaining([ + expect.stringContaining("The project export failed"), + "REA does not expose per-function records from a failed project export.", + ]), + ); expect(pseudocode.limitations.join(" ")).not.toContain("still building"); expect(pseudocode.limitations.join(" ")).toContain( "larger project-export spans remain in the local project export", diff --git a/tests/boundary/providers/kuna/kunaPublicationCleanup.test.ts b/tests/boundary/providers/kuna/kunaPublicationCleanup.test.ts index ffbaae6f48..ee8e64709f 100644 --- a/tests/boundary/providers/kuna/kunaPublicationCleanup.test.ts +++ b/tests/boundary/providers/kuna/kunaPublicationCleanup.test.ts @@ -182,3 +182,37 @@ it("preserves the original failed export when staging cleanup also fails", async await cache.close(); } }); + +it("retains failed export cleanup ownership while reporting fallback record availability", async () => { + const { cacheRoot, open } = await session("rea-kuna-failed-export-cleanup-", { + project: "exit-failure", + }); + const client = open(); + fault.root = cacheRoot; + fault.blocked = true; + try { + const first = await client.execute("procedure_assembly", { + procedure: "main", + }); + expect(first).toMatchObject({ + ok: false, + error: { cleanupIncomplete: true }, + }); + if (first.ok) throw new Error("Expected retained cleanup failure"); + expect(first.error.cleanupResources.length).toBeGreaterThan(0); + const pseudo = await run(client, "procedure_pseudo_code", { + procedure: "main", + }); + expect(pseudo.rawResult).toMatchObject({ + source: { kind: "single-function" }, + project_export: { state: "failed", failure_records: "not_exposed" }, + }); + expect((await client.close()).ok).toBe(false); + fault.blocked = false; + expect(await client.close()).toEqual({ ok: true, value: null }); + expect(first.error.cleanupIncomplete).toBe(false); + } finally { + fault.blocked = false; + await client.close(); + } +});