From 9be493880c98d5701cd702069054f2e7a338019c Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Sat, 17 Oct 2026 00:38:16 -0700 Subject: [PATCH] fix(kuna): name the setting that owns a timed-out Kuna run (#4343) * fix(kuna): name the setting that owns a timed-out Kuna run A Kuna run past its deadline surfaced the generic "Analysis took too long. Try a smaller request" message. The request is already one function or one whole-program scan, and the same run takes as long again, so the advice was wrong and REA_KUNA_TIMEOUT_MS or REA_KUNA_PROJECT_TIMEOUT_MS was never mentioned. Each Kuna command now carries a KunaDeadline: the configured milliseconds and the setting that owns them, from KunaConfiguration. A timeout reports, for example, "kuna decompile did not finish within REA_KUNA_TIMEOUT_MS (120000 ms)." Its remediation says to raise that setting and restart REA, and for a single-function decompile that the session's project export can still answer. AnalysisTimeoutError now accepts a producer message and remediation, which projection uses and per-waiter scan errors keep. Refs #1957 Co-Authored-By: Claude Opus 5.5 * docs(kuna): scope the provider_timeout statement to direct Kuna runs A timed-out project export is reported through the export failure as capability_unavailable, not provider_timeout. Refs #1957 Co-Authored-By: Claude Opus 5.5 * fix(kuna): verify native xrefs under the project deadline The provider's xrefs scan runs under REA_KUNA_PROJECT_TIMEOUT_MS, but the native oracle in verify:kuna aborted at REA_KUNA_TIMEOUT_MS. A scan that fit the project deadline but not the function deadline failed verification although the provider answered it. Use the same deadline. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- docs/kuna-provider.md | 2 ++ .../lib/kuna-native-reference-verifier.mjs | 8 +++-- scripts/lib/kuna-string-verifier.mjs | 3 +- src/kuna/KunaCommand.ts | 25 +++++++++++----- src/kuna/KunaConfiguration.ts | 22 ++++++++++++++ src/kuna/KunaProjectExport.ts | 3 +- src/kuna/KunaReferenceQueries.ts | 5 ++-- src/kuna/KunaSessionClient.ts | 5 ++-- src/kuna/KunaStringQueries.ts | 3 +- src/kuna/KunaTargetCommands.ts | 5 ++-- .../kuna/kunaCommandFailures.test.ts | 2 +- .../kuna/kunaCommandLifetime.test.ts | 2 +- .../kuna/kunaHashOwnerPropagation.test.ts | 10 +++---- .../providers/kuna/kunaRunOwnership.test.ts | 2 +- .../kuna/kunaSingleFunctionRuns.test.ts | 30 +++++++++++++++++++ .../providers/kuna/kunaStrings.test.ts | 12 +++++++- 16 files changed, 110 insertions(+), 29 deletions(-) diff --git a/docs/kuna-provider.md b/docs/kuna-provider.md index d0c72e54d1..0a37b98c81 100644 --- a/docs/kuna-provider.md +++ b/docs/kuna-provider.md @@ -22,6 +22,8 @@ Select it explicitly with `--provider kuna`, MCP's `open_binary.provider_id: "ku | `REA_KUNA_TIMEOUT_MS` | 120 s | Deadline for one single-function decompile, and for the inventory run REA makes when the export ends without one. | | `REA_KUNA_JOBS` | 1 | `decompile-project --jobs`. 1 is Kuna's reference answer; more jobs finish sooner but lose callee-first prototypes, and change about 40–60% of function bodies on the measured binaries. | +A single-function decompile, inventory, string, graph or xref run that exceeds its deadline fails as `provider_timeout`, naming the setting that owns the deadline and that raising it takes a restart. A project export that exceeds `REA_KUNA_PROJECT_TIMEOUT_MS` is an export failure instead: requests that need the export report `capability_unavailable` with the export's own retry guidance. + Windows is not supported yet. Supported targets are ELF and thin Mach-O executables for x86-64, AArch64 and ARM; PE and universal Mach-O are rejected with a typed reason. ## What Kuna answers diff --git a/scripts/lib/kuna-native-reference-verifier.mjs b/scripts/lib/kuna-native-reference-verifier.mjs index fad27f31e8..df48e017c5 100644 --- a/scripts/lib/kuna-native-reference-verifier.mjs +++ b/scripts/lib/kuna-native-reference-verifier.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { realpath } from "node:fs/promises"; import { runKunaCommand } from "../../dist/kuna/KunaCommand.js"; +import { kunaProjectDeadline } from "../../dist/kuna/KunaConfiguration.js"; import { inspectKunaInstallation } from "../../dist/kuna/KunaInstallation.js"; import { KUNA_XREF_OUTPUT_BYTES } from "../../dist/kuna/KunaReferenceFiles.js"; @@ -24,14 +25,15 @@ export const nativeKunaIncomingSites = async ({ ); assert.ok(installed.ok, "Native reference oracle could not inspect Kuna"); const input = await realpath(binary); - const timeoutMs = installed.value.configuration.functionTimeoutMs; + // Match the provider's own xrefs scan, which runs under the project deadline. + const deadline = kunaProjectDeadline(installed.value.configuration); const outcome = await runKunaCommand({ installation: installed.value, arguments: ["xrefs", input, "--to", address, "--json"], workspace, operation: "verify:kuna-native-xrefs", - timeoutMs, - reportedTimeoutMs: timeoutMs, + timeoutMs: deadline.ms, + deadline, outputBytes: KUNA_XREF_OUTPUT_BYTES, signal: new AbortController().signal, retainFailedFileClose: (owner, resource) => diff --git a/scripts/lib/kuna-string-verifier.mjs b/scripts/lib/kuna-string-verifier.mjs index 60f74e7489..157cce141a 100644 --- a/scripts/lib/kuna-string-verifier.mjs +++ b/scripts/lib/kuna-string-verifier.mjs @@ -3,6 +3,7 @@ import { readdir, realpath } from "node:fs/promises"; import { join } from "node:path"; import { runKunaCommand } from "../../dist/kuna/KunaCommand.js"; +import { kunaProjectDeadline } from "../../dist/kuna/KunaConfiguration.js"; import { inspectKunaInstallation } from "../../dist/kuna/KunaInstallation.js"; const entries = (path) => @@ -46,7 +47,7 @@ export const nativeKunaStrings = async ({ workspace, operation: "verify:kuna-native-strings", timeoutMs: installed.value.configuration.projectTimeoutMs, - reportedTimeoutMs: installed.value.configuration.projectTimeoutMs, + deadline: kunaProjectDeadline(installed.value.configuration), outputBytes: 16 * 1024 * 1024, signal: new AbortController().signal, retainFailedFileClose: (owner, resource) => diff --git a/src/kuna/KunaCommand.ts b/src/kuna/KunaCommand.ts index 0f8fe356b9..b4cc5a3de8 100644 --- a/src/kuna/KunaCommand.ts +++ b/src/kuna/KunaCommand.ts @@ -33,6 +33,7 @@ import { kunaChildEnvironment, type KunaInstallation, } from "./KunaInstallation.js"; +import type { KunaDeadline } from "./KunaConfiguration.js"; import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; /** @@ -54,9 +55,10 @@ export interface KunaCommandRequest { readonly workspace: string; /** Operation named by every failure this run reports. */ readonly operation: string; + /** Time left for this run; at most the configured deadline. */ readonly timeoutMs: number; - /** The deadline callers configured, reported when the run times out. */ - readonly reportedTimeoutMs: number; + /** The configured deadline and its setting, reported when the run times out. */ + readonly deadline: KunaDeadline; /** Buffered stderr bytes, plus stdout bytes unless `stdout` consumes them. */ readonly outputBytes: number; /** Parses stdout as it arrives instead of buffering it; see `runOwnedCommand`. */ @@ -302,12 +304,19 @@ const commandFailure = ( ...details, cause: cause.cancellation?.reason, }); - else if (cause.reason === "timeout") - primary = new AnalysisTimeoutError( - operation, - request.reportedTimeoutMs, - details, - ); + else if (cause.reason === "timeout") { + const { setting, ms } = request.deadline; + primary = new AnalysisTimeoutError(operation, ms, { + ...details, + userMessage: `kuna ${subcommand} did not finish within ${setting} (${String(ms)} ms).`, + // The same run takes as long again, so only a longer deadline helps. + remediationAction: `Raise ${setting} in the environment that starts REA, then restart REA; it reads Kuna settings when it starts.${ + subcommand === "decompile" + ? " Within a session, pseudocode is also read from the project export once it finishes." + : "" + }`, + }); + } // The same input overflows the same fixed buffer; waiting cannot help. else if (cause.reason === "output-limit") primary = new AnalysisResourceConstraintError( diff --git a/src/kuna/KunaConfiguration.ts b/src/kuna/KunaConfiguration.ts index 7de37b635d..e09b524896 100644 --- a/src/kuna/KunaConfiguration.ts +++ b/src/kuna/KunaConfiguration.ts @@ -47,6 +47,28 @@ export interface KunaConfiguration { readonly jobs: number; } +/** One configured deadline and the setting that owns it. */ +export interface KunaDeadline { + readonly setting: "REA_KUNA_TIMEOUT_MS" | "REA_KUNA_PROJECT_TIMEOUT_MS"; + readonly ms: number; +} + +/** Single-function decompiles and the standalone inventory. */ +export const kunaFunctionDeadline = ( + configuration: KunaConfiguration, +): KunaDeadline => ({ + setting: "REA_KUNA_TIMEOUT_MS", + ms: configuration.functionTimeoutMs, +}); + +/** The project export and whole-program string, graph and xref scans. */ +export const kunaProjectDeadline = ( + configuration: KunaConfiguration, +): KunaDeadline => ({ + setting: "REA_KUNA_PROJECT_TIMEOUT_MS", + ms: configuration.projectTimeoutMs, +}); + /** Why the configured environment cannot select a Kuna installation. */ export interface KunaConfigurationFailure { readonly code: "not_configured" | "open_options_invalid" | "unsupported_host"; diff --git a/src/kuna/KunaProjectExport.ts b/src/kuna/KunaProjectExport.ts index 273013822a..3c40699b5b 100644 --- a/src/kuna/KunaProjectExport.ts +++ b/src/kuna/KunaProjectExport.ts @@ -24,6 +24,7 @@ import { runKunaCommand, type KunaProcessLauncher, } from "./KunaCommand.js"; +import { kunaProjectDeadline } from "./KunaConfiguration.js"; import type { KunaInstallation } from "./KunaInstallation.js"; import { KUNA_DIAGNOSTIC_BYTES } from "./KunaLimits.js"; import { @@ -214,7 +215,7 @@ export const exportKunaProject = async ( workspace: request.workspace, operation: KUNA_EXPORT_OPERATION, timeoutMs: deadline - Date.now(), - reportedTimeoutMs: configuration.projectTimeoutMs, + deadline: kunaProjectDeadline(configuration), outputBytes, signal, launcher: request.launcher, diff --git a/src/kuna/KunaReferenceQueries.ts b/src/kuna/KunaReferenceQueries.ts index 797f2fc8b2..462293b52d 100644 --- a/src/kuna/KunaReferenceQueries.ts +++ b/src/kuna/KunaReferenceQueries.ts @@ -8,6 +8,7 @@ import type { FunctionDossier } from "../domain/hopperValues.js"; import type { RelationshipCoverage } from "../domain/relationshipCoverage.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { err, ok, type Result } from "../domain/result.js"; +import { kunaProjectDeadline } from "./KunaConfiguration.js"; import type { KunaInstallation } from "./KunaInstallation.js"; import { KUNA_XREF_OUTPUT_BYTES, @@ -120,7 +121,7 @@ export class KunaReferenceQueries { "--functions", "", ], - timeoutMs: installation.configuration.projectTimeoutMs, + deadline: kunaProjectDeadline(installation.configuration), }, new KunaCallGraphReader(), producerSignal, @@ -253,7 +254,7 @@ export class KunaReferenceQueries { installation, operation: "xrefs", arguments: (input) => ["xrefs", input, "--to", address, "--json"], - timeoutMs: installation.configuration.projectTimeoutMs, + deadline: kunaProjectDeadline(installation.configuration), outputBytes: KUNA_XREF_OUTPUT_BYTES, signal: producerSignal, }); diff --git a/src/kuna/KunaSessionClient.ts b/src/kuna/KunaSessionClient.ts index 5bcae71323..5a75c9bada 100644 --- a/src/kuna/KunaSessionClient.ts +++ b/src/kuna/KunaSessionClient.ts @@ -39,6 +39,7 @@ import { kunaFileCleanupError, type KunaProcessLauncher, } from "./KunaCommand.js"; +import { kunaFunctionDeadline } from "./KunaConfiguration.js"; import { createKunaFileCloseScope, inspectKunaInstallation, @@ -1117,7 +1118,7 @@ export class KunaSessionClient implements AnalysisClient { installation, arguments: (input) => ["decompile", input, address, "--addr"], operation, - timeoutMs: installation.configuration.functionTimeoutMs, + deadline: kunaFunctionDeadline(installation.configuration), outputBytes: KUNA_FUNCTION_TEXT_BYTES, signal, releaseRun: admitted, @@ -1607,7 +1608,7 @@ export class KunaSessionClient implements AnalysisClient { installation, arguments: (input) => ["functions", input, "--json"], operation: "list_procedures", - timeoutMs: installation.configuration.functionTimeoutMs, + deadline: kunaFunctionDeadline(installation.configuration), outputBytes: KUNA_INVENTORY_OUTPUT_BYTES, signal: this.#session.signal, }); diff --git a/src/kuna/KunaStringQueries.ts b/src/kuna/KunaStringQueries.ts index 222f40fa21..5158a142a1 100644 --- a/src/kuna/KunaStringQueries.ts +++ b/src/kuna/KunaStringQueries.ts @@ -11,6 +11,7 @@ import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js" import type { JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; import { normalizeKunaAddress } from "./KunaAddress.js"; +import { kunaProjectDeadline } from "./KunaConfiguration.js"; import type { KunaInstallation } from "./KunaInstallation.js"; import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js"; import { kunaQueryFailureForCaller } from "./KunaQueryErrors.js"; @@ -148,7 +149,7 @@ export class KunaStringQueries { "any", "--no-xrefs", ], - timeoutMs: installation.configuration.projectTimeoutMs, + deadline: kunaProjectDeadline(installation.configuration), }, new KunaStringInventoryReader(), producerSignal, diff --git a/src/kuna/KunaTargetCommands.ts b/src/kuna/KunaTargetCommands.ts index cb8cdbf3a7..aecf6126cb 100644 --- a/src/kuna/KunaTargetCommands.ts +++ b/src/kuna/KunaTargetCommands.ts @@ -20,6 +20,7 @@ import { runKunaCommand, type KunaProcessLauncher, } from "./KunaCommand.js"; +import type { KunaDeadline } from "./KunaConfiguration.js"; import type { KunaInstallation } from "./KunaInstallation.js"; import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; import type { KunaRunRelease } from "./KunaRunSlots.js"; @@ -51,7 +52,7 @@ export interface KunaTargetCommandRequest { readonly outputBytes: number; /** Parses stdout as it arrives instead of buffering it in the snapshot. */ readonly stdout?: Writable; - readonly timeoutMs: number; + readonly deadline: KunaDeadline; readonly signal: AbortSignal; /** Admission remains owned until the native process is proven stopped. */ readonly releaseRun?: KunaRunRelease; @@ -119,7 +120,7 @@ export class KunaTargetCommands { ...options, arguments: request.arguments(workspace.value.input), workspace: workspace.value.root.path, - reportedTimeoutMs: request.timeoutMs, + timeoutMs: request.deadline.ms, launcher: this.lifetime.launcher, retainFailedFileClose: (owner, resource) => { const lease = new AnalysisCleanupLease( diff --git a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts index 80ec77dd89..523043649f 100644 --- a/tests/boundary/providers/kuna/kunaCommandFailures.test.ts +++ b/tests/boundary/providers/kuna/kunaCommandFailures.test.ts @@ -25,7 +25,7 @@ it("reports output beyond REA's fixed buffer as a deterministic memory bound", a installation: installed.value, operation: "list_procedures", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 100, signal: controller.signal, }); diff --git a/tests/boundary/providers/kuna/kunaCommandLifetime.test.ts b/tests/boundary/providers/kuna/kunaCommandLifetime.test.ts index 88f0b67619..71cd213c41 100644 --- a/tests/boundary/providers/kuna/kunaCommandLifetime.test.ts +++ b/tests/boundary/providers/kuna/kunaCommandLifetime.test.ts @@ -24,7 +24,7 @@ const installation = await inspectKunaInstallation( assert.equal(installation.ok, true); const controller = new AbortController(); const commands = new KunaTargetCommands(fixture.target, { signal: controller.signal }); -const request = () => commands.run({ installation: installation.value, operation: "retention-probe", arguments: input => ["functions", input, "--json"], timeoutMs: 2000, outputBytes: 16384, signal: controller.signal }); +const request = () => commands.run({ installation: installation.value, operation: "retention-probe", arguments: input => ["functions", input, "--json"], deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 2000 }, outputBytes: 16384, signal: controller.signal }); try { const capture = async () => { const result = await request(); diff --git a/tests/boundary/providers/kuna/kunaHashOwnerPropagation.test.ts b/tests/boundary/providers/kuna/kunaHashOwnerPropagation.test.ts index 566243749f..b09c1e15dd 100644 --- a/tests/boundary/providers/kuna/kunaHashOwnerPropagation.test.ts +++ b/tests/boundary/providers/kuna/kunaHashOwnerPropagation.test.ts @@ -192,7 +192,7 @@ it.skipIf(process.platform === "win32")( installation: installed.value, operation: "list_procedures", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 1024 * 1024, signal: controller.signal, }); @@ -274,7 +274,7 @@ it.skipIf(process.platform === "win32")( installation: installed.value, operation: "list_procedures", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 1024 * 1024, signal: controller.signal, }); @@ -321,7 +321,7 @@ it.skipIf(process.platform === "win32")( installation: installed.value, operation: "list_procedures", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 1024 * 1024, signal: controller.signal, }); @@ -372,7 +372,7 @@ it.skipIf(process.platform === "win32")( installation: installed.value, operation: "list_procedures", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 1024 * 1024, signal: controller.signal, releaseRun: () => { @@ -430,7 +430,7 @@ it.skipIf(process.platform === "win32")( installation: installed.value, operation: "list_procedures", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 1024 * 1024, signal: controller.signal, }); diff --git a/tests/boundary/providers/kuna/kunaRunOwnership.test.ts b/tests/boundary/providers/kuna/kunaRunOwnership.test.ts index 63b6503823..62d354fc47 100644 --- a/tests/boundary/providers/kuna/kunaRunOwnership.test.ts +++ b/tests/boundary/providers/kuna/kunaRunOwnership.test.ts @@ -104,7 +104,7 @@ it("keeps an admission lease until retained native cleanup is proven", async () installation: installed.value, operation: "lease-probe", arguments: (input) => ["functions", input, "--json"], - timeoutMs: 10_000, + deadline: { setting: "REA_KUNA_TIMEOUT_MS", ms: 10_000 }, outputBytes: 16384, signal: caller.signal, releaseRun: lease!, diff --git a/tests/boundary/providers/kuna/kunaSingleFunctionRuns.test.ts b/tests/boundary/providers/kuna/kunaSingleFunctionRuns.test.ts index 030f525ede..8c838f7d1a 100644 --- a/tests/boundary/providers/kuna/kunaSingleFunctionRuns.test.ts +++ b/tests/boundary/providers/kuna/kunaSingleFunctionRuns.test.ts @@ -1,5 +1,7 @@ import { expect, it as test } from "vitest"; +import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; + import { fakeKunaCalls, fakeKunaExits, @@ -282,3 +284,31 @@ it("drops the least recently used text beyond the session's 16 MiB budget", asyn expect(await client.close()).toEqual({ ok: true, value: null }); } }); + +it("names the setting that owns a single-function deadline", async () => { + const { open } = await session( + "rea-kuna-function-deadline-", + { project: "hang", decompile: "hang" }, + { REA_KUNA_TIMEOUT_MS: "300" }, + ); + const client = open(); + try { + const result = await client.execute("procedure_pseudo_code", { + procedure: "main", + }); + if (result.ok) throw new Error("the hanging run answered"); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "provider_timeout", + message: + "kuna decompile did not finish within REA_KUNA_TIMEOUT_MS (300 ms).", + remediation: { + action: expect.stringMatching( + /^Raise REA_KUNA_TIMEOUT_MS .* restart REA.*project export/u, + ), + }, + details: { timeout_ms: 300 }, + }); + } finally { + expect(await client.close()).toEqual({ ok: true, value: null }); + } +}); diff --git a/tests/boundary/providers/kuna/kunaStrings.test.ts b/tests/boundary/providers/kuna/kunaStrings.test.ts index a5430dc760..e045843304 100644 --- a/tests/boundary/providers/kuna/kunaStrings.test.ts +++ b/tests/boundary/providers/kuna/kunaStrings.test.ts @@ -417,8 +417,18 @@ it.each([ exit_code: 7, stderr: expect.stringContaining("fixture string scan failed"), }); - if (mode === "hang") + if (mode === "hang") { expect(result.error).toMatchObject({ timeoutMs: 500 }); + // Each waiter keeps the producer's setting and recovery. + expect(projectAnalysisError(result.error)).toMatchObject({ + message: + "kuna strings did not finish within REA_KUNA_PROJECT_TIMEOUT_MS (500 ms).", + remediation: { + action: + "Raise REA_KUNA_PROJECT_TIMEOUT_MS in the environment that starts REA, then restart REA; it reads Kuna settings when it starts.", + }, + }); + } if (mode === "oversize") { expect(projectAnalysisError(result.error)).toMatchObject({ code: "resource_constraint",