@quolu/lattice 0.79.2 → 0.79.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.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.ja.md +1 -1
  3. package/README.md +9 -3
  4. package/bin/lattice-bridge.mjs +31 -24
  5. package/bin/lattice-dashboard-supervisor.mjs +18 -0
  6. package/bin/lattice-dashboard.mjs +32 -10
  7. package/docs/bridge-setup.md +29 -0
  8. package/package.json +2 -2
  9. package/sensor/dist/bin/lattice-sensor.js +6 -26
  10. package/sensor/dist/bin/node-version-check.d.ts +10 -1
  11. package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
  12. package/sensor/dist/bin/node-version-check.js +43 -1
  13. package/sensor/dist/bin/node-version-check.js.map +1 -1
  14. package/sensor/dist/extraction/parse-pool.d.ts +8 -1
  15. package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
  16. package/sensor/dist/extraction/parse-pool.js +83 -12
  17. package/sensor/dist/extraction/parse-pool.js.map +1 -1
  18. package/sensor/dist/extraction/parse-worker.js +2 -0
  19. package/sensor/dist/extraction/parse-worker.js.map +1 -1
  20. package/sensor/dist/mcp/daemon-paths.d.ts +4 -0
  21. package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
  22. package/sensor/dist/mcp/daemon-paths.js +12 -0
  23. package/sensor/dist/mcp/daemon-paths.js.map +1 -1
  24. package/sensor/dist/mcp/index.d.ts.map +1 -1
  25. package/sensor/dist/mcp/index.js +52 -28
  26. package/sensor/dist/mcp/index.js.map +1 -1
  27. package/sensor/package.json +1 -1
  28. package/src/bridge-cli.mjs +6 -2
  29. package/src/bridge-config.mjs +8 -0
  30. package/src/bridge-daemon.mjs +30 -2
  31. package/src/dashboard-delivery.mjs +14 -2
  32. package/src/dashboard-lifecycle-ipc.mjs +5 -0
  33. package/src/dashboard-lifecycle-journal.mjs +235 -0
  34. package/src/dashboard-process-control.mjs +74 -0
  35. package/src/dashboard-supervisor.mjs +180 -0
  36. package/src/dashboard-windows-process-control.ps1 +40 -0
  37. package/src/factory-diagnostics.mjs +22 -9
  38. package/src/node-version-guard.mjs +3 -42
  39. package/src/todo-dashboard-registry.mjs +24 -3
package/LICENSE CHANGED
@@ -138,7 +138,7 @@ Patent notice: the design implemented by this software is the subject of Japanes
138
138
  patent application 特願2026-178950 (filed 2026-07-27).
139
139
 
140
140
  Commercial use requires a separate commercial license.
141
- Enquiries: kitepon@gmail.com
141
+ Enquiries: quolu@kitepon.dev
142
142
  Whether such a license is granted, and on what terms, is decided case by case.
143
143
 
144
144
  Third-party components: the bundled structure sensor in `sensor/` is third-party work
package/README.ja.md CHANGED
@@ -446,7 +446,7 @@ authoring入口と閉じる口の契約は
446
446
  中で使う場合を含みます
447
447
 
448
448
  **商用利用には**、別途の商用ライセンスが必要です。
449
- 問い合わせは[kitepon@gmail.com](mailto:kitepon@gmail.com)へのメールで受け付けます。
449
+ 問い合わせは[quolu@kitepon.dev](mailto:quolu@kitepon.dev)へのメールで受け付けます。
450
450
  許諾するかどうか、どのような条件にするかは個別に判断します。
451
451
 
452
452
  同梱の構造sensor([`sensor/`](sensor/))は本repositoryへ吸収した第三者成果物で、
package/README.md CHANGED
@@ -159,8 +159,14 @@ npm install -g @quolu/lattice
159
159
  lattice setup --json
