@linzumi/cli 1.0.111 → 1.0.113
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 +1 -1
- package/dist/crossHarnessFailover.mjs +6 -0
- package/dist/index.js +431 -431
- package/package.json +1 -1
- package/scripts/build-core-commander-seams-parity-oracle.mjs +586 -0
- package/scripts/build-core-daemon-parity-oracle.mjs +573 -0
- package/scripts/build-core-dispatch-input-queue-parity-oracle.mjs +219 -0
- package/scripts/build-core-fate-ladder-parity-oracle.mjs +316 -0
- package/scripts/build-core-generation-overlap-parity-oracle.mjs +288 -0
- package/scripts/build-core-integration-parity-oracle.mjs +361 -0
- package/scripts/build-core-lifecycle-guards-parity-oracle.mjs +443 -0
- package/scripts/build-core-lock-parity-oracle.mjs +455 -0
- package/scripts/build-core-reconnect-resume-parity-oracle.mjs +256 -0
- package/scripts/build-core-startup-resume-scan-parity-oracle.mjs +296 -0
- package/scripts/build-core-supervisor-parity-oracle.mjs +734 -0
- package/scripts/build-cross-harness-failover-oracle.mjs +349 -0
- package/scripts/build-differential-entry.mjs +21 -1
- package/scripts/build.mjs +40 -1
- package/scripts/f5_live_cross_harness_failover_e2e.mjs +239 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 10, commander-core / supervisor).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the `connect
|
|
4
|
+
// --supervise` supervisor port (src/supervisor.ts -> ReScript
|
|
5
|
+
// CommanderSupervisor), the sibling of build-pipeline-parity-oracle.mjs
|
|
6
|
+
// (cluster 9). The oracle bundles the REAL supervisor module - the flag
|
|
7
|
+
// helpers, the code fingerprint, the supervisor lock (real fs on a scratch
|
|
8
|
+
// dir, byte-compared records), the console logger + signal disposition, the
|
|
9
|
+
// gone-TTY write-guard slice of ttyTeardownGuard.ts, the runSuperviseLoop
|
|
10
|
+
// refusal legs, and createSupervisor (THE state machine) driven through
|
|
11
|
+
// fully scripted deps: a scripted scheduler/clock (the scriptedTimers
|
|
12
|
+
// pattern), a scripted child-process seam (ops declare exits with
|
|
13
|
+
// codes/signals and IPC frames; spawns/kills/sends land in the trace), and
|
|
14
|
+
// scripted probe/install/fingerprint/reexec seams - behind the one-shot
|
|
15
|
+
// stdin/stdout JSON batch driver. Every supervision run returns the FULL
|
|
16
|
+
// trace (spawn calls with argv/env deltas, signals sent, timer set/clear,
|
|
17
|
+
// log events, store writes) for byte comparison.
|
|
18
|
+
//
|
|
19
|
+
// Determinism: clocks are injected per case; timers fire in scheduling
|
|
20
|
+
// order once the scripted clock passes their due instant, with a full
|
|
21
|
+
// microtask settle between fires and between ops (both drivers settle
|
|
22
|
+
// identically, so async update-check chains interleave identically); the
|
|
23
|
+
// only non-injectable TS timestamp (the lock record's startedAt) is pinned
|
|
24
|
+
// with a scoped global-Date override, the cluster 2 oracle's technique.
|
|
25
|
+
//
|
|
26
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
27
|
+
// argv/env/payloads. The output is a local test artifact (.differential/
|
|
28
|
+
// is gitignored, never published) and stays unminified for debuggability.
|
|
29
|
+
import { mkdir } from 'node:fs/promises';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { build } from 'esbuild';
|
|
33
|
+
|
|
34
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
35
|
+
|
|
36
|
+
const driverSource = `
|
|
37
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
38
|
+
import { tmpdir } from 'node:os';
|
|
39
|
+
import { dirname, join } from 'node:path';
|
|
40
|
+
import {
|
|
41
|
+
acquireSupervisorLock,
|
|
42
|
+
createSupervisor,
|
|
43
|
+
createSupervisorConsoleLogger,
|
|
44
|
+
defaultSupervisorReexecStatePath,
|
|
45
|
+
defaultSupervisorTiming,
|
|
46
|
+
hasSuperviseFlag,
|
|
47
|
+
installSupervisorSignalHandlers,
|
|
48
|
+
isSupervisedChild,
|
|
49
|
+
runSuperviseLoop,
|
|
50
|
+
supervisedChildEnvKey,
|
|
51
|
+
supervisorCodeFingerprint,
|
|
52
|
+
supervisorLockKey,
|
|
53
|
+
supervisorLockPath,
|
|
54
|
+
withoutSuperviseFlags,
|
|
55
|
+
} from './src/supervisor';
|
|
56
|
+
import {
|
|
57
|
+
defaultGenerationStillbornWindowMs,
|
|
58
|
+
generationEpochEnvKey,
|
|
59
|
+
generationHandoffPidEnvKey,
|
|
60
|
+
generationOverlapFlagEnvKey,
|
|
61
|
+
} from './src/generationOverlap';
|
|
62
|
+
import { defaultLinzumiWebSocketUrl } from './src/defaultUrls';
|
|
63
|
+
import {
|
|
64
|
+
goneTtyErrorCode,
|
|
65
|
+
goneTtyWriteErrorCode,
|
|
66
|
+
guardStreamWriteAgainstGoneTty,
|
|
67
|
+
streamGoneTtyWriteCodeForTest,
|
|
68
|
+
} from './src/ttyTeardownGuard';
|
|
69
|
+
|
|
70
|
+
const RealDate = Date;
|
|
71
|
+
|
|
72
|
+
function withPinnedDate(nowMs, fn) {
|
|
73
|
+
class PinnedDate extends RealDate {
|
|
74
|
+
constructor(...args) {
|
|
75
|
+
if (args.length === 0) {
|
|
76
|
+
super(nowMs);
|
|
77
|
+
} else {
|
|
78
|
+
super(...args);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
PinnedDate.now = () => nowMs;
|
|
83
|
+
globalThis.Date = PinnedDate;
|
|
84
|
+
try {
|
|
85
|
+
return fn();
|
|
86
|
+
} finally {
|
|
87
|
+
globalThis.Date = RealDate;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
|
92
|
+
|
|
93
|
+
// --- Scripted scheduler (the scriptedTimers pattern; ids in creation order) ---
|
|
94
|
+
|
|
95
|
+
function scriptedScheduler(clock, trace) {
|
|
96
|
+
let nextId = 1;
|
|
97
|
+
const pending = [];
|
|
98
|
+
return {
|
|
99
|
+
scheduler: {
|
|
100
|
+
setTimeout: (callback, ms) => {
|
|
101
|
+
const id = nextId;
|
|
102
|
+
nextId += 1;
|
|
103
|
+
trace.push({ t: 'timer_set', id, delayMs: ms });
|
|
104
|
+
pending.push({ id, dueAtMs: clock.nowMs + ms, callback });
|
|
105
|
+
return id;
|
|
106
|
+
},
|
|
107
|
+
clearTimeout: (handle) => {
|
|
108
|
+
const index = pending.findIndex((timer) => timer.id === handle);
|
|
109
|
+
if (index >= 0) {
|
|
110
|
+
trace.push({ t: 'timer_clear', id: pending[index].id });
|
|
111
|
+
pending.splice(index, 1);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
fireNextDue: () => {
|
|
116
|
+
const index = pending.findIndex((timer) => timer.dueAtMs <= clock.nowMs);
|
|
117
|
+
if (index < 0) return false;
|
|
118
|
+
const timer = pending.splice(index, 1)[0];
|
|
119
|
+
trace.push({ t: 'timer_fire', id: timer.id });
|
|
120
|
+
timer.callback();
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- The scripted supervision run --------------------------------------------------
|
|
127
|
+
|
|
128
|
+
async function runSupervisionCase(caseInput) {
|
|
129
|
+
// The TS serial drain reads process.platform at drain time (the win32
|
|
130
|
+
// SIGTERM fallback); the ReScript port injects isWin32. Pin the live
|
|
131
|
+
// platform for win32 cases so both impls run the same branch.
|
|
132
|
+
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
133
|
+
if (caseInput.win32 === true) {
|
|
134
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return await runSupervisionCaseInner(caseInput);
|
|
138
|
+
} finally {
|
|
139
|
+
if (caseInput.win32 === true && platformDescriptor !== undefined) {
|
|
140
|
+
Object.defineProperty(process, 'platform', platformDescriptor);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runSupervisionCaseInner(caseInput) {
|
|
146
|
+
const clock = { nowMs: caseInput.startMs ?? 0 };
|
|
147
|
+
const trace = [];
|
|
148
|
+
const timers = scriptedScheduler(clock, trace);
|
|
149
|
+
const children = [];
|
|
150
|
+
const entryBytes = new Map(Object.entries(caseInput.entryBytes ?? {}));
|
|
151
|
+
let probeState = { kind: 'none' };
|
|
152
|
+
let installState = { kind: 'none' };
|
|
153
|
+
let storedAttempt = caseInput.attemptRecord ?? undefined;
|
|
154
|
+
|
|
155
|
+
const initialEntryPath = caseInput.initialEntryPath ?? '/stable/v-base/bin/linzumi.js';
|
|
156
|
+
const childArgs = caseInput.childArgs ?? ['--workspace', 'ws-parity'];
|
|
157
|
+
|
|
158
|
+
const supervisor = createSupervisor({
|
|
159
|
+
childArgs,
|
|
160
|
+
initialEntryPath,
|
|
161
|
+
initialVersion: caseInput.initialVersion ?? '1.0.0',
|
|
162
|
+
ownEntryPath:
|
|
163
|
+
caseInput.ownEntryPath === null
|
|
164
|
+
? undefined
|
|
165
|
+
: (caseInput.ownEntryPath ?? initialEntryPath),
|
|
166
|
+
spawnChild: ({ entryPath, childArgs: spawnArgs, onExit, generation }) => {
|
|
167
|
+
const index = children.length;
|
|
168
|
+
const pid = 4200 + index;
|
|
169
|
+
const record = { index, pid, alive: true, onExit, generation, sendOk: true };
|
|
170
|
+
children.push(record);
|
|
171
|
+
trace.push({
|
|
172
|
+
t: 'spawn',
|
|
173
|
+
index,
|
|
174
|
+
pid,
|
|
175
|
+
entryPath,
|
|
176
|
+
argv: [entryPath, 'connect', ...spawnArgs],
|
|
177
|
+
generation:
|
|
178
|
+
generation === undefined
|
|
179
|
+
? null
|
|
180
|
+
: {
|
|
181
|
+
epoch: generation.epoch,
|
|
182
|
+
handoffFromPid: generation.handoffFromPid ?? null,
|
|
183
|
+
ipc: generation.onMessage !== undefined,
|
|
184
|
+
},
|
|
185
|
+
envDelta:
|
|
186
|
+
generation === undefined
|
|
187
|
+
? { [supervisedChildEnvKey]: '1' }
|
|
188
|
+
: {
|
|
189
|
+
[supervisedChildEnvKey]: '1',
|
|
190
|
+
[generationEpochEnvKey]: String(generation.epoch),
|
|
191
|
+
...(generation.handoffFromPid === undefined
|
|
192
|
+
? {}
|
|
193
|
+
: {
|
|
194
|
+
[generationHandoffPidEnvKey]: String(generation.handoffFromPid),
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
pid,
|
|
200
|
+
kill: (signal) => trace.push({ t: 'kill', child: index, signal }),
|
|
201
|
+
...(generation === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: {
|
|
204
|
+
send: (message) => {
|
|
205
|
+
trace.push({ t: 'send', child: index, message, ok: record.sendOk });
|
|
206
|
+
return record.sendOk;
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
probeLatestVersion: async () => {
|
|
212
|
+
trace.push({ t: 'probe' });
|
|
213
|
+
if (probeState.kind === 'error') throw new Error(probeState.message);
|
|
214
|
+
return probeState.kind === 'version' ? probeState.version : undefined;
|
|
215
|
+
},
|
|
216
|
+
installVersion: async (version) => {
|
|
217
|
+
trace.push({ t: 'install', version });
|
|
218
|
+
if (installState.kind === 'error') throw new Error(installState.message);
|
|
219
|
+
if (installState.kind === 'entry') return { entryPath: installState.entryPath };
|
|
220
|
+
throw new Error('no scripted install result');
|
|
221
|
+
},
|
|
222
|
+
readEntryBytes: (entryPath) => {
|
|
223
|
+
trace.push({ t: 'read_bytes', path: entryPath });
|
|
224
|
+
return entryBytes.get(entryPath);
|
|
225
|
+
},
|
|
226
|
+
reexecSelf: ({ entryPath }) => trace.push({ t: 'reexec', entryPath }),
|
|
227
|
+
reexecAttemptStore: {
|
|
228
|
+
load: () => storedAttempt,
|
|
229
|
+
save: (record) => {
|
|
230
|
+
trace.push({ t: 'attempt_save', record });
|
|
231
|
+
storedAttempt = record;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
log: (event, payload = {}) => trace.push({ t: 'log', event, payload }),
|
|
235
|
+
scheduler: timers.scheduler,
|
|
236
|
+
now: () => clock.nowMs,
|
|
237
|
+
timing: caseInput.timing ?? undefined,
|
|
238
|
+
...(caseInput.overlap === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: {
|
|
241
|
+
generationOverlap: {
|
|
242
|
+
enabled: caseInput.overlap.enabled === true,
|
|
243
|
+
stillbornWindowMs: caseInput.overlap.stillbornWindowMs,
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const aliveChildren = () => children.filter((child) => child.alive);
|
|
249
|
+
const exitChild = (record, code, signal) => {
|
|
250
|
+
record.alive = false;
|
|
251
|
+
trace.push({ t: 'exit', child: record.index, code, signal });
|
|
252
|
+
record.onExit({ code, signal });
|
|
253
|
+
};
|
|
254
|
+
const ipcTo = (record, label, message) => {
|
|
255
|
+
if (
|
|
256
|
+
record === undefined ||
|
|
257
|
+
record.generation === undefined ||
|
|
258
|
+
record.generation.onMessage === undefined
|
|
259
|
+
) {
|
|
260
|
+
trace.push({ t: 'ipc_no_channel', op: label });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
trace.push({ t: 'ipc', child: record.index, message });
|
|
264
|
+
record.generation.onMessage(message);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
for (const op of caseInput.ops ?? []) {
|
|
268
|
+
switch (op.op) {
|
|
269
|
+
case 'start':
|
|
270
|
+
supervisor.start();
|
|
271
|
+
break;
|
|
272
|
+
case 'advance': {
|
|
273
|
+
clock.nowMs += op.byMs;
|
|
274
|
+
let fired = timers.fireNextDue();
|
|
275
|
+
while (fired) {
|
|
276
|
+
await settle();
|
|
277
|
+
fired = timers.fireNextDue();
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case 'exitNewest':
|
|
282
|
+
case 'exitOldest': {
|
|
283
|
+
const alive = aliveChildren();
|
|
284
|
+
const target = op.op === 'exitNewest' ? alive[alive.length - 1] : alive[0];
|
|
285
|
+
if (target === undefined) {
|
|
286
|
+
trace.push({ t: 'no_child', op: op.op });
|
|
287
|
+
} else {
|
|
288
|
+
exitChild(target, op.code ?? null, op.signal ?? null);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'exitAll': {
|
|
293
|
+
for (const record of aliveChildren()) {
|
|
294
|
+
exitChild(record, op.code ?? null, op.signal ?? null);
|
|
295
|
+
await settle();
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'ipcReadyNewest': {
|
|
300
|
+
const alive = aliveChildren();
|
|
301
|
+
const target = alive[alive.length - 1];
|
|
302
|
+
ipcTo(target, op.op, {
|
|
303
|
+
type: 'generation_ready',
|
|
304
|
+
epoch: target?.generation?.epoch ?? 0,
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case 'ipcPinSnapshotOldest': {
|
|
309
|
+
const target = aliveChildren()[0];
|
|
310
|
+
ipcTo(target, op.op, {
|
|
311
|
+
type: 'generation_pin_snapshot',
|
|
312
|
+
epoch: target?.generation?.epoch ?? 0,
|
|
313
|
+
threadIds: op.threadIds,
|
|
314
|
+
});
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case 'ipcPinReleasedOldest': {
|
|
318
|
+
const target = aliveChildren()[0];
|
|
319
|
+
ipcTo(target, op.op, {
|
|
320
|
+
type: 'generation_pin_released',
|
|
321
|
+
epoch: target?.generation?.epoch ?? 0,
|
|
322
|
+
threadId: op.threadId,
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'ipcGarbageNewest': {
|
|
327
|
+
const alive = aliveChildren();
|
|
328
|
+
ipcTo(alive[alive.length - 1], op.op, op.message);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case 'setLatest':
|
|
332
|
+
probeState =
|
|
333
|
+
op.version === null ? { kind: 'none' } : { kind: 'version', version: op.version };
|
|
334
|
+
break;
|
|
335
|
+
case 'probeError':
|
|
336
|
+
probeState = { kind: 'error', message: op.message };
|
|
337
|
+
break;
|
|
338
|
+
case 'setInstall':
|
|
339
|
+
installState = { kind: 'entry', entryPath: op.entryPath };
|
|
340
|
+
break;
|
|
341
|
+
case 'installError':
|
|
342
|
+
installState = { kind: 'error', message: op.message };
|
|
343
|
+
break;
|
|
344
|
+
case 'setBytes':
|
|
345
|
+
if (op.bytes === null) entryBytes.delete(op.path);
|
|
346
|
+
else entryBytes.set(op.path, op.bytes);
|
|
347
|
+
break;
|
|
348
|
+
case 'setSendNewest': {
|
|
349
|
+
const alive = aliveChildren();
|
|
350
|
+
const target = alive[alive.length - 1];
|
|
351
|
+
if (target === undefined) trace.push({ t: 'no_child', op: op.op });
|
|
352
|
+
else target.sendOk = op.ok === true;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case 'requestStop':
|
|
356
|
+
supervisor.requestStop(
|
|
357
|
+
op.forwardSignal === undefined ? undefined : { forwardSignal: op.forwardSignal }
|
|
358
|
+
);
|
|
359
|
+
break;
|
|
360
|
+
case 'childActive':
|
|
361
|
+
trace.push({ t: 'child_active', value: supervisor.childActive() });
|
|
362
|
+
break;
|
|
363
|
+
case 'awaitDone': {
|
|
364
|
+
const outcome = await Promise.race([
|
|
365
|
+
supervisor.done.then(() => 'done'),
|
|
366
|
+
new Promise((resolve) => setTimeout(() => resolve('done_timeout'), 5000)),
|
|
367
|
+
]);
|
|
368
|
+
trace.push({ t: outcome });
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
default:
|
|
372
|
+
trace.push({ t: 'unknown_op', op: op.op });
|
|
373
|
+
}
|
|
374
|
+
await settle();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { trace };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// --- Lock acquire runs (real fs on a scratch dir; startedAt pinned) --------------------
|
|
381
|
+
|
|
382
|
+
function runAcquireLockCase(caseInput) {
|
|
383
|
+
const scratch = mkdtempSync(join(tmpdir(), 'linzumi-core-sup-lock-'));
|
|
384
|
+
const lockPath = join(scratch, 'sup.lock');
|
|
385
|
+
const trace = [];
|
|
386
|
+
const releases = [];
|
|
387
|
+
for (const step of caseInput.steps ?? []) {
|
|
388
|
+
if (step.action === 'acquire') {
|
|
389
|
+
const alivePids = step.alivePids ?? [];
|
|
390
|
+
const result = withPinnedDate(caseInput.nowMs ?? 0, () =>
|
|
391
|
+
acquireSupervisorLock({
|
|
392
|
+
path: lockPath,
|
|
393
|
+
workspace: caseInput.workspace ?? 'ws-lock',
|
|
394
|
+
linzumiUrl: caseInput.linzumiUrl ?? 'wss://parity.example',
|
|
395
|
+
pid: step.pid,
|
|
396
|
+
isPidAlive: (candidate) => alivePids.includes(candidate),
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
if (result.ok) {
|
|
400
|
+
releases.push(result.release);
|
|
401
|
+
trace.push({ t: 'acquired' });
|
|
402
|
+
} else {
|
|
403
|
+
trace.push({ t: 'held', holderPid: result.holderPid ?? null });
|
|
404
|
+
}
|
|
405
|
+
trace.push({
|
|
406
|
+
t: 'lock_bytes',
|
|
407
|
+
bytes: existsSync(lockPath) ? readFileSync(lockPath, 'utf8') : null,
|
|
408
|
+
});
|
|
409
|
+
} else if (step.action === 'releaseLast') {
|
|
410
|
+
const release = releases.pop();
|
|
411
|
+
if (release !== undefined) release();
|
|
412
|
+
trace.push({ t: 'released', fileExists: existsSync(lockPath) });
|
|
413
|
+
} else if (step.action === 'seedLock') {
|
|
414
|
+
writeFileSync(lockPath, step.content);
|
|
415
|
+
trace.push({ t: 'seeded' });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { trace };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- Console logger / signal handlers / tty guard --------------------------------------
|
|
422
|
+
|
|
423
|
+
function runConsoleLoggerCase(caseInput) {
|
|
424
|
+
const stderrLines = [];
|
|
425
|
+
const audits = [];
|
|
426
|
+
let writes = 0;
|
|
427
|
+
const logger = createSupervisorConsoleLogger({
|
|
428
|
+
writeStderr: (line) => {
|
|
429
|
+
writes += 1;
|
|
430
|
+
if (
|
|
431
|
+
caseInput.stderrFailAfter !== undefined &&
|
|
432
|
+
writes > caseInput.stderrFailAfter
|
|
433
|
+
) {
|
|
434
|
+
throw new Error('EIO scripted console death');
|
|
435
|
+
}
|
|
436
|
+
stderrLines.push(line);
|
|
437
|
+
},
|
|
438
|
+
audit: (event, payload) => {
|
|
439
|
+
if (caseInput.auditFails === true) throw new Error('scripted audit failure');
|
|
440
|
+
audits.push({ event, payload });
|
|
441
|
+
},
|
|
442
|
+
now: () => new RealDate(caseInput.nowMs ?? 0),
|
|
443
|
+
});
|
|
444
|
+
for (const entry of caseInput.events ?? []) {
|
|
445
|
+
logger(entry.event, entry.payload);
|
|
446
|
+
}
|
|
447
|
+
return { stderrLines, audits };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function runSignalHandlersCase(caseInput) {
|
|
451
|
+
const trace = [];
|
|
452
|
+
const once = new Map();
|
|
453
|
+
const on = new Map();
|
|
454
|
+
installSupervisorSignalHandlers(
|
|
455
|
+
{
|
|
456
|
+
requestStop: (options) =>
|
|
457
|
+
trace.push({ t: 'request_stop', forwardSignal: options?.forwardSignal ?? null }),
|
|
458
|
+
},
|
|
459
|
+
(event, payload = {}) => trace.push({ t: 'log', event, payload }),
|
|
460
|
+
{
|
|
461
|
+
once: (event, listener) => {
|
|
462
|
+
const queue = once.get(event) ?? [];
|
|
463
|
+
queue.push(listener);
|
|
464
|
+
once.set(event, queue);
|
|
465
|
+
trace.push({ t: 'registered_once', event });
|
|
466
|
+
},
|
|
467
|
+
on: (event, listener) => {
|
|
468
|
+
const queue = on.get(event) ?? [];
|
|
469
|
+
queue.push(listener);
|
|
470
|
+
on.set(event, queue);
|
|
471
|
+
trace.push({ t: 'registered_on', event });
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
for (const signal of caseInput.fires ?? []) {
|
|
476
|
+
trace.push({ t: 'fire', signal });
|
|
477
|
+
const onceQueue = once.get(signal) ?? [];
|
|
478
|
+
if (onceQueue.length > 0) {
|
|
479
|
+
const listener = onceQueue.shift();
|
|
480
|
+
listener();
|
|
481
|
+
}
|
|
482
|
+
for (const listener of on.get(signal) ?? []) {
|
|
483
|
+
listener();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { trace };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function errorOfSpec(spec) {
|
|
490
|
+
if (spec.kind === 'null') return null;
|
|
491
|
+
if (spec.kind === 'string') return spec.value;
|
|
492
|
+
if (spec.kind === 'number') return spec.value;
|
|
493
|
+
const error = {};
|
|
494
|
+
if (spec.code !== undefined) error.code = spec.code;
|
|
495
|
+
if (spec.message !== undefined) error.message = spec.message;
|
|
496
|
+
return error;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function runTtyClassifyCase(caseInput) {
|
|
500
|
+
return (caseInput.errors ?? []).map((spec) => {
|
|
501
|
+
const error = errorOfSpec(spec);
|
|
502
|
+
return {
|
|
503
|
+
gone: goneTtyErrorCode(error) ?? null,
|
|
504
|
+
write: goneTtyWriteErrorCode(error) ?? null,
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function runTtyGuardCase(caseInput) {
|
|
510
|
+
const trace = [];
|
|
511
|
+
const errorListeners = [];
|
|
512
|
+
let throwSpec;
|
|
513
|
+
const stream = {
|
|
514
|
+
isTTY: true,
|
|
515
|
+
write: (chunk) => {
|
|
516
|
+
if (throwSpec !== undefined) {
|
|
517
|
+
const error = new Error(throwSpec.message);
|
|
518
|
+
if (throwSpec.code !== undefined) error.code = throwSpec.code;
|
|
519
|
+
throwSpec = undefined;
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
trace.push({ t: 'underlying_write', chunk });
|
|
523
|
+
return true;
|
|
524
|
+
},
|
|
525
|
+
on: (event, listener) => {
|
|
526
|
+
if (event === 'error') errorListeners.push(listener);
|
|
527
|
+
trace.push({ t: 'listener_added', event });
|
|
528
|
+
},
|
|
529
|
+
listenerCount: (event) => (event === 'error' ? errorListeners.length : 0),
|
|
530
|
+
};
|
|
531
|
+
const sinks = {
|
|
532
|
+
audit: (event, fields) => trace.push({ t: 'sink', leg: 'audit', event, fields }),
|
|
533
|
+
log: (event, fields) => trace.push({ t: 'sink', leg: 'log', event, fields }),
|
|
534
|
+
};
|
|
535
|
+
const guard = (label) =>
|
|
536
|
+
trace.push({
|
|
537
|
+
t: 'guarded',
|
|
538
|
+
label,
|
|
539
|
+
result: guardStreamWriteAgainstGoneTty(stream, {
|
|
540
|
+
context: { surface: 'parity_surface' },
|
|
541
|
+
sinks,
|
|
542
|
+
onGoneTty: (code) => trace.push({ t: 'gone_tty', label, code }),
|
|
543
|
+
rethrow: (error) =>
|
|
544
|
+
trace.push({
|
|
545
|
+
t: 'rethrown',
|
|
546
|
+
message: error instanceof Error ? error.message : String(error),
|
|
547
|
+
}),
|
|
548
|
+
}),
|
|
549
|
+
});
|
|
550
|
+
guard('first');
|
|
551
|
+
for (const op of caseInput.ops ?? []) {
|
|
552
|
+
if (op.op === 'write') {
|
|
553
|
+
if (op.throwCode !== undefined || op.throwMessage !== undefined) {
|
|
554
|
+
throwSpec = { code: op.throwCode, message: op.throwMessage ?? 'scripted write failure' };
|
|
555
|
+
}
|
|
556
|
+
let outcome;
|
|
557
|
+
let threwMessage = null;
|
|
558
|
+
try {
|
|
559
|
+
outcome = stream.write(op.data);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
outcome = null;
|
|
562
|
+
threwMessage = error instanceof Error ? error.message : String(error);
|
|
563
|
+
}
|
|
564
|
+
trace.push({ t: 'write_result', returned: outcome, threw: threwMessage });
|
|
565
|
+
} else if (op.op === 'emitError') {
|
|
566
|
+
const error = new Error(op.message ?? 'scripted stream error');
|
|
567
|
+
if (op.code !== undefined) error.code = op.code;
|
|
568
|
+
trace.push({ t: 'emit_error', code: op.code ?? null });
|
|
569
|
+
for (const listener of [...errorListeners]) listener(error);
|
|
570
|
+
} else if (op.op === 'guardAgain') {
|
|
571
|
+
guard(op.label ?? 'again');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return { trace, goneCode: streamGoneTtyWriteCodeForTest(stream) ?? null };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// --- runSuperviseLoop refusal legs -----------------------------------------------------
|
|
578
|
+
|
|
579
|
+
async function runLockRefusalCase(caseInput) {
|
|
580
|
+
const scratch = mkdtempSync(join(tmpdir(), 'linzumi-core-sup-home-'));
|
|
581
|
+
const workspace = caseInput.workspace ?? '';
|
|
582
|
+
const args = workspace === '' ? [] : ['--workspace', workspace];
|
|
583
|
+
const lockPath = supervisorLockPath(
|
|
584
|
+
{ workspace, linzumiUrl: defaultLinzumiWebSocketUrl },
|
|
585
|
+
scratch
|
|
586
|
+
);
|
|
587
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
588
|
+
writeFileSync(
|
|
589
|
+
lockPath,
|
|
590
|
+
JSON.stringify({
|
|
591
|
+
pid: 1,
|
|
592
|
+
workspace,
|
|
593
|
+
linzumiUrl: defaultLinzumiWebSocketUrl,
|
|
594
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
595
|
+
}) + '\\n'
|
|
596
|
+
);
|
|
597
|
+
const previousHome = process.env.HOME;
|
|
598
|
+
process.env.HOME = scratch;
|
|
599
|
+
try {
|
|
600
|
+
await runSuperviseLoop(['--supervise', ...args]);
|
|
601
|
+
return { message: null };
|
|
602
|
+
} catch (error) {
|
|
603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
604
|
+
return { message: message.split(scratch).join('$SCRATCH') };
|
|
605
|
+
} finally {
|
|
606
|
+
process.env.HOME = previousHome;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function runEmptyEntryCase() {
|
|
611
|
+
const scratch = mkdtempSync(join(tmpdir(), 'linzumi-core-sup-home-'));
|
|
612
|
+
const previousHome = process.env.HOME;
|
|
613
|
+
const previousArgv1 = process.argv[1];
|
|
614
|
+
process.env.HOME = scratch;
|
|
615
|
+
process.argv[1] = '';
|
|
616
|
+
try {
|
|
617
|
+
await runSuperviseLoop(['--supervise', '--workspace', 'ws-empty']);
|
|
618
|
+
return { message: null };
|
|
619
|
+
} catch (error) {
|
|
620
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
621
|
+
return { message: message.split(scratch).join('$SCRATCH') };
|
|
622
|
+
} finally {
|
|
623
|
+
process.env.HOME = previousHome;
|
|
624
|
+
process.argv[1] = previousArgv1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// --- Case dispatch -----------------------------------------------------------------------
|
|
629
|
+
|
|
630
|
+
async function runCase(input) {
|
|
631
|
+
switch (input.kind) {
|
|
632
|
+
case 'constants':
|
|
633
|
+
return {
|
|
634
|
+
supervisedChildEnvKey,
|
|
635
|
+
generationOverlapFlagEnvKey,
|
|
636
|
+
generationEpochEnvKey,
|
|
637
|
+
generationHandoffPidEnvKey,
|
|
638
|
+
defaultGenerationStillbornWindowMs,
|
|
639
|
+
defaultSupervisorTiming,
|
|
640
|
+
defaultLinzumiWebSocketUrl,
|
|
641
|
+
};
|
|
642
|
+
case 'isSupervisedChild':
|
|
643
|
+
return isSupervisedChild(input.env);
|
|
644
|
+
case 'withoutSuperviseFlags':
|
|
645
|
+
return withoutSuperviseFlags(input.args);
|
|
646
|
+
case 'hasSuperviseFlag':
|
|
647
|
+
return hasSuperviseFlag(input.args);
|
|
648
|
+
case 'fingerprint':
|
|
649
|
+
return (
|
|
650
|
+
supervisorCodeFingerprint(input.entryPath, (path) => {
|
|
651
|
+
const bytes = (input.files ?? {})[path];
|
|
652
|
+
return bytes === undefined ? undefined : bytes;
|
|
653
|
+
}) ?? null
|
|
654
|
+
);
|
|
655
|
+
case 'lockKey':
|
|
656
|
+
return supervisorLockKey({ workspace: input.workspace, linzumiUrl: input.linzumiUrl });
|
|
657
|
+
case 'lockPath':
|
|
658
|
+
return supervisorLockPath(
|
|
659
|
+
{ workspace: input.workspace, linzumiUrl: input.linzumiUrl },
|
|
660
|
+
input.homeDir
|
|
661
|
+
);
|
|
662
|
+
case 'reexecStatePath':
|
|
663
|
+
return defaultSupervisorReexecStatePath(input.env);
|
|
664
|
+
case 'acquireLockRun':
|
|
665
|
+
return runAcquireLockCase(input);
|
|
666
|
+
case 'consoleLogger':
|
|
667
|
+
return runConsoleLoggerCase(input);
|
|
668
|
+
case 'signalHandlers':
|
|
669
|
+
return runSignalHandlersCase(input);
|
|
670
|
+
case 'ttyClassify':
|
|
671
|
+
return runTtyClassifyCase(input);
|
|
672
|
+
case 'ttyGuardRun':
|
|
673
|
+
return runTtyGuardCase(input);
|
|
674
|
+
case 'lockRefusalLoop':
|
|
675
|
+
return runLockRefusalCase(input);
|
|
676
|
+
case 'emptyEntryLoop':
|
|
677
|
+
return runEmptyEntryCase();
|
|
678
|
+
case 'supervisionRun':
|
|
679
|
+
return runSupervisionCase(input);
|
|
680
|
+
default:
|
|
681
|
+
return { error: 'unknown case kind ' + String(input.kind) };
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function runBatch() {
|
|
686
|
+
const chunks = [];
|
|
687
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
688
|
+
const cases = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
689
|
+
const results = [];
|
|
690
|
+
for (const testCase of cases) {
|
|
691
|
+
results.push(await runCase(testCase));
|
|
692
|
+
}
|
|
693
|
+
process.stdout.write(JSON.stringify(results), () => {
|
|
694
|
+
process.exit(0);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
runBatch().then(
|
|
699
|
+
() => {},
|
|
700
|
+
(error) => {
|
|
701
|
+
process.stderr.write(String(error && error.stack ? error.stack : error) + '\\n');
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
`;
|
|
706
|
+
|
|
707
|
+
const outDir = join(packageRoot, '.differential');
|
|
708
|
+
const outPath = join(outDir, 'core-supervisor-parity-oracle.mjs');
|
|
709
|
+
|
|
710
|
+
await mkdir(outDir, { recursive: true });
|
|
711
|
+
|
|
712
|
+
await build({
|
|
713
|
+
stdin: {
|
|
714
|
+
contents: driverSource,
|
|
715
|
+
resolveDir: packageRoot,
|
|
716
|
+
sourcefile: 'core-supervisor-parity-driver.ts',
|
|
717
|
+
loader: 'ts',
|
|
718
|
+
},
|
|
719
|
+
bundle: true,
|
|
720
|
+
platform: 'node',
|
|
721
|
+
target: 'node20',
|
|
722
|
+
format: 'esm',
|
|
723
|
+
outfile: outPath,
|
|
724
|
+
minify: false,
|
|
725
|
+
sourcemap: false,
|
|
726
|
+
legalComments: 'none',
|
|
727
|
+
logLevel: 'silent',
|
|
728
|
+
// supervisor.ts pulls the wider commander graph (updateNudge,
|
|
729
|
+
// workerEntryStability, runnerLogger); the same runtime externals as the
|
|
730
|
+
// differential entry build stay external.
|
|
731
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
process.stdout.write(outPath + '\n');
|