fix(javascript): name heap and semantic ceilings (#6022)

* fix(javascript): name heap and semantic ceilings

Refs #5910. Heap exhaustion names max_heap_mb with its effective value,
range, and next-analysis worker restart. Semantic ceiling coverage names
the four max_semantic_* arguments. Wakaru and transfer ceilings say this
release has no runtime override.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(javascript): distinguish adjustable ceilings from fixed limits

The effective transfer file ceiling and the parent result budget follow
the CLI/MCP process heap, so recovery names NODE_OPTIONS instead of
denying an override. A provenance report over 8 MiB keeps the fixed
allowance guidance, and the Wakaru deadline says an unchanged retry may
hit it again.

Refs #5910

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(javascript): keep Bun OOM and checkpoint heap controls distinct

Bun heap-exhaustion exits no longer claim the ignored max_heap_mb V8
old-space limit. Checkpoint decode names the parent heap and Node
NODE_OPTIONS, the same control as the parent result budget.

Refs #5910

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(javascript): name semantic ceilings without claiming both were reached

Truncated semantic retention names the total and per-file controls as
effective ceilings. Callers only know that retention was truncated, so
the limitation no longer says both ceilings were reached.

Refs #5910

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(javascript): name only the semantic ceiling that stopped retention

Semantic ceiling limitations claimed both the application total and the
per-file control whenever either one truncated the graph. Record which
offered budget stopped node and unknown retention, and name only those
controls. Coverage limits still list both effective values.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(javascript): require a value in the NODE_OPTIONS heap example

A bare --max-old-space-size is an illegal V8 flag and prevents Node from
starting. Parent-heap and transfer-ceiling recovery now share
--max-old-space-size=<MiB>, and the Kuna heap remediation uses the same
complete form.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(javascript): limit the old-space placeholder to NODE_OPTIONS recovery

The review covers the CLI/MCP NODE_OPTIONS example and the transfer-ceiling
copy. The Kuna heap-headroom sentence stays unchanged.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
morluto
2026-10-22 02:44:14 +08:00
committed by GitHub
parent 01788bac86
commit a0214e0e0e
22 changed files with 1166 additions and 87 deletions
@@ -47,11 +47,21 @@ import {
import {
readJavaScriptAnalysisTransfer,
JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES,
javaScriptForwardedTransferRemediation,
javaScriptTransferRetentionBytes,
} from "./JavaScriptAnalysisTransfer.js";
import {
JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION,
JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION,
JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM,
JAVASCRIPT_WORKER_HEAP_MIB_MINIMUM,
javaScriptWorkerHeapExhaustionRecovery,
javaScriptWorkerHeapRemediation,
} from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import { javaScriptWorkerStaticAnalysisSchema } from "./JavaScriptAnalysisWorkerSchemas.js";
import {
JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS as DEFAULTS,
javaScriptCheckpointCeilingRemediation,
javaScriptWorkerMessageSchema,
javaScriptWorkerRequestSchema,
javaScriptWorkerAdmissionSchema,
@@ -159,8 +169,8 @@ export class JavaScriptAnalysisWorker {
});
if (
!Number.isSafeInteger(limits.heapMb) ||
limits.heapMb < 128 ||
limits.heapMb > 16_384 ||
limits.heapMb < JAVASCRIPT_WORKER_HEAP_MIB_MINIMUM ||
limits.heapMb > JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM ||
!Number.isSafeInteger(limits.timeoutMs) ||
limits.timeoutMs <= 0 ||
limits.timeoutMs > 2_147_483_647
@@ -303,8 +313,11 @@ export class JavaScriptAnalysisWorker {
throw new AnalysisResourceConstraintError(
OPERATION,
"transport",
`Source identity for ${file.path} exceeds the worker request byte budget`,
`Source identity for ${file.path} exceeds the ${String(DEFAULTS.protocolBytes)}-byte worker request ceiling. This release has no runtime override.`,
{ worker_request_bytes: DEFAULTS.protocolBytes },
{
remediationAction: `This release has no runtime override for the ${String(DEFAULTS.protocolBytes)}-byte JavaScript worker request ceiling. Raising max_heap_mb does not raise it. Repeating the unchanged request fails the same way.`,
},
);
const stdin = supervisor.launch.process.stdin;
if (stdin === undefined || stdin === null)
@@ -422,8 +435,7 @@ export class JavaScriptAnalysisWorker {
module_retention_estimate_bytes: moduleEstimate,
},
{
remediationAction:
"Use the retained static facts or analyze fewer source files. This limit belongs to the parent result budget; increasing the worker heap alone does not increase it.",
remediationAction: `Use the retained static facts or analyze fewer source files. ${JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION}`,
},
),
});
@@ -462,8 +474,7 @@ export class JavaScriptAnalysisWorker {
graph_records: descriptor.graph_records,
},
{
remediationAction:
"Use the retained static and module facts or analyze fewer source files. This limit belongs to the parent result budget; increasing the worker heap alone does not increase it.",
remediationAction: `Use the retained static and module facts or analyze fewer source files. ${JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION}`,
},
),
});
@@ -685,6 +696,9 @@ export class JavaScriptAnalysisWorker {
"memory",
"The parent's remaining heap cannot safely decode the completed JavaScript static checkpoint",
{ ...this.#limits(), checkpoint_bytes: bytes },
{
remediationAction: JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION,
},
);
const text = await readFile(path, "utf8");
if (
@@ -720,8 +734,7 @@ export class JavaScriptAnalysisWorker {
application_projection_required_bytes: expandedBytes,
},
{
remediationAction:
"Read the complete static facts on the file node or analyze fewer source files. Expanded application relationships exceed the parent result budget; increasing the worker heap alone does not increase it.",
remediationAction: `Read the complete static facts on the file node or analyze fewer source files. Expanded application relationships exceed the parent result budget. ${JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION}`,
},
);
return facts;
@@ -753,9 +766,12 @@ export class JavaScriptAnalysisWorker {
return new AnalysisResourceConstraintError(
OPERATION,
"memory",
`Owned JavaScript worker reached its ${this.#processMemoryLimitBytes === null ? "" : `${String(this.#processMemoryLimitBytes)}-byte `}process memory limit while analyzing ${path} during ${this.#phase}`,
`Owned JavaScript worker reached its ${this.#processMemoryLimitBytes === null ? "" : `${String(this.#processMemoryLimitBytes)}-byte `}process memory limit while analyzing ${path} during ${this.#phase}. That limit is max_heap_mb ${String(this.limits.heapMb)} MiB plus a 512 MiB Bun runtime reserve`,
this.#limits(),
{ capturedOutput: this.#captured() },
{
capturedOutput: this.#captured(),
remediationAction: javaScriptWorkerHeapRemediation(this.limits.heapMb),
},
);
}
#remainingResultBudget(): number {
@@ -990,14 +1006,24 @@ export class JavaScriptAnalysisWorker {
path: string,
failure: Extract<WorkerPrimaryFailure, { kind: "exit" }>,
): AnalysisError {
if (failure.heapExhaustion)
if (failure.heapExhaustion) {
const recovery = javaScriptWorkerHeapExhaustionRecovery({
heapFlagsEnforced: WORKER_HEAP_FLAGS_ENFORCED,
heapMb: this.limits.heapMb,
path,
phase: this.#phase,
});
return new AnalysisResourceConstraintError(
OPERATION,
"memory",
`Owned JavaScript worker exhausted its heap while analyzing ${path} during ${this.#phase}`,
recovery.reason,
this.#limits(),
{ capturedOutput: this.#captured() },
{
capturedOutput: this.#captured(),
remediationAction: recovery.remediationAction,
},
);
}
return new ProviderAdapterError(PROVIDER, OPERATION, {
capturedOutput: this.#captured(),
diagnostics: {
@@ -1031,13 +1057,33 @@ export class JavaScriptAnalysisWorker {
response: Extract<JavaScriptWorkerMessage, { kind: "failure" }>,
phase: string,
): AnalysisError {
if (response.reason === "resource")
if (response.reason === "resource") {
const reported = response.limits;
const limits = { ...this.#limits(), ...reported };
const transferRemediation =
javaScriptForwardedTransferRemediation(reported);
const checkpointBytes = reported?.checkpoint_limit_bytes;
const checkpointRemediation =
typeof checkpointBytes === "number"
? javaScriptCheckpointCeilingRemediation(checkpointBytes)
: undefined;
const parentBudget =
reported?.semantic_result_budget_bytes !== undefined ||
reported?.parent_result_budget_bytes !== undefined;
const remediationAction =
transferRemediation ??
checkpointRemediation ??
(parentBudget
? JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION
: undefined);
return new AnalysisResourceConstraintError(
OPERATION,
response.resource ?? "memory",
response.message,
{ ...this.#limits(), ...response.limits },
limits,
remediationAction === undefined ? undefined : { remediationAction },
);
}
return new AnalysisOutputError(
OPERATION,
`${path} during ${phase}: ${response.message}`,