160
160
  ```
161
161
 
162
- Requires **Node.js 22.13 or newer, except 25.x** (`engines: >=22.13 <25 || >=26`; Node 25's V8
163
- turboshaft WASM JIT breaks the bundled sensor, so it is blocked with a banner). The structure sensor ships inside the package — there is nothing
162
+ Requires **Node.js 22.13 or newer** with the supported runtime ranges
163
+ `>=22.13 <24.2 || >=24.22 <25 || >=26.10`. Node 24.2–24.21 and 26.0–26.9
164
+ contain the [upstream cppgc Realm lifetime defect](https://github.com/nodejs/node/pull/65778),
165
+ which can corrupt the heap during worker teardown. Use 24.22.0+ or 26.10.0+ on
166
+ those release lines; Node 22 LTS remains supported. Node 25.x remains excluded
167
+ because its V8 WASM JIT can crash while compiling tree-sitter grammars.
168
+
169
+ The structure sensor ships inside the package — there is nothing
164
170
  else to install, and Lattice never falls back to a sensor on your `PATH`.
165
171
 
166
172
  `lattice setup`は既存のClaude・Codex・Grok・Cursor設定を検出し、LatticeのMCP登録、
@@ -385,7 +391,7 @@ Detailed operational notes (dashboard, bridge, actor environment, store transact
385
391
  regardless of whether Lattice itself is redistributed.
386
392
 
387
393
  **For commercial use**, a separate commercial license is required.
388
- Enquiries can be made by email at [kitepon@gmail.com](mailto:kitepon@gmail.com). Whether a
394
+ Enquiries can be made by email at [quolu@kitepon.dev](mailto:quolu@kitepon.dev). Whether a
389
395
  license is granted, and on what terms, is decided case by case.
390
396
 
391
397
  The bundled structure sensor in [`sensor/`](sensor/) is third-party work absorbed into this
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { readBridgeConfig } from '../src/bridge-config.mjs';
4
4
  import {
5
- bridgeDaemonVersionDrifted, readBridgeStopRequest, removeBridgeDaemonActiveMarker,
6
- removeBridgeDaemonDescriptor, writeBridgeDaemonDescriptor, writeBridgeStopReceipt,
5
+ bridgeDaemonVersionDrifted, completeBridgeDaemonStop, readBridgeStopRequest,
6
+ writeBridgeDaemonDescriptor,
7
7
  } from '../src/bridge-daemon.mjs';
8
8
  import { createBridgeHubHeartbeatController } from '../src/bridge-hub-heartbeat.mjs';
9
9
  import { migrateBridgeToHub, retireBridgeTunnelLaunchAgent } from '../src/bridge-hub-migration.mjs';
@@ -31,25 +31,29 @@ if (typeof instanceToken !== 'string' || !/^[0-9a-f]{64}$/u.test(instanceToken))
31
31
  const controller = bridgeRuntimeController({ env, instanceToken,
32
32
  heartbeat: () => hubHeartbeat.lastHeartbeatSummary() });
33
33
  let timer;
34
- let closing = false;
34
+ let closing = null;
35
+ let stopping = null;
35
36
  let descriptorFingerprint = null;
36
37
  let heartbeatBinding = null;
37
38
  let checking = false;
38
39
  let failClosedError = null;
39
40
  let hubHeartbeatError = null;
40
- const close = async () => {
41
- if (closing) return;
42
- closing = true;
43
- clearInterval(timer);
44
- await controller.close();
41
+ const close = () => {
42
+ if (closing === null) {
43
+ clearInterval(timer);
44
+ closing = controller.close();
45
+ }
46
+ return closing;
47
+ };
48
+ const stop = () => {
49
+ if (stopping === null) stopping = completeBridgeDaemonStop({ env, close });
50
+ return stopping;
45
51
  };
46
52
 
47
53
  try {
48
54
  const stopRequest = await readBridgeStopRequest({ env });
49
55
  if (stopRequest !== null) {
50
- await writeBridgeStopReceipt({ nonce: stopRequest.nonce, env });
51
- await removeBridgeDaemonDescriptor({ env });
52
- await removeBridgeDaemonActiveMarker({ env });
56
+ await stop();
53
57
  process.exit(0);
54
58
  }
55
59
  const config = await readBridgeConfig({ env });
@@ -64,15 +68,12 @@ try {
64
68
  }
65
69
 
66
70
  timer = setInterval(async () => {
67
- if (checking || closing) return;
71
+ if (checking || closing !== null || stopping !== null) return;
68
72
  checking = true;
69
73
  try {
70
74
  const stopRequest = await readBridgeStopRequest({ env });
71
75
  if (stopRequest !== null) {
72
- await close();
73
- await writeBridgeStopReceipt({ nonce: stopRequest.nonce, env });
74
- await removeBridgeDaemonDescriptor({ env });
75
- await removeBridgeDaemonActiveMarker({ env });
76
+ await stop();
76
77
  process.exit(0);
77
78
  }
78
79
  // A stale-version exit is a clean stop, not a failure: whatever supervises
@@ -83,9 +84,7 @@ timer = setInterval(async () => {
83
84
  if (Date.now() - lastVersionCheckAt >= BACKGROUND_CHECK_INTERVAL_MS) {
84
85
  lastVersionCheckAt = Date.now();
85
86
  if (await bridgeDaemonVersionDrifted({})) {
86
- await close();
87
- await removeBridgeDaemonDescriptor({ env });
88
- await removeBridgeDaemonActiveMarker({ env });
87
+ await stop();
89
88
  process.exit(0);
90
89
  }
91
90
  }
@@ -122,9 +121,7 @@ timer = setInterval(async () => {
122
121
  }
123
122
  const config = await readBridgeConfig({ env });
124
123
  if (config === null || !config.enabled) {
125
- await close();
126
- await removeBridgeDaemonDescriptor({ env });
127
- await removeBridgeDaemonActiveMarker({ env });
124
+ await stop();
128
125
  process.exit(0);
129
126
  }
130
127
  const binding = await controller.reconcile();
@@ -169,10 +166,20 @@ timer = setInterval(async () => {
169
166
  const fingerprint = JSON.stringify(failure);
170
167
  if (fingerprint !== failClosedError) process.stderr.write(`${fingerprint}\n`);
171
168
  failClosedError = fingerprint;
169
+ if (stopping !== null) {
170
+ // A failed terminal transition cannot be acknowledged or turned into exit 0.
171
+ clearInterval(timer);
172
+ process.exit(1);
173
+ }
172
174
  // Public traffic fails closed immediately. Keep only the local control loop alive so an
173
175
  // authenticated nonce stop request can still receive a socket-closed receipt.
174
176
  await controller.close();
175
177
  } finally { checking = false; }
176
178
  }, 250);
177
- process.once('SIGINT', () => close().finally(() => process.exit(0)));
178
- process.once('SIGTERM', () => close().finally(() => process.exit(0)));
179
+ const stopOnSignal = () => stop().then(() => process.exit(0), (error) => {
180
+ process.stderr.write(`${JSON.stringify({ schema: 'lattice.bridge_daemon_error.v1',
181
+ code: error?.code ?? 'BRIDGE_DAEMON_STOP_FAILED', message: 'bridge daemon did not complete shutdown' })}\n`);
182
+ process.exit(1);
183
+ });
184
+ process.once('SIGINT', stopOnSignal);
185
+ process.once('SIGTERM', stopOnSignal);
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { superviseDashboard } from '../src/dashboard-supervisor.mjs';
4
+
5
+ try {
6
+ const root = process.env.LATTICE_DASHBOARD_RUNTIME_DIR;
7
+ if (process.argv.length !== 3 || process.argv[2] !== root || !path.isAbsolute(root)) {
8
+ throw Object.assign(new Error('invalid supervisor runtime'), { code: 'DASHBOARD_SUPERVISOR_ARGUMENT_INVALID' });
9
+ }
10
+ await superviseDashboard({ args: [path.resolve(import.meta.dirname, 'lattice-dashboard.mjs'), '--supervised-runtime', root],
11
+ env: process.env, root: process.env.LATTICE_DASHBOARD_RUNTIME_DIR,
12
+ runId: process.env.LATTICE_DASHBOARD_LIFECYCLE_RUN });
13
+ } catch (error) {
14
+ const code = /^[A-Z_]+$/u.test(error.code ?? '') ? error.code : 'DASHBOARD_SUPERVISOR_FAILED';
15
+ if (process.connected) process.send({ type: 'supervisor_error', code }, () => process.disconnect());
16
+ process.stderr.write(`${JSON.stringify({ code })}\n`);
17
+ process.exitCode = 1;
18
+ }
@@ -14,8 +14,11 @@ import {
14
14
  createTodoGanttProjectRegistry,
15
15
  startTodoGanttDashboardServer,
16
16
  } from '../src/todo-gantt-live.mjs';
17
+ import { dashboardLifecyclePhase } from '../src/dashboard-lifecycle-ipc.mjs';
17
18
 
18
19
  const args = process.argv.slice(2);
20
+ if (args.length === 2 && args[0] === '--supervised-runtime' && process.connected
21
+ && args[1] === process.env.LATTICE_DASHBOARD_RUNTIME_DIR) args.splice(0, 2);
19
22
  if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
20
23
  process.stdout.write('Usage: lattice-dashboard\n');
21
24
  process.exit(0);
@@ -40,8 +43,10 @@ const storeCache = createTodoStoreCache();
40
43
  const readCachedStore = (repoRoot) => storeCache.read(repoRoot);
41
44
 
42
45
  async function synchronize() {
46
+ dashboardLifecyclePhase('registry');
43
47
  const active = await readVisibleTodoDashboardProjects({ env,
44
48
  projectHasActiveRun: async (entry) => {
49
+ dashboardLifecyclePhase('store');
45
50
  try {
46
51
  const store = await readCachedStore(entry.repo_root);
47
52
  const active = projectTodoStatus(store, TODO_STATUS_DISPATCH_ONLY).active_set.length > 0
@@ -51,8 +56,10 @@ async function synchronize() {
51
56
  } catch (error) {
52
57
  if (!reportedStoreReadFailures.has(entry.project_id)) {
53
58
  reportedStoreReadFailures.add(entry.project_id);
54
- console.error(JSON.stringify({ schema: 'lattice.dashboard_project_store_error.v1',
55
- project_id: entry.project_id, message: error instanceof Error ? error.message : String(error) }));
59
+ // This finite stage survives in the parent's receipt without project/error text.
60
+ // A lost supervisor leaves no stderr reader; do not create an EPIPE failure
61
+ // in an otherwise serving daemon by writing to its abandoned pipe.
62
+ dashboardLifecyclePhase('store_read_failed');
56
63
  }
57
64
  return false;
58
65
  }
@@ -103,28 +110,43 @@ let closing = null;
103
110
  */
104
111
  function close() {
105
112
  if (closing === null) {
113
+ dashboardLifecyclePhase('close');
106
114
  clearInterval(timer);
107
115
  closing = (async () => {
108
- try { await forgetTodoDashboardDaemonRecord({ env }); } catch {}
109
- await dashboard.close();
116
+ let cleanupFailure = false;
117
+ try { await forgetTodoDashboardDaemonRecord({ env }); }
118
+ catch { cleanupFailure = true; dashboardLifecyclePhase('cleanup_failed'); }
119
+ try { await dashboard.close(); }
120
+ catch (error) { dashboardLifecyclePhase('close_failed'); throw error; }
121
+ if (cleanupFailure) throw new Error('dashboard record cleanup failed');
110
122
  })();
111
123
  }
112
124
  return closing;
113
125
  }
114
126
 
115
127
  function exit(code) {
116
- close().catch(() => {}).finally(() => process.exit(code));
128
+ close().then(() => process.exit(code), () => process.exit(code === 0 ? 1 : code));
117
129
  }
118
130
 
119
131
  await synchronize();
132
+ dashboardLifecyclePhase('listen');
120
133
  const dashboard = await startTodoGanttDashboardServer({
121
134
  registry,
122
135
  port,
123
- onShutdown: process.platform === 'win32' ? () => exit(0) : null,
136
+ onShutdown: process.platform === 'win32' ? () => {
137
+ dashboardLifecyclePhase('shutdown_request'); exit(0);
138
+ } : null,
124
139
  });
125
140
  await writeTodoDashboardDaemonRecord({ port: dashboard.port, env });
141
+ dashboardLifecyclePhase('ready');
126
142
  const timer = setInterval(() => synchronize().then((count) => {
127
- if (count === 0) exit(0);
128
- }).catch(() => exit(1)), 1_000);
129
- process.once('SIGINT', () => exit(0));
130
- process.once('SIGTERM', () => exit(0));
143
+ dashboardLifecyclePhase('periodic_sync');
144
+ if (count === 0) { dashboardLifecyclePhase('empty_shutdown'); exit(0); }
145
+ }).catch(() => { dashboardLifecyclePhase('sync_failed'); exit(1); }), 1_000);
146
+ process.on('message', (message) => {
147
+ if (message?.type === 'dashboard_shutdown') {
148
+ dashboardLifecyclePhase('shutdown_request'); exit(0);
149
+ }
150
+ });
151
+ process.once('SIGINT', () => { dashboardLifecyclePhase('shutdown_request'); exit(0); });
152
+ process.once('SIGTERM', () => { dashboardLifecyclePhase('shutdown_request'); exit(0); });
@@ -119,6 +119,35 @@ bridgeが無効な間は以下すべてnullで、bridgeを有効にしている
119
119
  | `node_path`・`node_version` | そのprocessを実行しているNodeの実体pathと版 |
120
120
  | `bridge_path` | そのprocessが実行しているbridge script |
121
121
 
122
+ **`dashboard_lifecycle`** — `bridge status --json`の読み取り専用の終了記録。
123
+ `lattice.dashboard_lifecycle_status.v1`の`last_ensure`に起動・再利用・無応答拒否の判断、
124
+ `active`に監視中のrun、`last_termination`に最後の実終了または監督消失を返す。
125
+ 読み取りでdaemonを起動したり欠測を補完したりしない。未導入の旧daemonには記録が無い。
126
+
127
+ 通常のdashboard起動は小さいsupervisorが子をwaitし、実終了の観測時刻・codeまたはsignal・最後の段階を
128
+ 所有runtimeの`lifecycle/`へ保存する。POSIXはprivate directory/file、Windowsは既存storeの所有境界を使う。
129
+ 64run、各16KiB・64段階イベントまでとし、activeや生存中の監視外childは削除しない。閉じた古いrunから
130
+ 整理し、満杯なら記録を捨てて起動を続けずtyped容量エラーで止める。stderr本文やproject本文は保存せず、
131
+ byte数と先頭64KiBまでのdigestを残す。中断された一時fileも上限内で扱い、所有writerの死亡確認後に除去する。
132
+ Windowsで記録の置換が一時的に拒否された場合は、所有pathを再検証しながら短い期限内だけ公開を待つ。
133
+ 解除されなければ元のfilesystem errorを伴う`DASHBOARD_LIFECYCLE_PUBLISH_FAILED`を返し、旧完全記録を保持する。
134
+
135
+ Windowsの`observed_exit`は実waitで得たDWORDの`uint32`と`hex`を保持し、signalは`not_applicable`とする。
136
+ `0xC0000005`等の値だけでは例外と外部TerminateProcessを区別できない。`termination_request`は自分のIPC終了要求と、
137
+ FILETIMEで認証したhandleへのTerminateProcess要求・実GetExitCodeProcess結果を別に記録する。
138
+ Nodeの疑似SIGTERMしか残らない場合はraw値を得た扱いにしない。
139
+
140
+ supervisorだけがKILLなどで消えた場合、childは残して配信を継続する。Windowsではchildをdetachedで起動し、
141
+ supervisorが所有するJobの終了に連動させない。通常の停止要求はIPCで送り、実childの終了をwaitする。
142
+ 次の通常ensureはchildのhealthと
143
+ identityを確認し、同じdaemonを再利用する。監督消失後の過去の終了値・終了時刻はunknownのまま残す。
144
+ OS再起動などでsupervisorとchildの両方が消えた場合も、観測できなかった終了値や理由はunknownのまま残す。
145
+ 周期LaunchAgentは従来のstableなCLI pathでensureするままで、plist・Node・信用設定の書換えは要らない。
146
+
147
+ 配信失敗のreceiptには、`reason`のtyped codeに加え、許可済みの`failure.stage`と`failure.subreason`が入る。
148
+ 段階は`heartbeat_registration`、`hub_listing`、`project_route`、欠測時の`unknown`であり、任意のerror detailや
149
+ messageは写さない。配信確認の失敗は、daemonの死亡を示す判定には使わない。
150
+
122
151
  **`runtime_drift`** — 両者の食い違い。空配列は「差が無い」または「`runtime`が名乗っていないので
123
152
  判定できない」のどちらかである。
124
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.79.2",
3
+ "version": "0.79.4",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/kitepon/Lattice.git"
14
14
  },
15
15
  "engines": {
16
- "node": ">=22.13 <25 || >=26"
16
+ "node": ">=22.13 <24.2 || >=24.22 <25 || >=26.10"
17
17
  },
18
18
  "bin": {
19
19
  "lattice": "bin/lattice.mjs",
@@ -125,32 +125,12 @@ async function loadLatticeSensor() {
125
125
  // which fails for ESM-only packages. This bypasses the transformation.
126
126
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
127
127
  const importESM = new Function('specifier', 'return import(specifier)');
128
- // Block LatticeSensor on Node.js 25.x — V8's turboshaft WASM JIT has a Zone
129
- // allocator bug that reliably crashes when compiling tree-sitter
130
- // grammars (see #54, #81, #140). The previous behaviour was a soft
131
- // console.warn that scrolls off-screen before the OOM crash 30 seconds
132
- // later, leading to a steady stream of "what is this OOM" reports.
133
- // Hard-exit before any WASM work; allow override via env var for users
134
- // who patched V8 themselves or want to test a future fix.
135
- const nodeVersion = process.versions.node;
136
- const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
137
- if ((0, node_version_check_1.isNode25Affected)(nodeMajor)) {
138
- process.stderr.write((0, node_version_check_1.buildNode25BlockBanner)(nodeVersion) + '\n');
139
- if (!process.env.LATTICE_SENSOR_ALLOW_UNSAFE_NODE) {
140
- process.exit(1);
141
- }
142
- // Override active — banner shown for visibility, continuing.
143
- }
144
- // Enforce the supported Node floor. `engines` in package.json only *warns* on
145
- // install (unless engine-strict), so hard-block here to actually keep users off
146
- // unsupported versions. Mirrors the Node 25 block above. See package.json `engines`.
147
- if (nodeMajor < node_version_check_1.MIN_NODE_MAJOR) {
148
- process.stderr.write((0, node_version_check_1.buildNodeTooOldBanner)(nodeVersion) + '\n');
149
- if (!process.env.LATTICE_SENSOR_ALLOW_UNSAFE_NODE) {
150
- process.exit(1);
151
- }
152
- // Override active — banner shown for visibility, continuing.
153
- }
128
+ // Keep the CLI and MCP bootstrap on the same runtime compatibility policy.
129
+ const runtimeSupport = (0, node_version_check_1.evaluateNodeVersionGuard)(process.versions.node, Boolean(process.env.LATTICE_SENSOR_ALLOW_UNSAFE_NODE));
130
+ if (runtimeSupport.banner !== null)
131
+ process.stderr.write(runtimeSupport.banner + '\n');
132
+ if (runtimeSupport.blocked)
133
+ process.exit(1);
154
134
  // Re-exec with V8's `--liftoff-only` if it isn't already set, so tree-sitter's
155
135
  // large WASM grammars never hit the turboshaft Zone OOM (`Fatal process out of
