@quolu/lattice 0.79.3 → 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.
- package/README.md +8 -2
- package/bin/lattice-bridge.mjs +31 -24
- package/bin/lattice-dashboard-supervisor.mjs +18 -0
- package/bin/lattice-dashboard.mjs +32 -10
- package/docs/bridge-setup.md +29 -0
- package/package.json +2 -2
- package/sensor/dist/bin/lattice-sensor.js +6 -26
- package/sensor/dist/bin/node-version-check.d.ts +10 -1
- package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
- package/sensor/dist/bin/node-version-check.js +43 -1
- package/sensor/dist/bin/node-version-check.js.map +1 -1
- package/sensor/dist/mcp/daemon-paths.d.ts +4 -0
- package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-paths.js +12 -0
- package/sensor/dist/mcp/daemon-paths.js.map +1 -1
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +52 -28
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/package.json +1 -1
- package/src/bridge-cli.mjs +6 -2
- package/src/bridge-config.mjs +8 -0
- package/src/bridge-daemon.mjs +30 -2
- package/src/dashboard-delivery.mjs +14 -2
- package/src/dashboard-lifecycle-ipc.mjs +5 -0
- package/src/dashboard-lifecycle-journal.mjs +235 -0
- package/src/dashboard-process-control.mjs +74 -0
- package/src/dashboard-supervisor.mjs +180 -0
- package/src/dashboard-windows-process-control.ps1 +40 -0
- package/src/factory-diagnostics.mjs +22 -9
- package/src/node-version-guard.mjs +3 -42
- package/src/todo-dashboard-registry.mjs +24 -3
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
|
|
163
|
-
|
|
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登録、
|
package/bin/lattice-bridge.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { readBridgeConfig } from '../src/bridge-config.mjs';
|
|
4
4
|
import {
|
|
5
|
-
bridgeDaemonVersionDrifted,
|
|
6
|
-
|
|
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 =
|
|
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 =
|
|
41
|
-
if (closing)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
178
|
-
process.
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
109
|
-
await
|
|
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().
|
|
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' ? () =>
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
process.
|
|
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); });
|
package/docs/bridge-setup.md
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1
|
+
{"version":3,"file":"node-version-check.js","sourceRoot":"","sources":["../../src/bin/node-version-check.ts"],"names":[],"mappings":"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"}
|
|
@@ -59,6 +59,10 @@ export interface DaemonLockInfo {
|
|
|
59
59
|
socketPath: string;
|
|
60
60
|
startedAt: number;
|
|
61
61
|
}
|
|
62
|
+
/** The election and its clients must agree how long an observed startup is protected. */
|
|
63
|
+
export declare const DAEMON_STARTUP_GRACE_MS = 10000;
|
|
64
|
+
/** Time component only: callers still verify liveness and the socket hello. */
|
|
65
|
+
export declare function daemonStartupDeadline(info: Pick<DaemonLockInfo, 'startedAt'> | null, now?: number): number | null;
|
|
62
66
|
/** Whether a lock record contains enough identity data for a socket hello. */
|
|
63
67
|
export declare function canProbeDaemonIdentity(info: DaemonLockInfo): boolean;
|
|
64
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-paths.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA8BH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAUvE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAOpE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CA0C7F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAuBjE"}
|
|
1
|
+
{"version":3,"file":"daemon-paths.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA8BH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAUvE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,yFAAyF;AACzF,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,GAAG,IAAI,EAAE,GAAG,SAAa,GAAG,MAAM,GAAG,IAAI,CAKrH;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAOpE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CA0C7F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAuBjE"}
|
|
@@ -62,9 +62,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
62
62
|
};
|
|
63
63
|
})();
|
|
64
64
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
65
|
+
exports.DAEMON_STARTUP_GRACE_MS = void 0;
|
|
65
66
|
exports.getDaemonSocketCandidates = getDaemonSocketCandidates;
|
|
66
67
|
exports.getDaemonSocketPath = getDaemonSocketPath;
|
|
67
68
|
exports.getDaemonPidPath = getDaemonPidPath;
|
|
69
|
+
exports.daemonStartupDeadline = daemonStartupDeadline;
|
|
68
70
|
exports.canProbeDaemonIdentity = canProbeDaemonIdentity;
|
|
69
71
|
exports.probeDaemonIdentity = probeDaemonIdentity;
|
|
70
72
|
exports.encodeLockInfo = encodeLockInfo;
|
|
@@ -134,6 +136,16 @@ function getDaemonSocketPath(projectRoot) {
|
|
|
134
136
|
function getDaemonPidPath(projectRoot) {
|
|
135
137
|
return path.join((0, directory_1.getLatticeSensorDir)(projectRoot), 'daemon.pid');
|
|
136
138
|
}
|
|
139
|
+
/** The election and its clients must agree how long an observed startup is protected. */
|
|
140
|
+
exports.DAEMON_STARTUP_GRACE_MS = 10_000;
|
|
141
|
+
/** Time component only: callers still verify liveness and the socket hello. */
|
|
142
|
+
function daemonStartupDeadline(info, now = Date.now()) {
|
|
143
|
+
const startedAt = info?.startedAt;
|
|
144
|
+
if (typeof startedAt !== 'number' || !Number.isFinite(startedAt) || startedAt <= 0
|
|
145
|
+
|| !Number.isFinite(now) || now < startedAt || now - startedAt >= exports.DAEMON_STARTUP_GRACE_MS)
|
|
146
|
+
return null;
|
|
147
|
+
return startedAt + exports.DAEMON_STARTUP_GRACE_MS;
|
|
148
|
+
}
|
|
137
149
|
/** Whether a lock record contains enough identity data for a socket hello. */
|
|
138
150
|
function canProbeDaemonIdentity(info) {
|
|
139
151
|
return (Number.isInteger(info.pid) &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-paths.js","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG
|
|
1
|
+
{"version":3,"file":"daemon-paths.js","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAiKH;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,CAO7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAIvF;AAgFD;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,WAAW,CAAgB;IAGnC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAA+C;IAGnE,OAAO,CAAC,gBAAgB,CAA+B;IAIvD,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAA4D;IAE5E,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAA4D;IACxE,sFAAsF;IACtF,OAAO,CAAC,cAAc,CAAuB;gBAEjC,WAAW,CAAC,EAAE,MAAM;IAIhC;;;;;;;;;;;;OAYG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmE5B;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B3B,uEAAuE;YACzD,WAAW;IA4DzB;;;;;;;;OAQG;YACW,kBAAkB;IAiEhC;;;;;;;OAOG;YACW,0BAA0B;IAkExC,iFAAiF;IACjF,OAAO,CAAC,qBAAqB;IAK7B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAoB5B;AAWD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,2BAA2B,EAAE,MAAM,WAAW,CAAC"}
|
package/sensor/dist/mcp/index.js
CHANGED
|
@@ -151,21 +151,26 @@ function makeFallbackEngine(root) {
|
|
|
151
151
|
}
|
|
152
152
|
return new engine_1.MCPEngine({ writerLockRoot: root, queryPool: true, queryPoolDefaultMax: DIRECT_QUERY_POOL_MAX });
|
|
153
153
|
}
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
* common path the socket appears within a few rounds.
|
|
160
|
-
*/
|
|
161
|
-
// Poll finely (25ms) so the proxy attaches the instant the freshly-spawned
|
|
162
|
-
// daemon binds, instead of waiting up to a coarse 100ms after — shaves the
|
|
163
|
-
// cold-start handshake (the window the headless agent races). Same ~6s total
|
|
164
|
-
// give-up budget (240 × 25ms), just finer granularity; socket-connect probes
|
|
165
|
-
// are cheap. Paired with deferring the LatticeSensor load (engine.ts) off the bind
|
|
166
|
-
// path, this narrows the "No such tool available" race window.
|
|
154
|
+
/** Initial socket polling budget. A positively observed live same-version
|
|
155
|
+
* startup owner may receive the remainder of its original shared startup grace
|
|
156
|
+
* below; absent, expired, or untrusted owners do not extend this budget. Only a
|
|
157
|
+
* verified hello establishes readiness. */
|
|
158
|
+
// Fine polling attaches promptly without renewing the owner's fixed deadline.
|
|
167
159
|
const DAEMON_CONNECT_MAX_RETRIES = 240;
|
|
168
160
|
const DAEMON_CONNECT_RETRY_DELAY_MS = 25;
|
|
161
|
+
function observeStartupLock(root) {
|
|
162
|
+
try {
|
|
163
|
+
return (0, daemon_paths_1.decodeLockInfo)(fs.readFileSync((0, daemon_paths_1.getDaemonPidPath)(root), 'utf8'));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const code = error.code;
|
|
167
|
+
if (code === 'ENOENT')
|
|
168
|
+
return null;
|
|
169
|
+
const safeCode = typeof code === 'string' && /^[A-Z0-9_]{1,40}$/.test(code) ? code : 'UNKNOWN';
|
|
170
|
+
process.stderr.write(`[LatticeSensor MCP] Startup owner observation failed (${safeCode}).\n`);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
169
174
|
/** Whether `LATTICE_SENSOR_NO_DAEMON` was set to a truthy value. */
|
|
170
175
|
function daemonOptOutSet() {
|
|
171
176
|
const raw = process.env.LATTICE_SENSOR_NO_DAEMON;
|
|
@@ -551,9 +556,7 @@ class MCPServer {
|
|
|
551
556
|
// Give a newly-elected daemon time to bind, then require its socket hello
|
|
552
557
|
// to match the lock PID/version. PID existence alone accepts an unrelated
|
|
553
558
|
// process after OS PID reuse and permanently wedges startup (#1553).
|
|
554
|
-
const
|
|
555
|
-
const startupGraceMs = 10_000;
|
|
556
|
-
const stillStarting = existing.startedAt > 0 && age >= 0 && age < startupGraceMs;
|
|
559
|
+
const stillStarting = (0, daemon_paths_1.daemonStartupDeadline)(existing) !== null;
|
|
557
560
|
// Legacy plain-PID locks have no socket identity to test. Preserve those
|
|
558
561
|
// live holders: an inconclusive probe is not permission to create a
|
|
559
562
|
// second writer.
|
|
@@ -614,32 +617,53 @@ class MCPServer {
|
|
|
614
617
|
}
|
|
615
618
|
return null;
|
|
616
619
|
};
|
|
620
|
+
const connectOutcome = async () => {
|
|
621
|
+
const socket = await connectAnyCandidate();
|
|
622
|
+
if (socket === 'foreign-product')
|
|
623
|
+
return 'foreign-product';
|
|
624
|
+
if (socket === 'version-mismatch')
|
|
625
|
+
return { fallbackReason: 'version-skew' };
|
|
626
|
+
return socket;
|
|
627
|
+
};
|
|
617
628
|
const getDaemonSocket = async () => {
|
|
618
629
|
// Fast path: a daemon may already be listening (on either candidate).
|
|
619
|
-
const probe = await
|
|
630
|
+
const probe = await connectOutcome();
|
|
620
631
|
// ADR 0049 Decision 5③: a different product's daemon — propagate
|
|
621
632
|
// verbatim so the local-handshake proxy fails the session closed
|
|
622
633
|
// instead of silently falling back to direct mode.
|
|
623
|
-
if (probe === 'foreign-product')
|
|
624
|
-
return 'foreign-product';
|
|
625
|
-
// Same-product version skew — definitive, serve in-process (direct,
|
|
626
|
-
// reason: version-skew), don't poll for 6s.
|
|
627
|
-
if (probe === 'version-mismatch')
|
|
628
|
-
return { fallbackReason: 'version-skew' };
|
|
629
634
|
if (probe)
|
|
630
635
|
return probe;
|
|
631
636
|
// None reachable — spawn one (detached) and poll for its bind.
|
|
632
637
|
spawnDetachedDaemon(root);
|
|
633
638
|
for (let attempt = 0; attempt < DAEMON_CONNECT_MAX_RETRIES; attempt++) {
|
|
634
639
|
await sleep(DAEMON_CONNECT_RETRY_DELAY_MS);
|
|
635
|
-
const s = await
|
|
636
|
-
if (s === 'foreign-product')
|
|
637
|
-
return 'foreign-product';
|
|
638
|
-
if (s === 'version-mismatch')
|
|
639
|
-
return { fallbackReason: 'version-skew' };
|
|
640
|
+
const s = await connectOutcome();
|
|
640
641
|
if (s)
|
|
641
642
|
return s;
|
|
642
643
|
}
|
|
644
|
+
// An elected same-version owner may still be inside the very grace window
|
|
645
|
+
// in which competing candidates are required to leave it alone. Do not
|
|
646
|
+
// permanently degrade its clients before that owner's original deadline.
|
|
647
|
+
const owner = observeStartupLock(root);
|
|
648
|
+
const startupDeadline = owner?.version === version_1.LatticeSensorPackageVersion
|
|
649
|
+
&& (0, daemon_paths_1.canProbeDaemonIdentity)(owner) && (0, daemon_1.isProcessAlive)(owner.pid) ? (0, daemon_paths_1.daemonStartupDeadline)(owner) : null;
|
|
650
|
+
if (owner && startupDeadline !== null) {
|
|
651
|
+
const monotonicDeadline = performance.now() + Math.max(0, startupDeadline - Date.now());
|
|
652
|
+
const maxAttempts = Math.ceil(daemon_paths_1.DAEMON_STARTUP_GRACE_MS / DAEMON_CONNECT_RETRY_DELAY_MS);
|
|
653
|
+
for (let attempt = 0; attempt < maxAttempts && Date.now() < startupDeadline
|
|
654
|
+
&& performance.now() < monotonicDeadline; attempt++) {
|
|
655
|
+
await sleep(Math.min(DAEMON_CONNECT_RETRY_DELAY_MS, Math.max(1, monotonicDeadline - performance.now())));
|
|
656
|
+
const socket = await connectOutcome();
|
|
657
|
+
if (socket)
|
|
658
|
+
return socket;
|
|
659
|
+
const current = observeStartupLock(root);
|
|
660
|
+
// A temporarily missing record neither revokes nor renews the fixed
|
|
661
|
+
// observed wait. It never proves readiness; only the hello above does.
|
|
662
|
+
if (!(0, daemon_1.isProcessAlive)(owner.pid) || (current !== null && (current.pid !== owner.pid
|
|
663
|
+
|| current.startedAt !== owner.startedAt || current.version !== owner.version)))
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
643
667
|
return { fallbackReason: 'no-daemon' }; // never bound — the proxy serves this session in-process
|
|
644
668
|
};
|
|
645
669
|
await (0, proxy_1.runLocalHandshakeProxy)({ getDaemonSocket, makeEngine: () => makeFallbackEngine(root), root });
|