mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
+17
-8
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user