156
136
  // memory: Zone`) on Node >= 22. No-op under the bundled launcher, which already
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Node.js version compatibility check.
2
+ * Shared Node.js compatibility policy for CLI and MCP bootstrap.
3
+ * Includes the Node cppgc Realm lifetime repair and the existing Node25 exclusion.
3
4
  *
4
5
  * Node 25.x has a V8 turboshaft WASM JIT Zone allocator bug that
5
6
  * reliably crashes LatticeSensor with `Fatal process out of memory: Zone`
@@ -36,4 +37,12 @@ export declare const MIN_NODE_MAJOR = 20;
36
37
  * (see ../ui/glyphs.ts for the rationale).
37
38
  */
38
39
  export declare function buildNodeTooOldBanner(nodeVersion: string): string;
40
+ /** Node's cppgc Realm lifetime repair: nodejs/node#65778 (24.x backport#83b3431c). */
41
+ export declare function isNodeRealmLifetimeAffected(nodeVersion: string): boolean;
42
+ export declare function buildNodeRealmLifetimeBanner(nodeVersion: string): string;
43
+ /** Shared bootstrap policy for the sensor CLI and the packaged MCP entry. */
44
+ export declare function evaluateNodeVersionGuard(nodeVersion: string, overrideActive: boolean): {
45
+ blocked: boolean;
46
+ banner: string | null;
47
+ };
39
48
  //# sourceMappingURL=node-version-check.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-version-check.d.ts","sourceRoot":"","sources":["../../src/bin/node-version-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAmBlE;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAkBjE"}
