mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
fix(process): name the Swift compile deadline (#6071)
Name REA_SWIFTC_COMPILE_TIMEOUT_MS, its effective milliseconds, the 1..2147483647 range, and the omitted 120000 ms default when a Darwin ownership-helper compile times out. A change applies after REA restarts, on the next ownership-helper compile. Producer SIGXCPU and SIGXFSZ recovery says REA retains the tighter inherited limit. Agent-visible fixed caps in this theme say this release has no runtime override. Numbers are unchanged. Refs #5910 Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
26 files changed
+321
-95
No files matched your search
@@ -491,6 +491,77 @@ it.each([false, true])(
|
||||
},
|
||||
);
|
||||
|
||||
it("names the fixed producer command deadline and says it has no override", async () => {
|
||||
const resources = new ArtifactAnalysisResources(
|
||||
"fixture",
|
||||
"inspect_packet_fields",
|
||||
readStableArtifact,
|
||||
readLinuxResourceLimits,
|
||||
{ message: "busy", details: {} },
|
||||
);
|
||||
const failure = new OwnedCommandFailure("timeout", "command timed out", null);
|
||||
try {
|
||||
const error = resources.commandFailure(
|
||||
failure,
|
||||
{ command_timeout_ms: 30_000, output_bytes: 1024 },
|
||||
undefined,
|
||||
);
|
||||
const projected = projectAnalysisError(error);
|
||||
expect(projected.message).toContain("30000 ms");
|
||||
expect(projected.remediation.action).toContain(
|
||||
"this release has no runtime override",
|
||||
);
|
||||
expect(projected.remediation.action).not.toContain(
|
||||
"REA_SWIFTC_COMPILE_TIMEOUT_MS",
|
||||
);
|
||||
} finally {
|
||||
await resources.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("says REA retains the tighter inherited limit when a producer signal fires", async () => {
|
||||
const resources = new ArtifactAnalysisResources(
|
||||
"fixture",
|
||||
"inspect_packet_fields",
|
||||
readStableArtifact,
|
||||
readLinuxResourceLimits,
|
||||
{ message: "busy", details: {} },
|
||||
);
|
||||
const empty = { text: "", bytes: 0, observedBytes: 0 };
|
||||
const failure = new OwnedCommandFailure("process", "producer signal", {
|
||||
stdout: empty,
|
||||
stderr: empty,
|
||||
exitCode: null,
|
||||
signal: "SIGXCPU",
|
||||
});
|
||||
try {
|
||||
const error = resources.commandFailure(
|
||||
failure,
|
||||
{ command_timeout_ms: 30_000, output_bytes: 4_194_304 },
|
||||
{
|
||||
address_space_bytes: 1_073_741_824,
|
||||
file_bytes: 67_108_864,
|
||||
cpu_seconds: 30,
|
||||
},
|
||||
);
|
||||
const projected = projectAnalysisError(error);
|
||||
expect(projected.remediation.action).toContain(
|
||||
"REA retains the tighter inherited limit",
|
||||
);
|
||||
expect(projected.remediation.action).toContain("1073741824 bytes");
|
||||
expect(projected.remediation.action).toContain("67108864 bytes");
|
||||
expect(projected.remediation.action).toContain("30 CPU seconds");
|
||||
expect(projected.remediation.action).toContain(
|
||||
"This release has no runtime override",
|
||||
);
|
||||
expect(projected.details).toMatchObject({
|
||||
reported_limits: { cpu_seconds: 30 },
|
||||
});
|
||||
} finally {
|
||||
await resources.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("treats a fixed command-output cap as a non-retryable capture allowance", async () => {
|
||||
const resources = new ArtifactAnalysisResources(
|
||||
"fixture",
|
||||
@@ -519,7 +590,10 @@ it("treats a fixed command-output cap as a non-retryable capture allowance", asy
|
||||
const projected = projectAnalysisError(error);
|
||||
expect(projected.retryable).toBe(false);
|
||||
expect(projected.remediation.action).toContain(
|
||||
"The retained command-output allowance",
|
||||
"The retained command-output allowance of 1024 bytes",
|
||||
);
|
||||
expect(projected.remediation.action).toContain(
|
||||
"this release has no runtime override",
|
||||
);
|
||||
expect(projected.remediation.action).not.toMatch(
|
||||
/Review the reported worker memory limits/u,
|
||||
|
||||
@@ -352,11 +352,11 @@ export class ArtifactAnalysisResources {
|
||||
cause.cancellation === undefined ? cause : cause.cancellation.reason,
|
||||
});
|
||||
if (cause.reason === "timeout")
|
||||
return new AnalysisTimeoutError(
|
||||
operation,
|
||||
limits.command_timeout_ms,
|
||||
detail,
|
||||
);
|
||||
return new AnalysisTimeoutError(operation, limits.command_timeout_ms, {
|
||||
...detail,
|
||||
userMessage: `The owned producer command reached its fixed ${String(limits.command_timeout_ms)} ms deadline. This is not a client-side timeout; extending only the MCP client's deadline does not change this budget.`,
|
||||
remediationAction: `REA's fixed producer command deadline is ${String(limits.command_timeout_ms)} ms and this release has no runtime override. Inspect any partial output and cleanup before retrying. Repeating the unchanged request reaches the same deadline.`,
|
||||
});
|
||||
if (cause.reason === "output-limit")
|
||||
return new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
@@ -366,7 +366,7 @@ export class ArtifactAnalysisResources {
|
||||
{
|
||||
...detail,
|
||||
...fixedAllowanceOptions(
|
||||
"The retained command-output allowance",
|
||||
`The retained command-output allowance of ${String(limits.output_bytes)} bytes (this release has no runtime override)`,
|
||||
"capture",
|
||||
),
|
||||
},
|
||||
@@ -375,14 +375,23 @@ export class ArtifactAnalysisResources {
|
||||
cause.reason === "process" &&
|
||||
(cause.snapshot?.signal === "SIGXFSZ" ||
|
||||
cause.snapshot?.signal === "SIGXCPU")
|
||||
)
|
||||
) {
|
||||
const signal = cause.snapshot.signal;
|
||||
const reported =
|
||||
effectiveLimits === undefined
|
||||
? "The effective inherited limit was not reported."
|
||||
: `The effective inherited limits are ${String(effectiveLimits.address_space_bytes)} bytes of address space, ${String(effectiveLimits.file_bytes)} bytes per file, and ${String(effectiveLimits.cpu_seconds)} CPU seconds.`;
|
||||
return new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
cause.snapshot.signal === "SIGXFSZ" ? "file-size" : "cpu",
|
||||
`Producer terminated with ${cause.snapshot.signal}; the signal's exact cause is not independently established.`,
|
||||
signal === "SIGXFSZ" ? "file-size" : "cpu",
|
||||
`Producer terminated with ${signal}; the signal's exact cause is not independently established.`,
|
||||
effectiveLimits === undefined ? null : { ...effectiveLimits },
|
||||
detail,
|
||||
{
|
||||
...detail,
|
||||
remediationAction: `${reported} REA retains the tighter inherited limit. This release has no runtime override for that ceiling. A smaller artifact may finish inside the retained limit. Repeating the unchanged request fails the same way.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
return new AnalysisOutputError(operation, cause.message, detail);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveSwiftcCompileTimeoutMs } from "./DarwinProcessRunTokenReader.js";
|
||||
import {
|
||||
resolveSwiftcCompileTimeoutMs,
|
||||
swiftcCompileTimeoutRecovery,
|
||||
} from "./DarwinProcessRunTokenReader.js";
|
||||
import { MAX_PROVIDER_STARTUP_TIMEOUT_MS } from "../config/providerStartupTimeout.js";
|
||||
|
||||
describe("resolveSwiftcCompileTimeoutMs", () => {
|
||||
it("keeps the historical default when unset", () => {
|
||||
@@ -31,4 +35,14 @@ describe("resolveSwiftcCompileTimeoutMs", () => {
|
||||
}),
|
||||
).toBe(120_000);
|
||||
});
|
||||
|
||||
it("names the launch-environment deadline, range, and restart on a timeout", () => {
|
||||
const recovery = swiftcCompileTimeoutRecovery(300_000);
|
||||
expect(recovery).toContain("REA_SWIFTC_COMPILE_TIMEOUT_MS");
|
||||
expect(recovery).toContain("300000 ms");
|
||||
expect(recovery).toContain(`1..${String(MAX_PROVIDER_STARTUP_TIMEOUT_MS)}`);
|
||||
expect(recovery).toContain("120000 ms");
|
||||
expect(recovery).toContain("restart REA");
|
||||
expect(recovery).toContain("next ownership-helper compile");
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
|
||||
import {
|
||||
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
||||
MAX_PROVIDER_STARTUP_TIMEOUT_MS,
|
||||
providerStartupTimeoutSchema,
|
||||
} from "../config/providerStartupTimeout.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
@@ -105,7 +106,9 @@ export const SWIFTC_INTERRUPT = "SIGINT";
|
||||
* Swift SDK cache on the supported Intel macOS VM (about 88s), so compilation
|
||||
* stays bounded independently of short provider version probes. Invalid ambient
|
||||
* values fall back to the default; `parseConfig` still rejects them at CLI/MCP
|
||||
* admission.
|
||||
* admission. The resolved value is captured when the reader is created.
|
||||
* Production creates that reader from the launch environment at process start,
|
||||
* so a change applies after REA restarts, on the next ownership-helper compile.
|
||||
*/
|
||||
export const resolveSwiftcCompileTimeoutMs = (
|
||||
options: {
|
||||
@@ -121,6 +124,10 @@ export const resolveSwiftcCompileTimeoutMs = (
|
||||
return parsed.success ? parsed.data : DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS;
|
||||
};
|
||||
|
||||
/** Name the existing swiftc deadline when a helper compile times out. */
|
||||
export const swiftcCompileTimeoutRecovery = (timeoutMs: number): string =>
|
||||
`The effective REA_SWIFTC_COMPILE_TIMEOUT_MS value is ${String(timeoutMs)} ms (integer range 1..${String(MAX_PROVIDER_STARTUP_TIMEOUT_MS)}; omitted default ${String(DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS)} ms). Set it in the environment that starts REA and restart REA; the next ownership-helper compile uses the new value.`;
|
||||
|
||||
/** Bound on compiler diagnostics embedded in the inspection failure text. */
|
||||
const STDERR_EXCERPT_LIMIT = 512;
|
||||
|
||||
@@ -272,8 +279,11 @@ const compilerOutcomeDescription = (
|
||||
if (outcome === undefined) return `compiler result: ${String(fallbackCode)}`;
|
||||
if (outcome.outcome === "exited")
|
||||
return `compiler result: exit code ${String(outcome.code)}`;
|
||||
if (outcome.outcome === "timed_out")
|
||||
return `compiler result: timed out after ${String(outcome.timeoutMs)} ms`;
|
||||
if (outcome.outcome === "timed_out") {
|
||||
if (outcome.timeoutMs === undefined)
|
||||
return "compiler result: timed out. The effective REA_SWIFTC_COMPILE_TIMEOUT_MS value was not reported.";
|
||||
return `compiler result: timed out after ${String(outcome.timeoutMs)} ms. ${swiftcCompileTimeoutRecovery(outcome.timeoutMs)}`;
|
||||
}
|
||||
if (outcome.outcome === "signaled")
|
||||
return `compiler result: stopped by signal ${outcome.signal ?? "an unknown signal"}`;
|
||||
return `compiler result: ${String(outcome.code)}`;
|
||||
@@ -302,8 +312,13 @@ const compilerInspectionError = (
|
||||
stderr.truncated ||
|
||||
outcome?.stderrTruncated === true ||
|
||||
outcome?.outputTruncated === true;
|
||||
const truncationNote = !truncated
|
||||
? ""
|
||||
: stderr.truncated
|
||||
? `… [diagnostics truncated at ${String(STDERR_EXCERPT_LIMIT)} bytes; this release has no runtime override]`
|
||||
: "… [diagnostics truncated; this release has no runtime override]";
|
||||
const suffix = diagnostics
|
||||
? `: ${diagnostics}${truncated ? "… [diagnostics truncated]" : ""}`
|
||||
? `: ${diagnostics}${truncationNote}`
|
||||
: outcome === undefined
|
||||
? `: ${cause instanceof Error ? cause.message : String(cause)}`
|
||||
: ": no compiler diagnostics were captured";
|
||||
|
||||
@@ -328,6 +328,13 @@ const mergeUnverifiedObservations = (
|
||||
return { ...result, unverified: merged };
|
||||
};
|
||||
|
||||
/**
|
||||
* Times one group may be revalidated in a single cleanup. The second pass
|
||||
* covers a group that reappears after it was already processed. Another
|
||||
* appearance stays unresolved instead of reporting success.
|
||||
*/
|
||||
const OWNED_GROUP_REVALIDATION_LIMIT = 2;
|
||||
|
||||
/** Token-validate every rooted POSIX process group before signaling any. */
|
||||
export const cleanupOwnedProcessGroup = async (
|
||||
ownership: OwnedProcessGroup,
|
||||
@@ -360,7 +367,7 @@ export const cleanupOwnedProcessGroup = async (
|
||||
return mergeUnverifiedObservations(
|
||||
{
|
||||
cleaned: false,
|
||||
reason: `owned process group ${String(processGroupId)} reappeared after revalidation`,
|
||||
reason: `owned process group ${String(processGroupId)} reappeared after ${String(OWNED_GROUP_REVALIDATION_LIMIT)} revalidation passes. This release has no runtime override.`,
|
||||
},
|
||||
plan.unverified,
|
||||
);
|
||||
@@ -380,7 +387,7 @@ export const cleanupOwnedProcessGroup = async (
|
||||
return mergeUnverifiedObservations(
|
||||
{
|
||||
cleaned: false,
|
||||
reason: `owned process group ${String(groupId)} reappeared after revalidation`,
|
||||
reason: `owned process group ${String(groupId)} reappeared after ${String(OWNED_GROUP_REVALIDATION_LIMIT)} revalidation passes. This release has no runtime override.`,
|
||||
},
|
||||
plan.unverified,
|
||||
);
|
||||
@@ -631,13 +638,6 @@ const isExitRefreshCandidate = (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Times one group may be revalidated in a single cleanup. The second pass
|
||||
* covers a group that reappears after it was already processed. Another
|
||||
* appearance stays unresolved instead of reporting success.
|
||||
*/
|
||||
const OWNED_GROUP_REVALIDATION_LIMIT = 2;
|
||||
|
||||
/**
|
||||
* Token-check rows that exist only in the post-exit table. A prior PID, group,
|
||||
* or ancestry link is handled by the caller; these rows have none of those.
|
||||
@@ -716,8 +716,7 @@ const classifyUnlinkedExitRows = async (
|
||||
if (confirmations > EXIT_REFRESH_CONFIRMATION_LIMIT)
|
||||
return {
|
||||
kind: "uncertain",
|
||||
reason:
|
||||
"process ownership could not be revalidated: exit refresh confirmation limit exceeded",
|
||||
reason: `process ownership could not be revalidated: the exit refresh confirmation limit of ${String(EXIT_REFRESH_CONFIRMATION_LIMIT)} reads was exceeded. This release has no runtime override.`,
|
||||
};
|
||||
// The confirmation read is its own table. A PID classified on the previous
|
||||
// table, including a readable foreign token, does not identify the process
|
||||
|
||||
@@ -592,7 +592,7 @@ describe("identityless exit refresh keeps later confirmation rows", () => {
|
||||
).resolves.toMatchObject({
|
||||
cleaned: false,
|
||||
reason:
|
||||
"process ownership could not be revalidated: exit refresh confirmation limit exceeded",
|
||||
"process ownership could not be revalidated: the exit refresh confirmation limit of 8 reads was exceeded. This release has no runtime override.",
|
||||
});
|
||||
expect(signalGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -875,7 +875,8 @@ describe("identityless cleanup revalidates a group that reappears", () => {
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
cleaned: false,
|
||||
reason: "owned process group 101 reappeared after revalidation",
|
||||
reason:
|
||||
"owned process group 101 reappeared after 2 revalidation passes. This release has no runtime override.",
|
||||
});
|
||||
expect(signalGroup.mock.calls).toEqual([[100, "SIGKILL"]]);
|
||||
});
|
||||
|
||||
+33
-23
@@ -14,7 +14,37 @@ import { err, ok, type Result } from "../domain/result.js";
|
||||
const MATCHING_DEADLINE_MS = 5_000;
|
||||
const WORKER_HEAP_MIB = 128;
|
||||
/** Shared capability text for the supervised ECMAScript matching boundary. */
|
||||
export const REGEX_MATCHING_LIMITATION = `Regex mode uses ECMAScript Unicode semantics in a cancellable worker with a ${String(MATCHING_DEADLINE_MS / 1000)}-second matching deadline; matching failure leaves the service available.`;
|
||||
export const REGEX_MATCHING_LIMITATION = `Regex mode uses ECMAScript Unicode semantics in a cancellable worker with a ${String(MATCHING_DEADLINE_MS / 1000)}-second matching deadline and a ${String(WORKER_HEAP_MIB)} MiB worker heap. This release has no runtime override for either cap; matching failure leaves the service available.`;
|
||||
|
||||
/** Fixed regex deadline. The worker heap and this deadline are separate caps. */
|
||||
export const regexMatchingDeadlineError = (
|
||||
operation: string,
|
||||
): AnalysisResourceConstraintError =>
|
||||
new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"cpu",
|
||||
`Regex matching exceeded its supervised deadline of ${String(MATCHING_DEADLINE_MS)} ms. This release has no runtime override. The service remains available.`,
|
||||
{ matching_deadline_ms: MATCHING_DEADLINE_MS },
|
||||
{
|
||||
remediationAction: `The regex matching deadline is ${String(MATCHING_DEADLINE_MS)} ms and this release has no runtime override. Simplify the ECMAScript Unicode regex or use literal mode, then retry in the same session.`,
|
||||
},
|
||||
);
|
||||
|
||||
/** Fixed regex worker heap. Inherited limits and host memory do not raise it. */
|
||||
export const regexWorkerHeapError = (
|
||||
operation: string,
|
||||
cause: Error,
|
||||
): AnalysisResourceConstraintError =>
|
||||
new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"memory",
|
||||
`Regex worker exhausted its ${String(WORKER_HEAP_MIB)} MiB heap budget. This release has no runtime override.`,
|
||||
{ worker_heap_mib: WORKER_HEAP_MIB },
|
||||
{
|
||||
cause,
|
||||
remediationAction: `The regex worker heap is ${String(WORKER_HEAP_MIB)} MiB and this release has no runtime override. Inherited limits and additional host memory do not raise it. Simplify the pattern or use literal mode.`,
|
||||
},
|
||||
);
|
||||
const replySchema = z.discriminatedUnion("status", [
|
||||
z.strictObject({
|
||||
status: z.literal("matched"),
|
||||
@@ -84,21 +114,7 @@ export const matchRegexValues = async (input: {
|
||||
const onAbort = () =>
|
||||
finish(err(new AnalysisCancelledError(input.operation)));
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
finish(
|
||||
err(
|
||||
new AnalysisResourceConstraintError(
|
||||
input.operation,
|
||||
"cpu",
|
||||
"Regex matching exceeded its supervised deadline; the service remains available.",
|
||||
{ matching_deadline_ms: MATCHING_DEADLINE_MS },
|
||||
{
|
||||
remediationAction:
|
||||
"Simplify the ECMAScript Unicode regex or use literal mode, then retry in the same session.",
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
() => finish(err(regexMatchingDeadlineError(input.operation))),
|
||||
MATCHING_DEADLINE_MS,
|
||||
);
|
||||
input.signal?.addEventListener("abort", onAbort, { once: true });
|
||||
@@ -144,13 +160,7 @@ export const matchRegexValues = async (input: {
|
||||
finish(
|
||||
err(
|
||||
cause.code === "ERR_WORKER_OUT_OF_MEMORY"
|
||||
? new AnalysisResourceConstraintError(
|
||||
input.operation,
|
||||
"memory",
|
||||
"Regex worker exhausted its heap budget",
|
||||
{ worker_heap_mib: WORKER_HEAP_MIB },
|
||||
{ cause },
|
||||
)
|
||||
? regexWorkerHeapError(input.operation, cause)
|
||||
: new AnalysisOutputError(
|
||||
input.operation,
|
||||
`Regex worker failed: ${cause.message}`,
|
||||
|
||||
@@ -233,7 +233,7 @@ export const stopOwnedDebuggerProcess = async (
|
||||
if (detached === undefined)
|
||||
return {
|
||||
status: "incomplete",
|
||||
reason: `Owned ${debuggerName} inferior detach was not verified within ${DETACH_TIMEOUT_MS} ms.`,
|
||||
reason: `Owned ${debuggerName} inferior detach was not verified within ${String(DETACH_TIMEOUT_MS)} ms. This release has no runtime override.`,
|
||||
};
|
||||
if (!isVerifiedDetachResult(detached)) {
|
||||
detachOutcomes.set(session, "unconfirmed");
|
||||
|
||||
Reference in new issue
Block a user