From 88202cbf67d46d37b488fe435b3d1b485b3e34f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Nov 2026 09:57:09 +0000 Subject: [PATCH] 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. --- .../javascript/JavaScriptSourceAnalysis.ts | 7 ++++++- src/browser/history/CaptureFailures.test.ts | 3 ++- src/browser/history/CaptureFailures.ts | 9 +++++++++ src/config/captureDecoderHeap.ts | 20 ++++++++++++++++--- src/config/regexMatchingDeadline.test.ts | 15 ++++++++++---- src/config/regexMatchingDeadline.ts | 19 +++++++++++++++--- src/config/regexWorkerHeapRequest.test.ts | 3 ++- src/config/regexWorkerHeapRequest.ts | 19 +++++++++++++++--- src/process/RegexMatcher.test.ts | 3 ++- src/process/RegexMatcher.ts | 11 +++++++--- 10 files changed, 89 insertions(+), 20 deletions(-) diff --git a/src/artifacts/javascript/JavaScriptSourceAnalysis.ts b/src/artifacts/javascript/JavaScriptSourceAnalysis.ts index 69bcffde7d..2f890c866a 100644 --- a/src/artifacts/javascript/JavaScriptSourceAnalysis.ts +++ b/src/artifacts/javascript/JavaScriptSourceAnalysis.ts @@ -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, diff --git a/src/browser/history/CaptureFailures.test.ts b/src/browser/history/CaptureFailures.test.ts index b0923ea67c..4d85134462 100644 --- a/src/browser/history/CaptureFailures.test.ts +++ b/src/browser/history/CaptureFailures.test.ts @@ -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: { diff --git a/src/browser/history/CaptureFailures.ts b/src/browser/history/CaptureFailures.ts index 32e1166647..73b895418d 100644 --- a/src/browser/history/CaptureFailures.ts +++ b/src/browser/history/CaptureFailures.ts @@ -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: { diff --git a/src/config/captureDecoderHeap.ts b/src/config/captureDecoderHeap.ts index c95715a537..fff78278f2 100644 --- a/src/config/captureDecoderHeap.ts +++ b/src/config/captureDecoderHeap.ts @@ -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(" "); diff --git a/src/config/regexMatchingDeadline.test.ts b/src/config/regexMatchingDeadline.test.ts index b61e7eae4e..4ed7918d46 100644 --- a/src/config/regexMatchingDeadline.test.ts +++ b/src/config/regexMatchingDeadline.test.ts @@ -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, ); diff --git a/src/config/regexMatchingDeadline.ts b/src/config/regexMatchingDeadline.ts index 2345a09e82..73ac67dda2 100644 --- a/src/config/regexMatchingDeadline.ts +++ b/src/config/regexMatchingDeadline.ts @@ -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(" "); diff --git a/src/config/regexWorkerHeapRequest.test.ts b/src/config/regexWorkerHeapRequest.test.ts index 3bcf8e6ed6..d24583eac4 100644 --- a/src/config/regexWorkerHeapRequest.test.ts +++ b/src/config/regexWorkerHeapRequest.test.ts @@ -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( diff --git a/src/config/regexWorkerHeapRequest.ts b/src/config/regexWorkerHeapRequest.ts index 051e8226ca..8103970901 100644 --- a/src/config/regexWorkerHeapRequest.ts +++ b/src/config/regexWorkerHeapRequest.ts @@ -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(" "); diff --git a/src/process/RegexMatcher.test.ts b/src/process/RegexMatcher.test.ts index 15db616194..305e3565da 100644 --- a/src/process/RegexMatcher.test.ts +++ b/src/process/RegexMatcher.test.ts @@ -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(); }); diff --git a/src/process/RegexMatcher.ts b/src/process/RegexMatcher.ts index dccae9340c..930ff83104 100644 --- a/src/process/RegexMatcher.ts +++ b/src/process/RegexMatcher.ts @@ -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.`, + }, }, ); /**