@cleocode/core 2026.10.2 → 2026.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/http.d.ts +2 -0
- package/dist/cloud/http.d.ts.map +1 -1
- package/dist/cloud/http.js +3 -0
- package/dist/cloud/http.js.map +1 -1
- package/dist/cloud/manifest-check.d.ts.map +1 -1
- package/dist/cloud/manifest-check.js +13 -0
- package/dist/cloud/manifest-check.js.map +1 -1
- package/dist/cloud/nexus-enrol.d.ts +9 -0
- package/dist/cloud/nexus-enrol.d.ts.map +1 -1
- package/dist/cloud/nexus-enrol.js +45 -2
- package/dist/cloud/nexus-enrol.js.map +1 -1
- package/dist/cloud/nexus-first-run.d.ts +142 -0
- package/dist/cloud/nexus-first-run.d.ts.map +1 -0
- package/dist/cloud/nexus-first-run.js +352 -0
- package/dist/cloud/nexus-first-run.js.map +1 -0
- package/dist/cloud/nexus-project-names.d.ts +173 -0
- package/dist/cloud/nexus-project-names.d.ts.map +1 -0
- package/dist/cloud/nexus-project-names.js +314 -0
- package/dist/cloud/nexus-project-names.js.map +1 -0
- package/dist/cloud/nexus-vault-keys.d.ts +35 -3
- package/dist/cloud/nexus-vault-keys.d.ts.map +1 -1
- package/dist/cloud/nexus-vault-keys.js +212 -22
- package/dist/cloud/nexus-vault-keys.js.map +1 -1
- package/dist/cloud/nexus-vault.d.ts.map +1 -1
- package/dist/cloud/nexus-vault.js +14 -2
- package/dist/cloud/nexus-vault.js.map +1 -1
- package/dist/dispatch/contracts/derive-output-contract.d.ts +1 -1
- package/dist/dispatch/contracts/derive-output-contract.d.ts.map +1 -1
- package/dist/dispatch/contracts/derive-output-contract.js +2 -1
- package/dist/dispatch/contracts/derive-output-contract.js.map +1 -1
- package/dist/dispatch/contracts/output-contracts.d.ts +2 -1
- package/dist/dispatch/contracts/output-contracts.d.ts.map +1 -1
- package/dist/dispatch/contracts/output-contracts.js +1 -1
- package/dist/dispatch/contracts/output-contracts.js.map +1 -1
- package/dist/dispatch/mvi-projection.d.ts +1 -1
- package/dist/dispatch/mvi-projection.d.ts.map +1 -1
- package/dist/dispatch/mvi-projection.js +1 -1
- package/dist/dispatch/mvi-projection.js.map +1 -1
- package/dist/docs/export-document.js +24462 -24572
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/db-substrate.d.ts +6 -9
- package/dist/doctor/db-substrate.d.ts.map +1 -1
- package/dist/doctor/db-substrate.js +10 -14
- package/dist/doctor/db-substrate.js.map +1 -1
- package/dist/doctor/migrations.d.ts +18 -0
- package/dist/doctor/migrations.d.ts.map +1 -1
- package/dist/doctor/migrations.js +54 -2
- package/dist/doctor/migrations.js.map +1 -1
- package/dist/error-catalog.js +1 -1
- package/dist/error-catalog.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +21 -5
- package/dist/init.js.map +1 -1
- package/dist/llm/conversation.d.ts.map +1 -1
- package/dist/llm/conversation.js +15 -4
- package/dist/llm/conversation.js.map +1 -1
- package/dist/llm/plugin-facade.js +277 -376
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/llm/transports/bedrock.d.ts +1 -0
- package/dist/llm/transports/bedrock.d.ts.map +1 -1
- package/dist/llm/transports/bedrock.js +30 -12
- package/dist/llm/transports/bedrock.js.map +1 -1
- package/dist/memory/decision-contradiction.d.ts.map +1 -1
- package/dist/memory/decision-contradiction.js +10 -5
- package/dist/memory/decision-contradiction.js.map +1 -1
- package/dist/memory/dialectic-evaluator.d.ts.map +1 -1
- package/dist/memory/dialectic-evaluator.js +4 -1
- package/dist/memory/dialectic-evaluator.js.map +1 -1
- package/dist/memory/transcript-extractor.d.ts.map +1 -1
- package/dist/memory/transcript-extractor.js +3 -2
- package/dist/memory/transcript-extractor.js.map +1 -1
- package/dist/metrics/token-service.d.ts +16 -0
- package/dist/metrics/token-service.d.ts.map +1 -1
- package/dist/metrics/token-service.js +18 -0
- package/dist/metrics/token-service.js.map +1 -1
- package/dist/nexus/api-contracts.d.ts.map +1 -1
- package/dist/nexus/api-contracts.js +27 -11
- package/dist/nexus/api-contracts.js.map +1 -1
- package/dist/nexus/api-extractors/http-extractor.d.ts +2 -2
- package/dist/nexus/api-extractors/http-extractor.d.ts.map +1 -1
- package/dist/nexus/api-extractors/http-extractor.js +3 -3
- package/dist/nexus/api-extractors/http-extractor.js.map +1 -1
- package/dist/nexus/clusters.js +1 -1
- package/dist/nexus/clusters.js.map +1 -1
- package/dist/nexus/diff.d.ts +1 -1
- package/dist/nexus/diff.d.ts.map +1 -1
- package/dist/nexus/diff.js +3 -2
- package/dist/nexus/diff.js.map +1 -1
- package/dist/nexus/flows.js +1 -1
- package/dist/nexus/flows.js.map +1 -1
- package/dist/nexus/identity.d.ts +8 -0
- package/dist/nexus/identity.d.ts.map +1 -1
- package/dist/nexus/identity.js +17 -1
- package/dist/nexus/identity.js.map +1 -1
- package/dist/nexus/index.d.ts +1 -1
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/index.js +2 -2
- package/dist/nexus/index.js.map +1 -1
- package/dist/nexus/nexus-bridge.d.ts.map +1 -1
- package/dist/nexus/nexus-bridge.js +5 -5
- package/dist/nexus/nexus-bridge.js.map +1 -1
- package/dist/nexus/registry.d.ts +22 -0
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +76 -1
- package/dist/nexus/registry.js.map +1 -1
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/project-scope.js +1 -1
- package/dist/project-scope.js.map +1 -1
- package/dist/project-tombstone.js +1 -1
- package/dist/project-tombstone.js.map +1 -1
- package/dist/release/open.d.ts +3 -6
- package/dist/release/open.d.ts.map +1 -1
- package/dist/release/open.js +59 -23
- package/dist/release/open.js.map +1 -1
- package/dist/release/preflight-skip.d.ts +69 -10
- package/dist/release/preflight-skip.d.ts.map +1 -1
- package/dist/release/preflight-skip.js +195 -47
- package/dist/release/preflight-skip.js.map +1 -1
- package/dist/render/colors.js +1 -19
- package/dist/render/colors.js.map +1 -1
- package/dist/render/tasks/colors.js +1 -1
- package/dist/render/tasks/colors.js.map +1 -1
- package/dist/resources/governor.d.ts +12 -0
- package/dist/resources/governor.d.ts.map +1 -1
- package/dist/resources/governor.js +5 -1
- package/dist/resources/governor.js.map +1 -1
- package/dist/resources/heavy-command-hook-delivery.d.ts +144 -0
- package/dist/resources/heavy-command-hook-delivery.d.ts.map +1 -0
- package/dist/resources/heavy-command-hook-delivery.js +204 -0
- package/dist/resources/heavy-command-hook-delivery.js.map +1 -0
- package/dist/resources/heavy-command.d.ts +69 -0
- package/dist/resources/heavy-command.d.ts.map +1 -1
- package/dist/resources/heavy-command.js +479 -7
- package/dist/resources/heavy-command.js.map +1 -1
- package/dist/resources/run-class.d.ts.map +1 -1
- package/dist/resources/run-class.js +1 -0
- package/dist/resources/run-class.js.map +1 -1
- package/dist/routing/build-command-groups.d.ts +1 -1
- package/dist/routing/build-command-groups.d.ts.map +1 -1
- package/dist/routing/build-command-groups.js +4 -1
- package/dist/routing/build-command-groups.js.map +1 -1
- package/dist/scaffold/ensure-skills.d.ts.map +1 -1
- package/dist/scaffold/ensure-skills.js +58 -2
- package/dist/scaffold/ensure-skills.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +277 -376
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/briefing.js +11 -0
- package/dist/sessions/briefing.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +6 -1
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts +22 -0
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +97 -25
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/portable-bundle.d.ts +7 -0
- package/dist/store/portable-bundle.d.ts.map +1 -1
- package/dist/store/portable-bundle.js +10 -1
- package/dist/store/portable-bundle.js.map +1 -1
- package/dist/store/table-classification.d.ts +27 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +32 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/vault-manifest.d.ts +5 -1
- package/dist/store/vault-manifest.d.ts.map +1 -1
- package/dist/store/vault-manifest.js +9 -2
- package/dist/store/vault-manifest.js.map +1 -1
- package/dist/system/health.d.ts +13 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +96 -1
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +20 -7
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +39 -13
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-template.d.ts +93 -0
- package/dist/tasks/affected-template.d.ts.map +1 -0
- package/dist/tasks/affected-template.js +198 -0
- package/dist/tasks/affected-template.js.map +1 -0
- package/dist/tasks/command-line.d.ts +4 -1
- package/dist/tasks/command-line.d.ts.map +1 -1
- package/dist/tasks/command-line.js +7 -2
- package/dist/tasks/command-line.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +12 -2
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/duplicate-detector.d.ts.map +1 -1
- package/dist/tasks/duplicate-detector.js +11 -5
- package/dist/tasks/duplicate-detector.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +185 -11
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/heavy-tool-env.d.ts +385 -18
- package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-env.js +719 -40
- package/dist/tasks/heavy-tool-env.js.map +1 -1
- package/dist/tasks/index.d.ts +2 -2
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/tool-cache-env.d.ts +20 -13
- package/dist/tasks/tool-cache-env.d.ts.map +1 -1
- package/dist/tasks/tool-cache-env.js +27 -48
- package/dist/tasks/tool-cache-env.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts +8 -0
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +19 -6
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-semaphore.d.ts +66 -23
- package/dist/tasks/tool-semaphore.d.ts.map +1 -1
- package/dist/tasks/tool-semaphore.js +96 -34
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +37 -4
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +17 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +83 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/package.json +13 -13
- package/schemas/project-context.schema.json +5 -1
|
@@ -28,18 +28,49 @@
|
|
|
28
28
|
* |---|---|---|
|
|
29
29
|
* | `NODE_OPTIONS=--max-old-space-size=N` | Heap ceiling for EVERY node process in the tree, inherited | node docs; appended, never clobbered |
|
|
30
30
|
* | `VITEST_MAX_WORKERS=N` | `if (process.env.VITEST_MAX_WORKERS) resolved.maxWorkers = parseInt(...)` — overrides the resolved config, so it binds projects that set their own | read out of vitest 4.1.4 `dist/chunks/coverage.*.js` |
|
|
31
|
-
* | `npm_config_workspace_concurrency=N` | Bounds `pnpm -r` fan-out across workspace packages | `npm_config_workspace_concurrency=1 pnpm config get workspace-concurrency` → `1` |
|
|
31
|
+
* | `npm_config_workspace_concurrency=N` / `pnpm_config_workspace_concurrency=N` | Bounds `pnpm -r` fan-out across workspace packages (pnpm 10 reads the first, pnpm 11+ the second) | `npm_config_workspace_concurrency=1 pnpm config get workspace-concurrency` → `1` on pnpm 10.30; `pnpm_config_…` → `1` on pnpm 12.6 (T13122) |
|
|
32
32
|
*
|
|
33
33
|
* Together these bound the product the semaphore could not see:
|
|
34
34
|
* `packages in flight × workers per run × heap per worker`.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* The worker levers apply ONLY to `test` / `build`, the tools that fork.
|
|
37
|
+
* `typecheck` and `lint` are single processes, but not cheap ones: one
|
|
38
|
+
* TypeScript program on a large monorepo holds 2–5 GB (a live `tsc --noEmit`
|
|
39
|
+
* held 4.7 GB on 2026-10-03), so since T13123 they get the heap ceiling and the
|
|
40
|
+
* workspace-concurrency bound too ({@link isMemoryBoundTool}), and no worker
|
|
41
|
+
* variables, which nothing they run reads.
|
|
38
42
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
43
|
+
* ## An inherited value can tighten the plan, never loosen it (T13122)
|
|
44
|
+
*
|
|
45
|
+
* The overlay is merged over the CALLER's environment: whatever the shell
|
|
46
|
+
* profile, the agent harness or a parent process exported. Until T13122 an
|
|
47
|
+
* inherited value always won, on the theory that "a deliberate setting beats our
|
|
48
|
+
* default". A shell profile is not a per-project decision: on 2026-10-03 a
|
|
49
|
+
* `~/.zprofile` export of `NODE_OPTIONS=--max-old-space-size=8192` reached a
|
|
50
|
+
* `cleo verify --evidence tool:test`, and the run got 6 workers × 8 GiB = all of
|
|
51
|
+
* a 48 GiB Mac, because the worker count was sized for a 4 GiB heap it was not
|
|
52
|
+
* getting.
|
|
53
|
+
*
|
|
54
|
+
* So the run has a heap budget — the default worker count times the default heap
|
|
55
|
+
* ({@link heavyRunBudgetMb}) — and the overlay is planned against it
|
|
56
|
+
* ({@link planHeavyToolEnv}):
|
|
57
|
+
*
|
|
58
|
+
* - an inherited `NODE_OPTIONS` heap that fits the budget is kept, and the
|
|
59
|
+
* worker count shrinks so `workers × heap` still fits; one above the budget
|
|
60
|
+
* is clamped to it;
|
|
61
|
+
* - an inherited worker count or workspace concurrency at or below the plan
|
|
62
|
+
* is kept; one above it is clamped;
|
|
63
|
+
* - only an explicit CLEO override asks for more: `CLEO_HEAVY_HEAP_MB`,
|
|
64
|
+
* `CLEO_HEAVY_WORKERS`, `CLEO_HEAVY_WORKSPACE_CONCURRENCY`.
|
|
65
|
+
*
|
|
66
|
+
* A project that wants more for its own suite still sets it in its own script
|
|
67
|
+
* (`"test": "NODE_OPTIONS=… vitest run"`) or config: that runs below the
|
|
68
|
+
* overlay and is untouched by it. The plan, and every value it clamped, is
|
|
69
|
+
* reported by `cleo verify` and `cleo run`.
|
|
41
70
|
*
|
|
42
71
|
* @task T12096
|
|
72
|
+
* @task T13122
|
|
73
|
+
* @task T13123
|
|
43
74
|
*/
|
|
44
75
|
import { totalmem } from 'node:os';
|
|
45
76
|
/**
|
|
@@ -64,6 +95,87 @@ export const MIN_HEAVY_WORKERS = 1;
|
|
|
64
95
|
* memory-bound box does not finish at all.
|
|
65
96
|
*/
|
|
66
97
|
export const WORKSPACE_CONCURRENCY = 1;
|
|
98
|
+
/**
|
|
99
|
+
* The default heap never drops below this, in MiB, however small the machine —
|
|
100
|
+
* one worker must be able to run a real suite.
|
|
101
|
+
*
|
|
102
|
+
* @task T13122
|
|
103
|
+
*/
|
|
104
|
+
export const MIN_HEAVY_HEAP_MB = 1024;
|
|
105
|
+
/**
|
|
106
|
+
* Fraction of total RAM the DEFAULT heap may reach. {@link HEAVY_TOOL_HEAP_MB}
|
|
107
|
+
* on an 8 GiB laptop is half the machine; on a 4 GiB box it would be all of it,
|
|
108
|
+
* which is no ceiling at all. Machines of 8 GiB and up are unaffected.
|
|
109
|
+
*
|
|
110
|
+
* @task T13122
|
|
111
|
+
*/
|
|
112
|
+
export const HEAVY_HEAP_RAM_FRACTION = 0.5;
|
|
113
|
+
/**
|
|
114
|
+
* Explicit heap ceiling for heavy tools, in MiB. The only way to ask for a heap
|
|
115
|
+
* above the run's budget: unlike an inherited `NODE_OPTIONS`, it is never
|
|
116
|
+
* clamped.
|
|
117
|
+
*
|
|
118
|
+
* @task T13122
|
|
119
|
+
*/
|
|
120
|
+
export const HEAVY_HEAP_ENV = 'CLEO_HEAVY_HEAP_MB';
|
|
121
|
+
/**
|
|
122
|
+
* Explicit worker count for heavy tools. Replaces the planned count for every
|
|
123
|
+
* runner variable, even when that puts the run over its budget.
|
|
124
|
+
*
|
|
125
|
+
* @task T13122
|
|
126
|
+
*/
|
|
127
|
+
export const HEAVY_WORKERS_ENV = 'CLEO_HEAVY_WORKERS';
|
|
128
|
+
/**
|
|
129
|
+
* Explicit workspace concurrency for heavy tools (`pnpm -r` packages in flight).
|
|
130
|
+
*
|
|
131
|
+
* @task T13122
|
|
132
|
+
*/
|
|
133
|
+
export const HEAVY_WORKSPACE_CONCURRENCY_ENV = 'CLEO_HEAVY_WORKSPACE_CONCURRENCY';
|
|
134
|
+
/**
|
|
135
|
+
* The two spellings of pnpm's `workspace-concurrency` in the environment. pnpm
|
|
136
|
+
* 10 reads only `npm_config_workspace_concurrency`; pnpm 11+ reads only
|
|
137
|
+
* `pnpm_config_workspace_concurrency` (measured: pnpm 10.30.0 and 12.6.0 each
|
|
138
|
+
* report `undefined` for the other spelling). Setting one bounded `pnpm -r`
|
|
139
|
+
* fan-out on one pnpm major and silently nothing on the other, so the overlay
|
|
140
|
+
* sets both — for every launcher, because an `npm test` script that runs
|
|
141
|
+
* `pnpm -r` still reads them. Both are read case-insensitively, and an
|
|
142
|
+
* uppercase spelling outranks the lowercase one, so an inherited
|
|
143
|
+
* `NPM_CONFIG_WORKSPACE_CONCURRENCY` is planned and overlaid as well
|
|
144
|
+
* ({@link workspaceConcurrencyNames}).
|
|
145
|
+
*
|
|
146
|
+
* @task T13122
|
|
147
|
+
*/
|
|
148
|
+
export const WORKSPACE_CONCURRENCY_VARS = [
|
|
149
|
+
'npm_config_workspace_concurrency',
|
|
150
|
+
'pnpm_config_workspace_concurrency',
|
|
151
|
+
];
|
|
152
|
+
/**
|
|
153
|
+
* Every environment name that sets pnpm's workspace concurrency in `env`: the
|
|
154
|
+
* two canonical spellings, plus any case or dash variant of them present (pnpm
|
|
155
|
+
* reads these case-insensitively, uppercase first, and `workspace-concurrency`
|
|
156
|
+
* with a dash as readily).
|
|
157
|
+
*
|
|
158
|
+
* @param env - The caller's environment.
|
|
159
|
+
* @returns The names to plan and overlay, canonical first.
|
|
160
|
+
* @task T13122
|
|
161
|
+
*/
|
|
162
|
+
export function workspaceConcurrencyNames(env) {
|
|
163
|
+
const canonical = [...WORKSPACE_CONCURRENCY_VARS];
|
|
164
|
+
const wanted = new Set(canonical);
|
|
165
|
+
const variants = Object.keys(env).filter((name) => !wanted.has(name) && wanted.has(name.toLowerCase().replace(/-/g, '_')));
|
|
166
|
+
return [...canonical, ...variants.sort()];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* V8 young-generation (semi-space) ceiling a plan allows, in MiB: Node 24's own
|
|
170
|
+
* default on a 64-bit host. `heap_size_limit` is old space plus three
|
|
171
|
+
* semi-spaces (measured on Node 24: `--max-old-space-size=4096` with
|
|
172
|
+
* `--max-semi-space-size=4096` reports 16384 MiB), so an inherited large
|
|
173
|
+
* semi-space would multiply the planned heap without touching the old-space
|
|
174
|
+
* flag; above this it is clamped.
|
|
175
|
+
*
|
|
176
|
+
* @task T13122
|
|
177
|
+
*/
|
|
178
|
+
export const MAX_SEMI_SPACE_MB = 64;
|
|
67
179
|
/**
|
|
68
180
|
* Canonical tools treated as HEAVY — the ones that fork.
|
|
69
181
|
*
|
|
@@ -97,6 +209,35 @@ const HEAVY_TOOLS = new Set(['test', 'build']);
|
|
|
97
209
|
export function isHeavyTool(canonical) {
|
|
98
210
|
return HEAVY_TOOLS.has(canonical);
|
|
99
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Canonical tools whose memory must be bounded: the heavy ones, plus the
|
|
214
|
+
* single-process tools that build a whole TypeScript program (T13123).
|
|
215
|
+
*/
|
|
216
|
+
const MEMORY_BOUND_TOOLS = new Set(['test', 'build', 'typecheck', 'lint']);
|
|
217
|
+
/**
|
|
218
|
+
* Whether a canonical tool's memory is bounded: a heap ceiling and workspace
|
|
219
|
+
* concurrency in its spawn env, and a RAM-derived machine-wide slot count that
|
|
220
|
+
* shrinks under pressure.
|
|
221
|
+
*
|
|
222
|
+
* A superset of {@link isHeavyTool}. `typecheck` and `lint` were treated as
|
|
223
|
+
* cheap (`max(2, cpus/2)` slots, no ceiling) until T13123: nine concurrent
|
|
224
|
+
* `tsc` runs of 2–5 GB each on an 18-core box is most of 48 GB. `audit` and
|
|
225
|
+
* `security-scan` stay unbounded — they are network-bound and small.
|
|
226
|
+
*
|
|
227
|
+
* @param canonical - the tool in question.
|
|
228
|
+
* @returns `true` for `test`, `build`, `typecheck` and `lint`.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* isMemoryBoundTool('typecheck'); // true
|
|
233
|
+
* isMemoryBoundTool('audit'); // false
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @task T13123
|
|
237
|
+
*/
|
|
238
|
+
export function isMemoryBoundTool(canonical) {
|
|
239
|
+
return MEMORY_BOUND_TOOLS.has(canonical);
|
|
240
|
+
}
|
|
100
241
|
/**
|
|
101
242
|
* Per-runner worker-count variables, keyed by the env var each runner reads.
|
|
102
243
|
*
|
|
@@ -126,7 +267,11 @@ const WORKER_COUNT_VARS = [
|
|
|
126
267
|
'PYTEST_XDIST_AUTO_NUM_WORKERS',
|
|
127
268
|
];
|
|
128
269
|
/**
|
|
129
|
-
*
|
|
270
|
+
* Default worker count this machine can hold, given {@link GIB_PER_WORKER}.
|
|
271
|
+
*
|
|
272
|
+
* The planned count ({@link planHeavyToolEnv}) never exceeds this: a small
|
|
273
|
+
* inherited heap does not buy extra workers, because each worker also costs
|
|
274
|
+
* memory outside its heap.
|
|
130
275
|
*
|
|
131
276
|
* @param totalRamGib - total RAM in GiB; defaults to a live reading.
|
|
132
277
|
* @returns a value in `[MIN_HEAVY_WORKERS, MAX_HEAVY_WORKERS]`.
|
|
@@ -136,12 +281,340 @@ export function heavyToolWorkers(totalRamGib = totalmem() / 1024 ** 3) {
|
|
|
136
281
|
return Math.min(MAX_HEAVY_WORKERS, Math.max(MIN_HEAVY_WORKERS, byRam));
|
|
137
282
|
}
|
|
138
283
|
/**
|
|
139
|
-
*
|
|
284
|
+
* Default heap ceiling per Node process, in MiB: {@link HEAVY_TOOL_HEAP_MB},
|
|
285
|
+
* lowered to {@link HEAVY_HEAP_RAM_FRACTION} of RAM on a machine under 8 GiB
|
|
286
|
+
* (never below {@link MIN_HEAVY_HEAP_MB}).
|
|
287
|
+
*
|
|
288
|
+
* @param totalRamGib - total RAM in GiB; defaults to a live reading.
|
|
289
|
+
* @returns the default heap in MiB.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```ts
|
|
293
|
+
* defaultHeavyHeapMb(64); // → 4096
|
|
294
|
+
* defaultHeavyHeapMb(4); // → 2048
|
|
295
|
+
* ```
|
|
296
|
+
*
|
|
297
|
+
* @task T13122
|
|
298
|
+
*/
|
|
299
|
+
export function defaultHeavyHeapMb(totalRamGib = totalmem() / 1024 ** 3) {
|
|
300
|
+
const byRam = Math.floor(totalRamGib * 1024 * HEAVY_HEAP_RAM_FRACTION);
|
|
301
|
+
return Math.min(HEAVY_TOOL_HEAP_MB, Math.max(MIN_HEAVY_HEAP_MB, byRam));
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Default heap ceiling for a single-process tool (`typecheck`, `lint`), in MiB:
|
|
305
|
+
* {@link defaultHeavyHeapMb}, but never above a quarter of RAM — V8's own
|
|
306
|
+
* default old space on a machine under 16 GiB (measured 4288 MiB with no flags
|
|
307
|
+
* on a 64 GiB host, the capped default) — so the plan never RAISES a `tsc`'s
|
|
308
|
+
* ceiling on a small machine (review of #1810). Never below
|
|
309
|
+
* {@link MIN_HEAVY_HEAP_MB}.
|
|
310
|
+
*
|
|
311
|
+
* @param totalRamGib - total RAM in GiB; defaults to a live reading.
|
|
312
|
+
* @returns the default heap in MiB.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* defaultSingleProcessHeapMb(64); // → 4096
|
|
317
|
+
* defaultSingleProcessHeapMb(8); // → 2048
|
|
318
|
+
* ```
|
|
319
|
+
*
|
|
320
|
+
* @task T13123
|
|
321
|
+
*/
|
|
322
|
+
export function defaultSingleProcessHeapMb(totalRamGib = totalmem() / 1024 ** 3) {
|
|
323
|
+
const quarter = Math.max(MIN_HEAVY_HEAP_MB, Math.floor((totalRamGib * 1024) / 4));
|
|
324
|
+
return Math.min(defaultHeavyHeapMb(totalRamGib), quarter);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Heap budget for one heavy run, in MiB: the default worker count times the
|
|
328
|
+
* default heap. `workspace concurrency × workers × heap` must fit in it.
|
|
329
|
+
*
|
|
330
|
+
* Unchanged by T13122 for the default case (6 × 4096 = 24 GiB on 36 GiB and
|
|
331
|
+
* up, 1 × 4096 on 8 GiB); what changed is that an inherited heap is now planned
|
|
332
|
+
* against it instead of multiplying it.
|
|
333
|
+
*
|
|
334
|
+
* @param totalRamGib - total RAM in GiB; defaults to a live reading.
|
|
335
|
+
* @returns the budget in MiB.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```ts
|
|
339
|
+
* heavyRunBudgetMb(64); // → 24576 (6 workers × 4096)
|
|
340
|
+
* heavyRunBudgetMb(16); // → 8192 (2 workers × 4096)
|
|
341
|
+
* heavyRunBudgetMb(8); // → 4096 (1 worker × 4096)
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* @task T13122
|
|
345
|
+
*/
|
|
346
|
+
export function heavyRunBudgetMb(totalRamGib = totalmem() / 1024 ** 3) {
|
|
347
|
+
return heavyToolWorkers(totalRamGib) * defaultHeavyHeapMb(totalRamGib);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* `NODE_OPTIONS` flags that set a V8 heap limit. `max-old-space-size` and its
|
|
351
|
+
* percentage form decide the old-space ceiling; `max-semi-space-size` sizes the
|
|
352
|
+
* young generation, three of which count toward the limit, and is capped at
|
|
353
|
+
* {@link MAX_SEMI_SPACE_MB}.
|
|
354
|
+
*
|
|
355
|
+
* @task T12989
|
|
356
|
+
* @task T13122
|
|
357
|
+
*/
|
|
358
|
+
export const HEAP_FLAG_NAMES = new Set([
|
|
359
|
+
'max-old-space-size',
|
|
360
|
+
'max-old-space-size-percentage',
|
|
361
|
+
'max-semi-space-size',
|
|
362
|
+
]);
|
|
363
|
+
/** The two flags that set the old-space ceiling, which a plan replaces. */
|
|
364
|
+
const OLD_SPACE_FLAGS = new Set([
|
|
365
|
+
'max-old-space-size',
|
|
366
|
+
'max-old-space-size-percentage',
|
|
367
|
+
]);
|
|
368
|
+
/** The young-generation flag, capped at {@link MAX_SEMI_SPACE_MB}. */
|
|
369
|
+
const SEMI_SPACE_FLAG = new Set(['max-semi-space-size']);
|
|
370
|
+
function spannedWords(text) {
|
|
371
|
+
return [...text.matchAll(/\S+/g)].map((m) => ({
|
|
372
|
+
text: m[0],
|
|
373
|
+
start: m.index,
|
|
374
|
+
end: m.index + m[0].length,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Remove the heap flags named in `names` from a `NODE_OPTIONS` value, cutting
|
|
379
|
+
* their spans (and the whitespace before each) out of the original string, so
|
|
380
|
+
* every other byte — quoted paths with runs of spaces included — is kept.
|
|
381
|
+
*/
|
|
382
|
+
function cutHeapFlags(text, names) {
|
|
383
|
+
const words = spannedWords(text);
|
|
384
|
+
const tokens = words.map((w) => w.text);
|
|
385
|
+
let out = '';
|
|
386
|
+
let cursor = 0;
|
|
387
|
+
for (let i = 0; i < words.length; i++) {
|
|
388
|
+
const flag = heapTokenAt(tokens, i);
|
|
389
|
+
if (flag === null || !names.has(flag.name))
|
|
390
|
+
continue;
|
|
391
|
+
const first = words[i];
|
|
392
|
+
const last = words[i + flag.width - 1];
|
|
393
|
+
if (first === undefined || last === undefined)
|
|
394
|
+
continue;
|
|
395
|
+
// Drop the whitespace before the flag along with it.
|
|
396
|
+
let from = first.start;
|
|
397
|
+
while (from > cursor && /\s/.test(text[from - 1] ?? ''))
|
|
398
|
+
from--;
|
|
399
|
+
out += text.slice(cursor, from);
|
|
400
|
+
cursor = last.end;
|
|
401
|
+
i += flag.width - 1;
|
|
402
|
+
}
|
|
403
|
+
return (out + text.slice(cursor)).trim();
|
|
404
|
+
}
|
|
405
|
+
function heapTokenAt(tokens, i) {
|
|
406
|
+
const token = tokens[i] ?? '';
|
|
407
|
+
if (!token.startsWith('--'))
|
|
408
|
+
return null;
|
|
409
|
+
const eq = token.indexOf('=');
|
|
410
|
+
const name = (eq === -1 ? token.slice(2) : token.slice(2, eq)).replace(/_/g, '-');
|
|
411
|
+
if (!HEAP_FLAG_NAMES.has(name))
|
|
412
|
+
return null;
|
|
413
|
+
const next = tokens[i + 1];
|
|
414
|
+
if (eq === -1 && next !== undefined && /^\d+(\.\d+)?$/.test(next)) {
|
|
415
|
+
return { name, value: next, width: 2 };
|
|
416
|
+
}
|
|
417
|
+
return { name, value: eq === -1 ? '' : token.slice(eq + 1), width: 1 };
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The V8 heap flags a `NODE_OPTIONS` value sets, by name, with the value in
|
|
421
|
+
* effect.
|
|
422
|
+
*
|
|
423
|
+
* V8 reads flags left to right and a later value replaces an earlier one, so
|
|
424
|
+
* the LAST occurrence of each flag is the effective one. Underscores in a flag
|
|
425
|
+
* name read as dashes, as they do to V8, and the space-separated spelling
|
|
426
|
+
* (`--max-old-space-size 4096`) is read too.
|
|
427
|
+
*
|
|
428
|
+
* @param nodeOptions - A `NODE_OPTIONS` value, if any.
|
|
429
|
+
* @returns flag name → effective value, in first-seen order.
|
|
430
|
+
*
|
|
431
|
+
* @task T12989
|
|
432
|
+
* @task T13122
|
|
433
|
+
*/
|
|
434
|
+
export function parseHeapFlags(nodeOptions) {
|
|
435
|
+
const tokens = (nodeOptions ?? '').trim().split(/\s+/).filter(Boolean);
|
|
436
|
+
const values = new Map();
|
|
437
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
438
|
+
const flag = heapTokenAt(tokens, i);
|
|
439
|
+
if (flag === null)
|
|
440
|
+
continue;
|
|
441
|
+
values.set(flag.name, flag.value);
|
|
442
|
+
i += flag.width - 1;
|
|
443
|
+
}
|
|
444
|
+
return values;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* The old-space heap ceiling a `NODE_OPTIONS` value asks for, in MiB, or
|
|
448
|
+
* `null` when it sets none (or sets one Node could not use).
|
|
449
|
+
*
|
|
450
|
+
* `--max-old-space-size-percentage` wins over `--max-old-space-size` whatever
|
|
451
|
+
* their order (measured on Node 24.21: `=10` beside `=2048` gave a 6745 MiB
|
|
452
|
+
* limit on a 64 GiB machine either way round), and resolves against total RAM.
|
|
453
|
+
*
|
|
454
|
+
* @param nodeOptions - A `NODE_OPTIONS` value, if any.
|
|
455
|
+
* @param totalRamMb - Total RAM in MiB, for the percentage form.
|
|
456
|
+
* @returns the requested heap in MiB, or `null`.
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* inheritedHeapMb('--max-old-space-size=8192', 65536); // → 8192
|
|
461
|
+
* inheritedHeapMb('--max-old-space-size-percentage=25', 65536); // → 16384
|
|
462
|
+
* inheritedHeapMb('--enable-source-maps', 65536); // → null
|
|
463
|
+
* ```
|
|
464
|
+
*
|
|
465
|
+
* @task T13122
|
|
466
|
+
*/
|
|
467
|
+
export function inheritedHeapMb(nodeOptions, totalRamMb) {
|
|
468
|
+
const flags = parseHeapFlags(nodeOptions);
|
|
469
|
+
const pct = Number(flags.get('max-old-space-size-percentage') ?? Number.NaN);
|
|
470
|
+
if (Number.isFinite(pct) && pct > 0 && pct <= 100) {
|
|
471
|
+
return Math.floor((totalRamMb * pct) / 100);
|
|
472
|
+
}
|
|
473
|
+
const mb = Number(flags.get('max-old-space-size') ?? Number.NaN);
|
|
474
|
+
return Number.isFinite(mb) && mb >= 1 ? Math.floor(mb) : null;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Set the old-space heap ceiling in a `NODE_OPTIONS` value: every existing
|
|
478
|
+
* `--max-old-space-size` and `--max-old-space-size-percentage` is cut out and
|
|
479
|
+
* one `--max-old-space-size=<heapMb>` appended. Every other byte survives — a
|
|
480
|
+
* project may rely on `--experimental-*` or a quoted `--require` path there.
|
|
140
481
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
482
|
+
* Removing the percentage form is required, not tidiness: it outranks the size
|
|
483
|
+
* form whatever the order, so leaving it would silently undo the ceiling.
|
|
484
|
+
*
|
|
485
|
+
* @param existing - current `NODE_OPTIONS`, if any.
|
|
486
|
+
* @param heapMb - ceiling to apply.
|
|
487
|
+
* @returns the rewritten value.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```ts
|
|
491
|
+
* withHeapCeiling('--enable-source-maps --max-old-space-size=8192', 4096);
|
|
492
|
+
* // → '--enable-source-maps --max-old-space-size=4096'
|
|
493
|
+
* ```
|
|
494
|
+
*
|
|
495
|
+
* @task T13122
|
|
496
|
+
*/
|
|
497
|
+
export function withHeapCeiling(existing, heapMb) {
|
|
498
|
+
const rest = cutHeapFlags(existing ?? '', OLD_SPACE_FLAGS);
|
|
499
|
+
const flag = `--max-old-space-size=${heapMb}`;
|
|
500
|
+
return rest.length > 0 ? `${rest} ${flag}` : flag;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Cap the semi-space (young generation) flag of a `NODE_OPTIONS` value at
|
|
504
|
+
* {@link MAX_SEMI_SPACE_MB}: a larger `--max-semi-space-size` is cut out and the
|
|
505
|
+
* cap appended. A value at or under the cap is left alone.
|
|
506
|
+
*
|
|
507
|
+
* @param existing - `NODE_OPTIONS`, if any.
|
|
508
|
+
* @returns The value to use, and the inherited semi-space it clamped (or `null`).
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* ```ts
|
|
512
|
+
* withSemiSpaceCap('--max-semi-space-size=4096'); // → { value: '--max-semi-space-size=64', clampedFrom: 4096 }
|
|
513
|
+
* ```
|
|
514
|
+
*
|
|
515
|
+
* @task T13122
|
|
516
|
+
*/
|
|
517
|
+
export function withSemiSpaceCap(existing) {
|
|
518
|
+
const raw = parseHeapFlags(existing).get('max-semi-space-size');
|
|
519
|
+
const semi = Number(raw ?? Number.NaN);
|
|
520
|
+
if (raw === undefined || (Number.isFinite(semi) && semi <= MAX_SEMI_SPACE_MB)) {
|
|
521
|
+
return { value: existing, clampedFrom: null };
|
|
522
|
+
}
|
|
523
|
+
const rest = cutHeapFlags(existing ?? '', SEMI_SPACE_FLAG);
|
|
524
|
+
const flag = `--max-semi-space-size=${MAX_SEMI_SPACE_MB}`;
|
|
525
|
+
return {
|
|
526
|
+
value: rest.length > 0 ? `${rest} ${flag}` : flag,
|
|
527
|
+
clampedFrom: Number.isFinite(semi) ? semi : null,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Bound an inherited `MAKEFLAGS` to `workers` jobs.
|
|
532
|
+
*
|
|
533
|
+
* Absent: `-j<workers>` (GNU make sizes a bare `-j` off nproc). Carrying a
|
|
534
|
+
* jobserver (`--jobserver-auth=` / `--jobserver-fds=`): kept, since inside a
|
|
535
|
+
* `make` recipe the parent's jobserver already bounds the jobs. Otherwise any
|
|
536
|
+
* `-j`, `-jN`, `--jobs[=N]`, a short cluster ending in j (`-sj18`) or make's
|
|
537
|
+
* dash-less first word (`j18`) above `workers` (a bare one is unlimited) is
|
|
538
|
+
* replaced by `-j<workers>`, keeping every other flag — a profile-wide
|
|
539
|
+
* `export MAKEFLAGS=-j18` no longer outruns the plan.
|
|
540
|
+
*
|
|
541
|
+
* @param raw - The inherited `MAKEFLAGS`.
|
|
542
|
+
* @param workers - The planned worker count.
|
|
543
|
+
* @returns The value to set, or `null` to keep the inherited one.
|
|
544
|
+
* @task T13122
|
|
545
|
+
*/
|
|
546
|
+
export function boundMakeflags(raw, workers) {
|
|
547
|
+
if (raw === undefined || raw.trim() === '')
|
|
548
|
+
return `-j${workers}`;
|
|
549
|
+
const words = raw.trim().split(/\s+/);
|
|
550
|
+
if (words.some((w) => /^--jobserver-(auth|fds)=/.test(w)))
|
|
551
|
+
return null;
|
|
552
|
+
const kept = [];
|
|
553
|
+
let over = false;
|
|
554
|
+
for (let i = 0; i < words.length; i++) {
|
|
555
|
+
const word = words[i] ?? '';
|
|
556
|
+
const long = /^--jobs(?:=(\d*))?$/.exec(word);
|
|
557
|
+
// A short-flag cluster ending in j (`-j18`, `-sj18`, `-kj`), or make's own
|
|
558
|
+
// dash-less letter form as the first word (`j18`, `kj`). Only make's
|
|
559
|
+
// argument-less short flags may precede the j: in `-Ij18` the `j18` is
|
|
560
|
+
// -I's directory, and splitting it would hand -I the `-j` (review of #1824).
|
|
561
|
+
const short = long === null ? /^(-?)([BbdehikLnpqrRsStvw]*)j(\d*)$/.exec(word) : null;
|
|
562
|
+
const isShort = short !== null && (short[1] === '-' || i === 0);
|
|
563
|
+
if (long === null && !isShort) {
|
|
564
|
+
kept.push(word);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
let count = long !== null ? (long[1] ?? '') : (short?.[3] ?? '');
|
|
568
|
+
const takesNext = long !== null ? long[1] === undefined : count === '';
|
|
569
|
+
const next = words[i + 1];
|
|
570
|
+
if (count === '' && takesNext && next !== undefined && /^\d+$/.test(next)) {
|
|
571
|
+
count = next;
|
|
572
|
+
i++;
|
|
573
|
+
}
|
|
574
|
+
// The cluster's other letters (`s`, `k`) are flags of their own: keep them.
|
|
575
|
+
if (isShort && short?.[2])
|
|
576
|
+
kept.push(`-${short[2]}`);
|
|
577
|
+
if (count === '' || Number(count) > workers)
|
|
578
|
+
over = true;
|
|
579
|
+
else
|
|
580
|
+
kept.push(`-j${count}`);
|
|
581
|
+
}
|
|
582
|
+
return over ? [...kept.filter((w) => !/^-j\d+$/.test(w)), `-j${workers}`].join(' ') : null;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Remove npm's `Unknown env config` warnings from captured output.
|
|
586
|
+
*
|
|
587
|
+
* npm warns once per run for every `npm_config_*` variable it does not know,
|
|
588
|
+
* including `npm_config_workspace_concurrency`, which the overlay sets for the
|
|
589
|
+
* `pnpm -r` an `npm test` script may run. The warning would otherwise sit in
|
|
590
|
+
* the stderr tail CLEO quotes when a tool fails and push the real error out of
|
|
591
|
+
* it; the variable itself stays, because dropping it for npm unbounded those
|
|
592
|
+
* child `pnpm -r` runs (review of #1808).
|
|
593
|
+
*
|
|
594
|
+
* @param text - Captured stderr.
|
|
595
|
+
* @returns The text without those warning lines.
|
|
596
|
+
* @task T13122
|
|
597
|
+
*/
|
|
598
|
+
export function withoutNpmEnvConfigWarnings(text) {
|
|
599
|
+
if (!text.includes('Unknown env config'))
|
|
600
|
+
return text;
|
|
601
|
+
return text
|
|
602
|
+
.split('\n')
|
|
603
|
+
.filter(
|
|
604
|
+
// A coloured npm (FORCE_COLOR, color=always) prefixes ANSI codes.
|
|
605
|
+
(line) => !/^npm (warn|WARN) Unknown env config\b/.test(line.replace(ANSI_SGR, '')))
|
|
606
|
+
.join('\n');
|
|
607
|
+
}
|
|
608
|
+
/** ANSI select-graphic-rendition escapes (colours), stripped before matching npm's warning. */
|
|
609
|
+
const ANSI_SGR = /\u001b\[[0-9;]*m/g;
|
|
610
|
+
/**
|
|
611
|
+
* Append `--max-old-space-size` to an existing `NODE_OPTIONS`, or create it,
|
|
612
|
+
* leaving an existing `--max-old-space-size` alone.
|
|
613
|
+
*
|
|
614
|
+
* @deprecated Since T13122 the heavy-tool overlay plans the heap against the
|
|
615
|
+
* run's budget instead of letting any inherited value win; use
|
|
616
|
+
* {@link planHeavyToolEnv} (or {@link withHeapCeiling} to set a ceiling).
|
|
617
|
+
* Kept for SDK consumers.
|
|
145
618
|
*
|
|
146
619
|
* @param existing - current `NODE_OPTIONS`, if any.
|
|
147
620
|
* @param heapMb - ceiling to apply.
|
|
@@ -154,50 +627,256 @@ export function mergeNodeOptions(existing, heapMb) {
|
|
|
154
627
|
const flag = `--max-old-space-size=${heapMb}`;
|
|
155
628
|
return current.length > 0 ? `${current} ${flag}` : flag;
|
|
156
629
|
}
|
|
630
|
+
/** A strictly positive base-10 integer, or `null` (`'50%'`, `'0'`, `'-1'`, `'4x'` are all `null`). */
|
|
631
|
+
function positiveInt(raw) {
|
|
632
|
+
const text = (raw ?? '').trim();
|
|
633
|
+
if (!/^\d+$/.test(text))
|
|
634
|
+
return null;
|
|
635
|
+
const n = Number.parseInt(text, 10);
|
|
636
|
+
return n > 0 ? n : null;
|
|
637
|
+
}
|
|
638
|
+
/** An explicit `CLEO_HEAVY_*` override, or `null`; an unusable value is noted and ignored. */
|
|
639
|
+
function readOverride(env, name, ignored) {
|
|
640
|
+
const raw = env[name];
|
|
641
|
+
if (raw === undefined || raw.trim() === '')
|
|
642
|
+
return null;
|
|
643
|
+
const n = positiveInt(raw);
|
|
644
|
+
if (n === null)
|
|
645
|
+
ignored.push(`${name}=${JSON.stringify(raw)} (not a positive integer)`);
|
|
646
|
+
return n;
|
|
647
|
+
}
|
|
648
|
+
/** The heap flags of a `NODE_OPTIONS` value as one readable string, e.g. `--max-old-space-size=8192`. */
|
|
649
|
+
function heapFlagsText(nodeOptions) {
|
|
650
|
+
return [...parseHeapFlags(nodeOptions)]
|
|
651
|
+
.filter(([name]) => OLD_SPACE_FLAGS.has(name))
|
|
652
|
+
.map(([name, value]) => `--${name}=${value}`)
|
|
653
|
+
.join(' ');
|
|
654
|
+
}
|
|
655
|
+
function chooseHeap(env, override, maxHeapMb, totalRamMb, defaultHeapMb) {
|
|
656
|
+
const inherited = inheritedHeapMb(env.NODE_OPTIONS, totalRamMb);
|
|
657
|
+
if (override !== null)
|
|
658
|
+
return { heapMb: override, source: 'override', inherited };
|
|
659
|
+
// The default heap never shrinks to fit an explicit workspace concurrency: a
|
|
660
|
+
// run asked for that way is reported over budget instead (`overBudget`).
|
|
661
|
+
if (inherited === null)
|
|
662
|
+
return { heapMb: defaultHeapMb, source: 'default', inherited };
|
|
663
|
+
if (inherited <= maxHeapMb)
|
|
664
|
+
return { heapMb: inherited, source: 'inherited', inherited };
|
|
665
|
+
return { heapMb: maxHeapMb, source: 'clamped', inherited };
|
|
666
|
+
}
|
|
667
|
+
/** One worker-count variable: keep an inherited value within the plan, else overlay the plan. */
|
|
668
|
+
function planCount(name, raw, planned, forced, overlay, clamped, kept) {
|
|
669
|
+
const inherited = positiveInt(raw);
|
|
670
|
+
if (!forced && inherited !== null && inherited <= planned) {
|
|
671
|
+
kept.push(`${name}=${inherited}`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
overlay[name] = String(planned);
|
|
675
|
+
if (!forced && raw !== undefined && raw !== '') {
|
|
676
|
+
clamped.push({ name, from: raw, to: String(planned) });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** A tool that runs as one process: nothing to size. */
|
|
680
|
+
const SINGLE_PROCESS = {
|
|
681
|
+
workers: 1,
|
|
682
|
+
source: 'plan',
|
|
683
|
+
reason: 'one process, no worker pool',
|
|
684
|
+
};
|
|
157
685
|
/**
|
|
158
|
-
*
|
|
686
|
+
* The worker count of a forking tool — `CLEO_HEAVY_WORKERS`, else as many as
|
|
687
|
+
* fit (`fit`, the budget over packages × heap), never above the default — written
|
|
688
|
+
* to every runner variable and to `MAKEFLAGS`.
|
|
689
|
+
*/
|
|
690
|
+
function planWorkers(env, fit, defaultWorkers, ledger) {
|
|
691
|
+
const override = readOverride(env, HEAVY_WORKERS_ENV, ledger.ignored);
|
|
692
|
+
const workers = override ?? Math.max(MIN_HEAVY_WORKERS, Math.min(defaultWorkers, Math.floor(fit)));
|
|
693
|
+
for (const key of WORKER_COUNT_VARS) {
|
|
694
|
+
planCount(key, env[key], workers, override !== null, ledger.overlay, ledger.clamped, ledger.kept);
|
|
695
|
+
}
|
|
696
|
+
// GNU make sizes `-j` off nproc when told `-j` with no argument; an explicit
|
|
697
|
+
// job count bounds a Makefile-driven test/build target too.
|
|
698
|
+
const makeflags = boundMakeflags(env.MAKEFLAGS, workers);
|
|
699
|
+
if (makeflags !== null) {
|
|
700
|
+
ledger.overlay.MAKEFLAGS = makeflags;
|
|
701
|
+
if (env.MAKEFLAGS && env.MAKEFLAGS.trim() !== '' && override === null) {
|
|
702
|
+
ledger.clamped.push({ name: 'MAKEFLAGS', from: env.MAKEFLAGS, to: makeflags });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (override !== null)
|
|
706
|
+
return { workers, source: 'override', reason: HEAVY_WORKERS_ENV };
|
|
707
|
+
return {
|
|
708
|
+
workers,
|
|
709
|
+
source: 'plan',
|
|
710
|
+
reason: workers < defaultWorkers
|
|
711
|
+
? `fewer than the default ${defaultWorkers} so the run fits its budget`
|
|
712
|
+
: 'default for this RAM',
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function heapReason(choice) {
|
|
716
|
+
switch (choice.source) {
|
|
717
|
+
case 'override':
|
|
718
|
+
return HEAVY_HEAP_ENV;
|
|
719
|
+
case 'inherited':
|
|
720
|
+
return 'inherited NODE_OPTIONS';
|
|
721
|
+
case 'clamped':
|
|
722
|
+
return `inherited NODE_OPTIONS asked for ${choice.inherited} MiB, more than the budget allows`;
|
|
723
|
+
default:
|
|
724
|
+
return 'CLEO default';
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Plan a heavy or memory-bound tool spawn: the environment overlay, and the
|
|
729
|
+
* resource plan behind it (T13122, T13123).
|
|
159
730
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
731
|
+
* The heap is chosen first — `CLEO_HEAVY_HEAP_MB`, else the inherited
|
|
732
|
+
* `NODE_OPTIONS` heap when it fits the budget (clamped to it otherwise), else
|
|
733
|
+
* {@link defaultHeavyHeapMb}. The worker count then derives from the heap
|
|
734
|
+
* actually in effect, `⌊budget / (packages × heap)⌋`, never above
|
|
735
|
+
* {@link heavyToolWorkers}, so `packages × workers × heap` stays within
|
|
736
|
+
* {@link heavyRunBudgetMb}. Inherited worker counts and workspace concurrency
|
|
737
|
+
* are kept at or below the plan and clamped above it; `CLEO_HEAVY_WORKERS` and
|
|
738
|
+
* `CLEO_HEAVY_WORKSPACE_CONCURRENCY` override.
|
|
739
|
+
*
|
|
740
|
+
* A single-process memory-bound tool (`typecheck`, `lint`) gets the same heap
|
|
741
|
+
* ceiling and workspace concurrency, and no worker variables: its plan is one
|
|
742
|
+
* process (T13123).
|
|
162
743
|
*
|
|
163
744
|
* @param canonical - the canonical tool about to be spawned.
|
|
164
745
|
* @param env - the environment the child would otherwise inherit.
|
|
165
746
|
* @param totalRamGib - total RAM in GiB; injectable for deterministic tests.
|
|
166
|
-
* @returns
|
|
747
|
+
* @returns the overlay and plan; an empty overlay and `null` plan for a tool
|
|
748
|
+
* that is not memory-bound (`audit`, `security-scan`).
|
|
167
749
|
*
|
|
168
750
|
* @example
|
|
169
751
|
* ```ts
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* //
|
|
174
|
-
*
|
|
752
|
+
* // A shell profile exported an 8 GiB heap on a 64 GiB machine:
|
|
753
|
+
* const { overlay, resources } = planHeavyToolEnv(
|
|
754
|
+
* 'test', { NODE_OPTIONS: '--max-old-space-size=8192' }, 64);
|
|
755
|
+
* overlay.VITEST_MAX_WORKERS; // → '3' (3 × 8192 = the 24576 MiB budget)
|
|
756
|
+
* resources?.heapSource; // → 'inherited'
|
|
175
757
|
* ```
|
|
176
758
|
*
|
|
177
759
|
* @task T12096
|
|
760
|
+
* @task T13122
|
|
761
|
+
* @task T13123
|
|
178
762
|
*/
|
|
179
|
-
export function
|
|
180
|
-
if (!
|
|
181
|
-
return {};
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
763
|
+
export function planHeavyToolEnv(canonical, env = process.env, totalRamGib = totalmem() / 1024 ** 3) {
|
|
764
|
+
if (!isMemoryBoundTool(canonical))
|
|
765
|
+
return { overlay: {}, resources: null };
|
|
766
|
+
const totalRamMb = Math.floor(totalRamGib * 1024);
|
|
767
|
+
const defaultWorkers = heavyToolWorkers(totalRamGib);
|
|
768
|
+
// A single process starts from Node's own default ceiling on its machine; a
|
|
769
|
+
// forking tool from the heavy default. Both share the heavy run's budget.
|
|
770
|
+
const defaultHeapMb = isHeavyTool(canonical)
|
|
771
|
+
? defaultHeavyHeapMb(totalRamGib)
|
|
772
|
+
: defaultSingleProcessHeapMb(totalRamGib);
|
|
773
|
+
const budgetMb = defaultWorkers * defaultHeavyHeapMb(totalRamGib);
|
|
774
|
+
const overlay = {};
|
|
775
|
+
const clamped = [];
|
|
776
|
+
const kept = [];
|
|
777
|
+
const ignored = [];
|
|
778
|
+
// Packages in flight first: the heap ceiling an inherited value may keep is
|
|
779
|
+
// the budget shared between them.
|
|
780
|
+
const packagesOverride = readOverride(env, HEAVY_WORKSPACE_CONCURRENCY_ENV, ignored);
|
|
781
|
+
const packages = packagesOverride ?? WORKSPACE_CONCURRENCY;
|
|
782
|
+
const packageNames = workspaceConcurrencyNames(env);
|
|
783
|
+
for (const name of packageNames) {
|
|
784
|
+
planCount(name, env[name], packages, packagesOverride !== null, overlay, clamped, kept);
|
|
192
785
|
}
|
|
193
|
-
|
|
194
|
-
|
|
786
|
+
// What the child gets: the overlay's value, or the inherited one it kept —
|
|
787
|
+
// the largest across every spelling, since which one pnpm reads depends on
|
|
788
|
+
// its version and the case.
|
|
789
|
+
const packagesInFlight = Math.max(...packageNames.map((name) => positiveInt(overlay[name] ?? env[name]) ?? packages));
|
|
790
|
+
const heap = chooseHeap(env, readOverride(env, HEAVY_HEAP_ENV, ignored), Math.max(1, Math.floor(budgetMb / packagesInFlight)), totalRamMb, defaultHeapMb);
|
|
791
|
+
// Young generation first: three semi-spaces count toward the heap limit.
|
|
792
|
+
const semi = withSemiSpaceCap(env.NODE_OPTIONS);
|
|
793
|
+
if (semi.clampedFrom !== null) {
|
|
794
|
+
clamped.push({
|
|
795
|
+
name: 'NODE_OPTIONS',
|
|
796
|
+
from: `--max-semi-space-size=${semi.clampedFrom}`,
|
|
797
|
+
to: `--max-semi-space-size=${MAX_SEMI_SPACE_MB}`,
|
|
798
|
+
});
|
|
195
799
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
800
|
+
overlay.NODE_OPTIONS = withHeapCeiling(semi.value, heap.heapMb);
|
|
801
|
+
if (heap.source === 'clamped') {
|
|
802
|
+
clamped.push({
|
|
803
|
+
name: 'NODE_OPTIONS',
|
|
804
|
+
from: heapFlagsText(env.NODE_OPTIONS),
|
|
805
|
+
to: `--max-old-space-size=${heap.heapMb}`,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
// A single-process tool (typecheck, lint) has no worker pool to size.
|
|
809
|
+
const pool = isHeavyTool(canonical)
|
|
810
|
+
? planWorkers(env, budgetMb / (packagesInFlight * heap.heapMb), defaultWorkers, {
|
|
811
|
+
overlay,
|
|
812
|
+
clamped,
|
|
813
|
+
kept,
|
|
814
|
+
ignored,
|
|
815
|
+
})
|
|
816
|
+
: SINGLE_PROCESS;
|
|
817
|
+
const { workers } = pool;
|
|
818
|
+
const product = packagesInFlight * workers * heap.heapMb;
|
|
819
|
+
const overBudget = product > budgetMb;
|
|
820
|
+
const parts = [
|
|
821
|
+
`heap ${heap.heapMb} MiB (${heapReason(heap)}) × ${workers} worker(s) (${pool.reason}) × ` +
|
|
822
|
+
`${packagesInFlight} workspace package(s) at once = ${product} MiB of a ${budgetMb} MiB budget ` +
|
|
823
|
+
`(${Math.round(totalRamMb / 1024)} GiB RAM)`,
|
|
824
|
+
];
|
|
825
|
+
if (clamped.length > 0) {
|
|
826
|
+
parts.push(`clamped ${clamped.map((c) => `${c.name} ${c.from} → ${c.to}`).join(', ')}; ` +
|
|
827
|
+
`set ${HEAVY_HEAP_ENV}, ${HEAVY_WORKERS_ENV} or ${HEAVY_WORKSPACE_CONCURRENCY_ENV} to ask for more`);
|
|
200
828
|
}
|
|
201
|
-
|
|
829
|
+
if (overBudget)
|
|
830
|
+
parts.push('OVER budget by an explicit CLEO_HEAVY_* override');
|
|
831
|
+
if (ignored.length > 0)
|
|
832
|
+
parts.push(`ignored ${ignored.join(', ')}`);
|
|
833
|
+
return {
|
|
834
|
+
overlay,
|
|
835
|
+
resources: {
|
|
836
|
+
heapMb: heap.heapMb,
|
|
837
|
+
heapSource: heap.source,
|
|
838
|
+
inheritedHeapMb: heap.inherited,
|
|
839
|
+
workers,
|
|
840
|
+
workersSource: pool.source,
|
|
841
|
+
workspaceConcurrency: packagesInFlight,
|
|
842
|
+
budgetMb,
|
|
843
|
+
totalRamMb,
|
|
844
|
+
clamped,
|
|
845
|
+
kept,
|
|
846
|
+
overBudget,
|
|
847
|
+
summary: parts.join('; '),
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Build the environment overlay for a heavy or memory-bound tool spawn: the
|
|
853
|
+
* `overlay` of {@link planHeavyToolEnv}.
|
|
854
|
+
*
|
|
855
|
+
* Returns an empty object for a tool that is not memory-bound, so the caller
|
|
856
|
+
* can merge unconditionally.
|
|
857
|
+
*
|
|
858
|
+
* @param canonical - the canonical tool about to be spawned.
|
|
859
|
+
* @param env - the environment the child would otherwise inherit.
|
|
860
|
+
* @param totalRamGib - total RAM in GiB; injectable for deterministic tests.
|
|
861
|
+
* @returns variables to overlay; inherited values within the plan are kept.
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* ```ts
|
|
865
|
+
* const overlay = heavyToolEnv('test', {}, 62);
|
|
866
|
+
* // { NODE_OPTIONS: '--max-old-space-size=4096',
|
|
867
|
+
* // VITEST_MAX_WORKERS: '6',
|
|
868
|
+
* // npm_config_workspace_concurrency: '1', … }
|
|
869
|
+
* heavyToolEnv('typecheck', {}, 62);
|
|
870
|
+
* // { NODE_OPTIONS: '--max-old-space-size=4096',
|
|
871
|
+
* // npm_config_workspace_concurrency: '1', pnpm_config_workspace_concurrency: '1' }
|
|
872
|
+
* heavyToolEnv('audit', process.env, 62); // → {}
|
|
873
|
+
* ```
|
|
874
|
+
*
|
|
875
|
+
* @task T12096
|
|
876
|
+
* @task T13122
|
|
877
|
+
* @task T13123
|
|
878
|
+
*/
|
|
879
|
+
export function heavyToolEnv(canonical, env = process.env, totalRamGib = totalmem() / 1024 ** 3) {
|
|
880
|
+
return planHeavyToolEnv(canonical, env, totalRamGib).overlay;
|
|
202
881
|
}
|
|
203
882
|
//# sourceMappingURL=heavy-tool-env.js.map
|