fix(settings): keep invocation recovery on the launch seed

A one-shot CLI failure tells the caller to set the launch environment for the next REA process. update_analysis_settings stays on the continuing session. Reused JavaScript workers also keep the limit source that selected them.
This commit is contained in:
Cursor Agent authored and morluto committed 2026-11-11 19:48:33 +08:00
1 parent 073dda2a0a
commit 88202cbf67
10 files changed
+89 -20

No files matched your search

@@ -130,7 +130,12 @@ const sameLimits = (
left: JavaScriptSourceAnalysisLimits,
right: JavaScriptSourceAnalysisLimits,
): boolean => {
if (left.heapMb !== right.heapMb || left.timeoutMs !== right.timeoutMs)
if (
left.heapMb !== right.heapMb ||
left.timeoutMs !== right.timeoutMs ||
left.heapSource !== right.heapSource ||
left.timeoutSource !== right.timeoutSource
)
return false;
const leftPrecision = resolveJavaScriptSemanticPrecision(
left.semanticPrecision,
+2 -1
View File
@@ -1,6 +1,7 @@
import { expect, it } from "vitest";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
captureDecoderHeapContinuingRecovery,
captureDecoderHeapRecovery,
DEFAULT_CAPTURE_DECODER_HEAP,
} from "../../config/captureDecoderHeap.js";
@@ -74,7 +75,7 @@ it.each([
retryable: false,
remediation: {
action: expect.stringContaining(
captureDecoderHeapRecovery(DEFAULT_CAPTURE_DECODER_HEAP),
captureDecoderHeapContinuingRecovery(DEFAULT_CAPTURE_DECODER_HEAP),
),
},
details: {
+9
View File
@@ -30,6 +30,7 @@ import {
type InspectWebNetworkCaptureInput,
} from "../../domain/webNetworkCapture.js";
import {
captureDecoderHeapContinuingRecovery,
captureDecoderHeapRecovery,
DEFAULT_CAPTURE_DECODER_HEAP,
type CaptureDecoderHeap,
@@ -299,6 +300,7 @@ export const captureMemoryFailure = (
heap.oldGenerationMiB,
);
const recovery = captureDecoderHeapRecovery(heap);
const continuingRecovery = captureDecoderHeapContinuingRecovery(heap);
return new AnalysisResourceConstraintError(
OPERATION,
"memory",
@@ -341,6 +343,13 @@ export const captureMemoryFailure = (
}
: {}),
remediationAction: `Use a smaller capture exported by its producer, or another decoder with sufficient capacity for the original capture. Preserve the original for provenance; a subset does not establish complete-capture coverage.${input.format !== "mitmproxy" ? ` ${recovery}` : ""}${input.format !== "mitmproxy" && processMemoryLimitBytes !== null ? ` The Windows Job Object commit limit for this decoder is ${String(processMemoryLimitBytes)} bytes, ${String(heap.oldGenerationMiB)} MiB plus a 512 MiB Bun reserve.` : ""}`,
...(input.format === "mitmproxy"
? {}
: {
continuingCallerGuidance: {
remediationAction: `Use a smaller capture exported by its producer, or another decoder with sufficient capacity for the original capture. Preserve the original for provenance; a subset does not establish complete-capture coverage. ${continuingRecovery}${processMemoryLimitBytes !== null ? ` The Windows Job Object commit limit for this decoder is ${String(processMemoryLimitBytes)} bytes, ${String(heap.oldGenerationMiB)} MiB plus a 512 MiB Bun reserve.` : ""}`,
},
}),
...(cause instanceof OwnedCommandFailure && cause.snapshot !== null
? {
capturedOutput: {
+17 -3
View File
@@ -67,11 +67,25 @@ export const resolveCaptureDecoderHeap = (
return parsed.value;
};
/** How to read and change the effective HAR/SAZ/qlog old-generation heap. */
export const captureDecoderHeapRecovery = (heap: CaptureDecoderHeap): string =>
const captureDecoderHeapFacts = (heap: CaptureDecoderHeap): string =>
[
`The effective ${CAPTURE_DECODER_OLD_GENERATION_SETTING} value is ${String(heap.oldGenerationMiB)} MiB (source ${heap.source}; default ${String(DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB, floor ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB, maximum ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB).`,
`capture.decoder_old_generation_mib can be changed at runtime with update_analysis_settings. The launch seed is ${CAPTURE_DECODER_OLD_GENERATION_SETTING}, a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)}.`,
`On Node.js the decoder passes --max-old-space-size=${String(heap.oldGenerationMiB)} and removes inherited NODE_OPTIONS, so additional host memory and NODE_OPTIONS do not raise this heap. The ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB semi-space stays fixed.`,
"On Bun the V8 flags are ignored. Windows limits the decoder's committed memory to this value plus a 512 MiB reserve. Other Bun hosts enforce no decoder heap limit.",
].join(" ");
/** Recovery after this REA process has exited. */
export const captureDecoderHeapRecovery = (heap: CaptureDecoderHeap): string =>
[
captureDecoderHeapFacts(heap),
`Set ${CAPTURE_DECODER_OLD_GENERATION_SETTING} in the environment that starts the next REA invocation. It must be a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)}.`,
].join(" ");
/** Recovery while this session can still call update_analysis_settings. */
export const captureDecoderHeapContinuingRecovery = (
heap: CaptureDecoderHeap,
): string =>
[
captureDecoderHeapFacts(heap),
`Call update_analysis_settings with setting capture.decoder_old_generation_mib. It applies to the next decoder in this session. The launch seed remains ${CAPTURE_DECODER_OLD_GENERATION_SETTING}.`,
].join(" ");
+11 -4
View File
@@ -12,6 +12,7 @@ import {
DEFAULT_REGEX_MATCHING_DEADLINE,
parseRegexMatchingDeadline,
REGEX_MATCHING_DEADLINE_SETTING,
regexMatchingDeadlineContinuingRecovery,
regexMatchingDeadlineRecovery,
} from "./regexMatchingDeadline.js";
import { MAXIMUM_REGEX_MATCHING_DEADLINE_MS } from "../domain/analysisResourceLimits.js";
@@ -32,9 +33,9 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
const recovery = regexMatchingDeadlineRecovery(
DEFAULT_REGEX_MATCHING_DEADLINE,
);
const projected = projectAnalysisError(
regexMatchingDeadlineError("search_procedures"),
);
const error = regexMatchingDeadlineError("search_procedures");
const projected = projectAnalysisError(error);
const invocation = projectAnalysisError(error, "invocation");
expect(projected).toMatchObject({
code: "resource_constraint",
details: {
@@ -58,7 +59,13 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
},
},
});
expect(projected.remediation.action).toContain(recovery);
expect(projected.remediation.action).toContain(
regexMatchingDeadlineContinuingRecovery(DEFAULT_REGEX_MATCHING_DEADLINE),
);
expect(invocation.remediation.action).toContain(recovery);
expect(invocation.remediation.action).not.toContain(
"update_analysis_settings",
);
expect(projected.remediation.action).toContain(
REGEX_MATCHING_DEADLINE_SETTING,
);
+16 -3
View File
@@ -64,12 +64,25 @@ export const resolveRegexMatchingDeadline = (
return parsed.value;
};
/** How to read and change the effective supervised matching deadline. */
const regexMatchingDeadlineFacts = (deadline: RegexMatchingDeadline): string =>
`The effective ${REGEX_MATCHING_DEADLINE_SETTING} value is ${String(deadline.deadlineMs)} ms (source ${deadline.source}; default ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms, floor 1 ms, maximum ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms).`;
/** Recovery after this REA process has exited. The next invocation reads the launch seed. */
export const regexMatchingDeadlineRecovery = (
deadline: RegexMatchingDeadline,
): string =>
[
`The effective ${REGEX_MATCHING_DEADLINE_SETTING} value is ${String(deadline.deadlineMs)} ms (source ${deadline.source}; default ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms, floor 1 ms, maximum ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms).`,
`regex.matching_deadline_ms can be changed at runtime with update_analysis_settings. The launch seed is ${REGEX_MATCHING_DEADLINE_SETTING}, a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)}.`,
regexMatchingDeadlineFacts(deadline),
`Set ${REGEX_MATCHING_DEADLINE_SETTING} in the environment that starts the next REA invocation. It must be a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)}.`,
"The worker old-generation heap does not raise this deadline.",
].join(" ");
/** Recovery while the same REA session can still call update_analysis_settings. */
export const regexMatchingDeadlineContinuingRecovery = (
deadline: RegexMatchingDeadline,
): string =>
[
regexMatchingDeadlineFacts(deadline),
`Call update_analysis_settings with setting regex.matching_deadline_ms. It applies to the next match in this session. The launch seed remains ${REGEX_MATCHING_DEADLINE_SETTING}.`,
"The worker old-generation heap does not raise this deadline.",
].join(" ");
+2 -1
View File
@@ -12,6 +12,7 @@ import {
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
parseRegexWorkerHeapRequest,
REGEX_WORKER_HEAP_REQUEST_SETTING,
regexWorkerHeapRequestContinuingRecovery,
regexWorkerHeapRequestRecovery,
} from "./regexWorkerHeapRequest.js";
import {
@@ -32,7 +33,7 @@ it("keeps the 128 MiB request when the setting is unset", () => {
expect(parsed.value.regexWorkerHeapRequest).toEqual(
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
);
const recovery = regexWorkerHeapRequestRecovery(
const recovery = regexWorkerHeapRequestContinuingRecovery(
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
);
const projected = projectAnalysisError(
+16 -3
View File
@@ -66,12 +66,25 @@ export const resolveRegexWorkerHeapRequest = (
return parsed.value;
};
/** How to read and change the regex worker old-generation request. */
const regexWorkerHeapFacts = (request: RegexWorkerHeapRequest): string =>
`The effective ${REGEX_WORKER_HEAP_REQUEST_SETTING} value is ${String(request.heapMiB)} MiB (source ${request.source}; default ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, floor ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, maximum ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB).`;
/** Recovery after this REA process has exited. */
export const regexWorkerHeapRequestRecovery = (
request: RegexWorkerHeapRequest,
): string =>
[
`The effective ${REGEX_WORKER_HEAP_REQUEST_SETTING} value is ${String(request.heapMiB)} MiB (source ${request.source}; default ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, floor ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, maximum ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB).`,
`regex.worker_heap_request_mib can be changed at runtime with update_analysis_settings. The launch seed is ${REGEX_WORKER_HEAP_REQUEST_SETTING}, a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)}.`,
regexWorkerHeapFacts(request),
`Set ${REGEX_WORKER_HEAP_REQUEST_SETTING} in the environment that starts the next REA invocation. It must be a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)}.`,
"A positive Node --max-old-space-size, including NODE_OPTIONS, overrides this request and sets the effective heap. That flag is not limited by this setting's floor or maximum. Raising host memory alone does not raise the request.",
].join(" ");
/** Recovery while this session can still call update_analysis_settings. */
export const regexWorkerHeapRequestContinuingRecovery = (
request: RegexWorkerHeapRequest,
): string =>
[
regexWorkerHeapFacts(request),
`Call update_analysis_settings with setting regex.worker_heap_request_mib. It changes the request for the next worker in this session. The launch seed remains ${REGEX_WORKER_HEAP_REQUEST_SETTING}.`,
"A positive Node --max-old-space-size, including NODE_OPTIONS, overrides this request and sets the effective heap. That flag is not limited by this setting's floor or maximum. Raising host memory alone does not raise the request.",
].join(" ");
+2 -1
View File
@@ -5,6 +5,7 @@ import {
regexMatchingDeadlineRecovery,
} from "../config/regexMatchingDeadline.js";
import {
regexWorkerHeapRequestContinuingRecovery,
regexWorkerHeapRequestRecovery,
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
} from "../config/regexWorkerHeapRequest.js";
@@ -476,7 +477,7 @@ it("keeps a selected heap request unless a positive Node flag overrides it", ()
},
});
expect(projected.remediation.action).toContain(
regexWorkerHeapRequestRecovery(request),
regexWorkerHeapRequestContinuingRecovery(request),
);
expect(projected.details?.limit).toBeUndefined();
});
+8 -3
View File
@@ -15,12 +15,14 @@ import { err, ok, type Result } from "../domain/result.js";
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
REGEX_MATCHING_DEADLINE_SETTING,
regexMatchingDeadlineContinuingRecovery,
regexMatchingDeadlineRecovery,
type RegexMatchingDeadline,
} from "../config/regexMatchingDeadline.js";
import {
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
REGEX_WORKER_HEAP_REQUEST_SETTING,
regexWorkerHeapRequestContinuingRecovery,
regexWorkerHeapRequestRecovery,
type RegexWorkerHeapRequest,
} from "../config/regexWorkerHeapRequest.js";
@@ -404,8 +406,8 @@ const launchRegexWorkerHeap = resolveLaunchRegexWorkerHeap(
const heapBound = (
heap: RegexWorkerHeapLimit,
request: RegexWorkerHeapRequest,
recovery = regexWorkerHeapRequestRecovery(request),
): string => {
const recovery = regexWorkerHeapRequestRecovery(request);
if (heap.kind === "unknown")
return `The worker requests ${String(request.heapMiB)} MiB. The effective old-generation heap was not established from the launch flags. --max-old-space-size, --max-old-space-size-percentage, --max-heap-size, --env-file, --env-file-if-exists, --experimental-config-file, and --experimental-default-config-file can change that request. ${recovery}`;
const override = `A positive Node --max-old-space-size, including NODE_OPTIONS, overrides the ${String(request.heapMiB)} MiB worker request`;
@@ -476,7 +478,7 @@ export const regexMatchingDeadlineError = (
// A one-shot invocation has ended its session before the caller retries.
continuingCallerGuidance: {
userMessage: "The service remains available.",
remediationAction: `${regexMatchingDeadlineRecovery(deadline)} Simplify the ECMAScript Unicode regex or use literal mode, then retry in the same session.`,
remediationAction: `${regexMatchingDeadlineContinuingRecovery(deadline)} Simplify the ECMAScript Unicode regex or use literal mode, then retry in the same session.`,
},
},
);
@@ -515,7 +517,10 @@ export const regexWorkerHeapError = (
),
}
: {}),
remediationAction: `${heapBound(heap, request)} Use update_analysis_settings with setting regex.worker_heap_request_mib to change the request for the next worker; launch heap flags can override it. Restart REA after changing --max-old-space-size or ${REGEX_WORKER_HEAP_REQUEST_SETTING} in the launch environment. A launch --max-old-space-size-percentage takes precedence over that flag in either order. Simplify the pattern or use literal mode.`,
remediationAction: `${heapBound(heap, request)} Set ${REGEX_WORKER_HEAP_REQUEST_SETTING} in the environment that starts the next REA invocation. Restart REA after changing --max-old-space-size. A launch --max-old-space-size-percentage takes precedence over that flag in either order. Simplify the pattern or use literal mode.`,
continuingCallerGuidance: {
remediationAction: `${heapBound(heap, request, regexWorkerHeapRequestContinuingRecovery(request))} Call update_analysis_settings with setting regex.worker_heap_request_mib to change the request for the next worker in this session. Launch heap flags can still override it. The launch seed remains ${REGEX_WORKER_HEAP_REQUEST_SETTING}. A launch --max-old-space-size-percentage takes precedence over --max-old-space-size in either order. Simplify the pattern or use literal mode.`,
},
},
);
/**