1
+ {"version":3,"file":"node-version-check.d.ts","sourceRoot":"","sources":["../../src/bin/node-version-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAmBlE;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAkBjE;AAED,sFAAsF;AACtF,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAMxE;AAED,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAiBxE;AAED,6EAA6E;AAC7E,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAOlI"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
- * Node.js version compatibility check.
3
+ * Shared Node.js compatibility policy for CLI and MCP bootstrap.
4
+ * Includes the Node cppgc Realm lifetime repair and the existing Node25 exclusion.
4
5
  *
5
6
  * Node 25.x has a V8 turboshaft WASM JIT Zone allocator bug that
6
7
  * reliably crashes LatticeSensor with `Fatal process out of memory: Zone`
@@ -13,6 +14,9 @@ exports.MIN_NODE_MAJOR = void 0;
13
14
  exports.buildNode25BlockBanner = buildNode25BlockBanner;
14
15
  exports.isNode25Affected = isNode25Affected;
15
16
  exports.buildNodeTooOldBanner = buildNodeTooOldBanner;
17
+ exports.isNodeRealmLifetimeAffected = isNodeRealmLifetimeAffected;
18
+ exports.buildNodeRealmLifetimeBanner = buildNodeRealmLifetimeBanner;
19
+ exports.evaluateNodeVersionGuard = evaluateNodeVersionGuard;
16
20
  /**
17
21
  * Build the bordered banner shown when LatticeSensor detects an
18
22
  * unsupported Node.js major version (Node 25.x). Pinned via unit
@@ -81,4 +85,42 @@ function buildNodeTooOldBanner(nodeVersion) {
81
85
  sep,
82
86
  ].join('\n');
83
87
  }
88
+ /** Node's cppgc Realm lifetime repair: nodejs/node#65778 (24.x backport#83b3431c). */
89
+ function isNodeRealmLifetimeAffected(nodeVersion) {
90
+ const parts = nodeVersion.split('.');
91
+ const major = Number(parts[0]);
92
+ const minor = Number(parts[1]);
93
+ return (major === 24 && minor >= 2 && minor < 22)
94
+ || (major === 26 && minor < 10);
95
+ }
96
+ function buildNodeRealmLifetimeBanner(nodeVersion) {
97
+ const sep = '-'.repeat(72);
98
+ return [
99
+ sep,
100
+ `[LatticeSensor] Unsupported Node.js version: ${nodeVersion}`,
101
+ sep,
102
+ 'This Node.js release has a cppgc wrapper/Realm lifetime defect.',
103
+ 'Worker teardown can write to freed memory and corrupt the native heap.',
104
+ 'Upstream repair: https://github.com/nodejs/node/pull/65778',
105
+ '',
106
+ 'Fix: use Node.js 24.22.0+ on the 24.x line or 26.10.0+ on the 26.x line.',
107
+ 'Supported Node.js 22 LTS is also outside this affected implementation.',
108
+ '',
109
+ 'For a custom patched runtime only, the existing override is:',
110
+ ' LATTICE_SENSOR_ALLOW_UNSAFE_NODE=1 lattice sensor ...',
111
+ sep,
112
+ ].join('\n');
113
+ }
114
+ /** Shared bootstrap policy for the sensor CLI and the packaged MCP entry. */
115
+ function evaluateNodeVersionGuard(nodeVersion, overrideActive) {
116
+ const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
117
+ let banner = null;
118
+ if (isNode25Affected(nodeMajor))
119
+ banner = buildNode25BlockBanner(nodeVersion);
120
+ else if (nodeMajor < exports.MIN_NODE_MAJOR)
121
+ banner = buildNodeTooOldBanner(nodeVersion);
122
+ else if (isNodeRealmLifetimeAffected(nodeVersion))
123
+ banner = buildNodeRealmLifetimeBanner(nodeVersion);
124
+ return { blocked: banner !== null && !overrideActive, banner };
125
+ }
84
126
  //# sourceMappingURL=node-version-check.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-version-check.js","sourceRoot":"","sources":["../../src/bin/node-version-check.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAWH,wDAmBC;AAGD,4CAEC;AAmBD,sDAkBC;AAtED;;;;;;;;GAQG;AACH,SAAgB,sBAAsB,CAAC,WAAmB;IACxD,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,OAAO;QACL,GAAG;QACH,gDAAgD,WAAW,EAAE;QAC7D,GAAG;QACH,qEAAqE;QACrE,qEAAqE;QACrE,sEAAsE;QACtE,wEAAwE;QACxE,EAAE;QACF,8BAA8B;QAC9B,+DAA+D;QAC/D,6EAA6E;QAC7E,EAAE;QACF,sDAAsD;QACtD,yDAAyD;QACzD,GAAG;KACJ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,wDAAwD;AACxD,SAAgB,gBAAgB,CAAC,SAAiB;IAChD,OAAO,SAAS,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACU,QAAA,cAAc,GAAG,EAAE,CAAC;AAEjC;;;;;;;GAOG;AACH,SAAgB,qBAAqB,CAAC,WAAmB;IACvD,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,OAAO;QACL,GAAG;QACH,gDAAgD,WAAW,EAAE;QAC7D,GAAG;QACH,kCAAkC,sBAAc,gCAAgC;QAChF,yEAAyE;QACzE,sBAAsB;QACtB,EAAE;QACF,8BAA8B;QAC9B,+DAA+D;QAC/D,6EAA6E;QAC7E,EAAE;QACF,8CAA8C;QAC9C,yDAAyD;QACzD,GAAG;KACJ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"node-version-check.js","sourceRoot":"","sources":["../../src/bin/node-version-check.ts"],"names":[],"mappings":"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"}
@@ -105,6 +105,10 @@ export declare class ParseWorkerPool {
105
105
  private nextId;
106
106
  private totalCrashes;
107
107
  private destroyed;
108
+ private readonly lifecycle;
109
+ private readonly retiring;
110
+ private readonly retirement;
111
+ private destroying?;
108
112
  private readonly languages;
109
113
  private readonly maxSize;
110
114
  private readonly recycleInterval;
@@ -161,7 +165,10 @@ export declare class ParseWorkerPool {
161
165
  * this before its retry pass so crash-on-memory files get the cleanest heap.
162
166
  */
163
167
  recycleAll(): void;
164
- /** Terminate all workers and reject any outstanding parses. */
168
+ /** Request natural exit once grammars have loaded; force only a wedged worker. */
169
+ private stopWorker;
170
+ private waitForShutdown;
171
+ /** Close all workers and reject outstanding parses. Also join prior recycling. */
165
172
  destroy(): Promise<void>;
166
173
  }
167
174
  //# sourceMappingURL=parse-pool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse-pool.d.ts","sourceRoot":"","sources":["../../src/extraction/parse-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACpC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IACrD,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACjD,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;CACrD;AAED;uFACuF;AACvF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAuCD;;;;;;;GAOG;AACH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMxE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAGlF;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASzF;AAyBD,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,eAAe,CAAC;IACrC,wEAAwE;IACxE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC7C;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,OAAO,CAA8B;IAG7C,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,WAAW,CAAsC;IACzD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwB;IACrD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAA6B;gBAEjD,IAAI,EAAE,sBAAsB;IAwBxC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAQf,mCAAmC;IACnC,IAAI,IAAI,IAAI,MAAM,CAAyB;IAE3C,qCAAqC;IACrC,IAAI,WAAW,IAAI,MAAM,CAA8B;IAEvD,mEAAmE;IACnE,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQxD,OAAO,CAAC,QAAQ;IAqBhB,OAAO,CAAC,SAAS;IAwCjB;4EACwE;IACxE,OAAO,CAAC,YAAY;IAYpB;yDACqD;IACrD,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,QAAQ;IAmBhB;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;IASjB,6EAA6E;IAC7E,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,KAAK;IA8Bb,OAAO,CAAC,MAAM;IASd;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB,+DAA+D;IACzD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAe/B"}
1
+ {"version":3,"file":"parse-pool.d.ts","sourceRoot":"","sources":["../../src/extraction/parse-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACpC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IACrD,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACjD,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;CACrD;AAED;uFACuF;AACvF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAiDD;;;;;;;GAOG;AACH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMxE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAGlF;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASzF;AAyBD,MAAM,WAAW,sBAAsB;IACrC,+DAA+D;IAC/D,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,eAAe,CAAC;IACrC,wEAAwE;IACxE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC7C;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,OAAO,CAA8B;IAG7C,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,WAAW,CAAsC;IACzD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmD;IAC7E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiD;IAC5E,OAAO,CAAC,UAAU,CAAC,CAAgB;IAEnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwB;IACrD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAA6B;gBAEjD,IAAI,EAAE,sBAAsB;IAwBxC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAQf,mCAAmC;IACnC,IAAI,IAAI,IAAI,MAAM,CAAyB;IAE3C,qCAAqC;IACrC,IAAI,WAAW,IAAI,MAAM,CAA8B;IAEvD,mEAAmE;IACnE,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQxD,OAAO,CAAC,QAAQ;IAkChB,OAAO,CAAC,SAAS;IAyCjB;4EACwE;IACxE,OAAO,CAAC,YAAY;IAYpB;yDACqD;IACrD,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,QAAQ;IAmBhB;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;IASjB,6EAA6E;IAC7E,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,KAAK;IA8Bb,OAAO,CAAC,MAAM;IASd;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB,kFAAkF;IAClF,OAAO,CAAC,UAAU;YAUJ,eAAe;IA4B7B,kFAAkF;IAClF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAiBzB"}
@@ -69,6 +69,8 @@ const MAX_CONCURRENT_SPAWN = 2;
69
69
  * orchestrator's retry pass and shouldn't trip this on a merely-crashy repo.
70
70
  */
71
71
  const CRASH_BUDGET = 100;
72
+ /** Startup plus orderly shutdown deadline; wedged workers still have a backstop. */
73
+ const SHUTDOWN_TIMEOUT_MS = 15_000;
72
74
  /**
73
75
  * Resolve the pool size from the `LATTICE_SENSOR_PARSE_WORKERS` override and the
74
76
  * machine's core count.
@@ -122,6 +124,10 @@ class ParseWorkerPool {
122
124
  nextId = 1;
123
125
  totalCrashes = 0;
124
126
  destroyed = false;
127
+ lifecycle = new WeakMap();
128
+ retiring = new Set();
129
+ retirement = new WeakMap();
130
+ destroying;
125
131
  languages;
126
132
  maxSize;
127
133
  recycleInterval;
@@ -204,10 +210,23 @@ class ParseWorkerPool {
204
210
  this.workers.add(w);
205
211
  this.pending.add(w);
206
212
  this.parseCounts.set(w, 0);
213
+ let started;
214
+ let gone;
215
+ const life = {
216
+ ready: new Promise((resolve) => { started = resolve; }),
217
+ exited: new Promise((resolve) => { gone = resolve; }),
218
+ started: () => started(), gone: () => gone(), didExit: false,
219
+ };
220
+ this.lifecycle.set(w, life);
207
221
  w.on('message', (m) => this.onMessage(w, (m ?? {})));
208
222
  w.on('error', (e) => this.onWorkerGone(w, `Worker error: ${e?.message ?? 'unknown'}`));
209
- w.on('exit', (code) => { if (code !== 0)
210
- this.onWorkerGone(w, `Worker exited with code ${code}`); });
223
+ w.on('exit', (code) => {
224
+ life.didExit = true;
225
+ life.started();
226
+ life.gone();
227
+ if (code !== 0)
228
+ this.onWorkerGone(w, `Worker exited with code ${code}`);
229
+ });
211
230
  // Load grammars; the worker replies 'grammars-loaded' and only then is idle.
212
231
  // Pre-read WASM bytes (when the orchestrator provided them) make this a
213
232
  // memory load instead of a per-spawn disk read.
@@ -215,6 +234,7 @@ class ParseWorkerPool {
215
234
  }
216
235
  onMessage(w, m) {
217
236
  if (m.type === 'grammars-loaded') {
237
+ this.lifecycle.get(w)?.started();
218
238
  if (!this.workers.has(w))
219
239
  return; // recycled/destroyed before ready
220
240
  this.pending.delete(w);
@@ -279,11 +299,9 @@ class ParseWorkerPool {
279
299
  recycle(w) {
280
300
  this.log(`Recycling worker after ${this.parseCounts.get(w)} parses (heap: ${Math.round(process.memoryUsage().rss / 1024 / 1024)}MB RSS)`);
281
301
  this.removeWorker(w);
282
- // Fire-and-forget: worker.terminate() can hang if WASM is wedged.
283
- try {
284
- void w.terminate();
285
- }
286
- catch { /* already gone */ }
302
+ // An idle worker can close its port and let Node finish outstanding native
303
+ // work. Windows crash captures coincide with forced V8 teardown here.
304
+ void this.stopWorker(w);
287
305
  if (this.healthy && !this.destroyed)
288
306
  this.spawnOne();
289
307
  }
@@ -399,10 +417,60 @@ class ParseWorkerPool {
399
417
  for (const w of [...this.idle])
400
418
  this.recycle(w);
401
419
  }
402
- /** Terminate all workers and reject any outstanding parses. */
403
- async destroy() {
404
- if (this.destroyed)
405
- return;
420
+ /** Request natural exit once grammars have loaded; force only a wedged worker. */
421
+ stopWorker(w) {
422
+ const existing = this.retirement.get(w);
423
+ if (existing)
424
+ return existing;
425
+ const stopping = this.waitForShutdown(w);
426
+ this.retirement.set(w, stopping);
427
+ this.retiring.add(stopping);
428
+ void stopping.finally(() => this.retiring.delete(stopping));
429
+ return stopping;
430
+ }
431
+ async waitForShutdown(w) {
432
+ const life = this.lifecycle.get(w);
433
+ let timer;
434
+ const force = async () => {
435
+ if (!life.didExit) {
436
+ try {
437
+ await w.terminate();
438
+ }
439
+ catch { /* already gone */ }
440
+ }
441
+ };
442
+ const orderly = life.ready.then(async () => {
443
+ if (!life.didExit) {
444
+ try {
445
+ w.postMessage({ type: 'shutdown' });
446
+ }
447
+ catch {
448
+ await force();
449
+ return;
450
+ }
451
+ }
452
+ await life.exited;
453
+ });
454
+ try {
455
+ await Promise.race([
456
+ orderly,
457
+ new Promise((resolve) => {
458
+ timer = setTimeout(() => {
459
+ this.log(`Parse worker did not exit after ${SHUTDOWN_TIMEOUT_MS}ms — forcing termination`);
460
+ void force().then(resolve);
461
+ }, SHUTDOWN_TIMEOUT_MS);
462
+ }),
463
+ ]);
464
+ }
465
+ finally {
466
+ if (timer)
467
+ clearTimeout(timer);
468
+ }
469
+ }
470
+ /** Close all workers and reject outstanding parses. Also join prior recycling. */
471
+ destroy() {
472
+ if (this.destroying)
473
+ return this.destroying;
406
474
  this.destroyed = true;
407
475
  const ws = [...this.workers];
408
476
  this.workers.clear();
@@ -414,7 +482,10 @@ class ParseWorkerPool {
414
482
  }
415
483
  this.inflight.clear();
416
484
  this.queue = [];
417
- await Promise.all(ws.map((w) => Promise.resolve(w.terminate()).catch(() => { })));
485
+ for (const w of ws)
486
+ this.stopWorker(w);
487
+ this.destroying = Promise.all([...this.retiring]).then(() => { });
488
+ return this.destroying;
418
489
  }
419
490
  }
420
491
  exports.ParseWorkerPool = ParseWorkerPool;