@linzumi/cli 1.0.114 → 1.0.115

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.
@@ -0,0 +1,814 @@
1
+ // Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
2
+ // (M3 cluster 12, worker-spawn).
3
+ // Relationship: Builds the TS-side PARITY ORACLE for the worker fork +
4
+ // lifecycle + IPC protocol of the ReScript commander port. The oracle
5
+ // bundles the REAL TS worker seams - the runner.ts spawn-path exports (the
6
+ // worker argv render + redaction, the node loader exec-argv filter, the env
7
+ // overlay, the ready-message parse, the ready-timeout vocabulary, the GAP A
8
+ // bounded-respawn orchestrator, the doomed-start preflight, the
9
+ // stillborn-worker vocabulary), the thread-worker IPC module (BOTH halves),
10
+ // the worker output routing module, the active-worker-liveness state
11
+ // machine, the remote codex harness worker's pure exports, and the worker
12
+ // argv PARSE (parseThreadCodexWorkerArgs, imported lazily with a
13
+ // neutralized argv[1] so the CLI entry's main-module gate can never fire) -
14
+ // behind the cluster 1/2 one-shot BATCH driver (a JSON array of cases on
15
+ // stdin, a JSON array of results on stdout).
16
+ //
17
+ // PROCESS MODE: invoked as `node <oracle> ipc-child <threadId>` the oracle
18
+ // becomes a REAL IPC worker child running the REAL bindThreadCodexWorkerIpc
19
+ // over process IPC with a scripted codex client - the cross-impl leg's TS
20
+ // worker (a ReScript parent drives it; a TS parent drives the ReScript
21
+ // child; the frames must be byte-identical either way).
22
+ //
23
+ // Determinism: IPC/microtask scheduling serializes through a bounded
24
+ // microtask drain after every scripted step (the cluster 10 convention);
25
+ // scratch paths normalize through "$DIR"; the remote join payload's
26
+ // process material (pid, CLI version) normalizes through placeholders.
27
+ //
28
+ // SECRETS: synthetic case payloads only; .differential/ is gitignored.
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 { mkdirSync, mkdtempSync, realpathSync } from 'node:fs';
38
+ import { tmpdir } from 'node:os';
39
+ import { dirname as pathDirname, join } from 'node:path';
40
+ import {
41
+ boundedWorkerOutputForTest,
42
+ isStillbornWorkerErrorForTest,
43
+ redactedThreadRunnerCliArgsForTest,
44
+ spawnThreadRunnerWithBoundedRespawnForTest,
45
+ stillbornWorkerFailedReasonForTest,
46
+ threadRunnerCliArgsForTest,
47
+ threadRunnerExitBeforeReadyMessageForTest,
48
+ threadRunnerNodeArgsForTest,
49
+ threadRunnerReadyMessageForTest,
50
+ threadRunnerReadyTimeoutErrorForTest,
51
+ threadRunnerReadyTimeoutMsForTest,
52
+ threadRunnerScriptPathForTest,
53
+ threadRunnerWorkerEnvOverlay,
54
+ threadStartTerminalFailureForTest,
55
+ threadWorkerPreflightFailureLineForTest,
56
+ threadWorkerStartPreflightFailureForTest,
57
+ } from './src/runner';
58
+ import {
59
+ bindThreadCodexWorkerIpc,
60
+ connectThreadCodexWorkerIpc,
61
+ isThreadCodexWorkerSessionClosedMessage,
62
+ } from './src/threadCodexWorkerIpc';
63
+ import {
64
+ createThreadWorkerOutputChunkHandler,
65
+ routeThreadWorkerOutputChunk,
66
+ } from './src/threadWorkerOutputRouting';
67
+ import {
68
+ createWorkerLivenessState,
69
+ defaultWorkerLivenessConfig,
70
+ recordPingSent,
71
+ recordPong,
72
+ recordProbeReply,
73
+ recordProbeSent,
74
+ shouldSendPing,
75
+ tickPongDeadline,
76
+ workerLivenessVerdict,
77
+ } from './src/activeWorkerLiveness';
78
+ import {
79
+ remoteCodexHarnessWorkerConfigFromEnv,
80
+ remoteCodexHarnessWorkerConfigFromJson,
81
+ remoteHarnessRunnerJoinPayload,
82
+ resolveLinzumiCliEntrypoint,
83
+ } from './src/remoteCodexHarnessWorker';
84
+
85
+ const dirPlaceholder = '$DIR';
86
+
87
+ function scratch() {
88
+ return realpathSync(mkdtempSync(join(tmpdir(), 'linzumi-worker-spawn-oracle-')));
89
+ }
90
+
91
+ function substitute(value, from, to) {
92
+ if (typeof value === 'string') {
93
+ return value.split(from).join(to);
94
+ }
95
+ if (Array.isArray(value)) {
96
+ return value.map((entry) => substitute(entry, from, to));
97
+ }
98
+ if (typeof value === 'object' && value !== null) {
99
+ return Object.fromEntries(
100
+ Object.entries(value).map(([key, entry]) => [
101
+ substitute(key, from, to),
102
+ substitute(entry, from, to),
103
+ ])
104
+ );
105
+ }
106
+ return value;
107
+ }
108
+
109
+ function outcome(run) {
110
+ try {
111
+ const value = run();
112
+ return value === undefined ? { defined: false } : { defined: true, value };
113
+ } catch (error) {
114
+ return {
115
+ threw: true,
116
+ message: error instanceof Error ? error.message : String(error),
117
+ };
118
+ }
119
+ }
120
+
121
+ async function drainMicrotasks() {
122
+ for (let tick = 0; tick < 8; tick += 1) {
123
+ await Promise.resolve();
124
+ }
125
+ }
126
+
127
+ // --- The scripted fake codex client (both the batch driver and ipc-child use it) ---
128
+
129
+ function scriptedCodex(spec, trace) {
130
+ const notificationCallbacks = [];
131
+ const requestCallbacks = [];
132
+ const responses = spec.responses ?? {};
133
+ return {
134
+ surface: {
135
+ request: (method, params) => {
136
+ trace.push({ t: 'codexRequest', method, params: params ?? null });
137
+ const scripted = responses[method];
138
+ if (scripted === undefined) {
139
+ return Promise.resolve({ id: 0, result: { echoed: method } });
140
+ }
141
+ if (scripted.reject !== undefined) {
142
+ return Promise.reject(new Error(scripted.reject));
143
+ }
144
+ return Promise.resolve(scripted.response);
145
+ },
146
+ notify: (method, params) => {
147
+ trace.push({ t: 'codexNotify', method, params: params ?? null });
148
+ },
149
+ onNotification: (callback) => {
150
+ notificationCallbacks.push(callback);
151
+ },
152
+ onRequest:
153
+ spec.withoutOnRequest === true
154
+ ? undefined
155
+ : (callback) => {
156
+ requestCallbacks.push(callback);
157
+ },
158
+ },
159
+ emitNotification: (notification) => {
160
+ for (const callback of notificationCallbacks) {
161
+ callback(notification);
162
+ }
163
+ },
164
+ emitServerRequest: (request) => {
165
+ const settled = [];
166
+ for (const callback of requestCallbacks) {
167
+ settled.push(
168
+ Promise.resolve(callback(request)).then(
169
+ (value) => trace.push({ t: 'serverRequestResolved', value: value ?? null }),
170
+ (error) =>
171
+ trace.push({
172
+ t: 'serverRequestRejected',
173
+ message: error instanceof Error ? error.message : String(error),
174
+ })
175
+ )
176
+ );
177
+ }
178
+ return settled;
179
+ },
180
+ };
181
+ }
182
+
183
+ // --- ipc-child mode: the REAL TS worker half over REAL process IPC -----------------
184
+
185
+ async function runIpcChild() {
186
+ const trace = [];
187
+ const codex = scriptedCodex(
188
+ {
189
+ responses: {
190
+ 'thread/read': { response: { id: 7, result: { items: ['a', 'b'] } } },
191
+ 'boom/method': { reject: 'scripted boom' },
192
+ },
193
+ },
194
+ trace
195
+ );
196
+ bindThreadCodexWorkerIpc(codex.surface);
197
+ // Scripted choreography driven by plain (non-worker-frame) IPC cues from
198
+ // the parent harness: emit a notification / a server request, then report
199
+ // the child-side trace.
200
+ process.on('message', (message) => {
201
+ if (message === null || typeof message !== 'object') {
202
+ return;
203
+ }
204
+ if (message.cue === 'emit_notification') {
205
+ codex.emitNotification(message.notification);
206
+ process.send?.({ cue: 'notification_emitted' });
207
+ } else if (message.cue === 'emit_server_request') {
208
+ void Promise.all(codex.emitServerRequest(message.request)).then(() => {
209
+ process.send?.({ cue: 'server_request_settled', trace });
210
+ });
211
+ } else if (message.cue === 'report') {
212
+ process.send?.({ cue: 'trace', trace });
213
+ } else if (message.cue === 'exit') {
214
+ process.exit(0);
215
+ }
216
+ });
217
+ process.send?.({ cue: 'child_ready' });
218
+ // Keep the event loop alive until the parent orders exit.
219
+ setInterval(() => {}, 60_000);
220
+ }
221
+
222
+ // --- Batch ops ----------------------------------------------------------------------
223
+
224
+ function handleIpcWorker(testCase) {
225
+ const trace = [];
226
+ const sent = [];
227
+ const codex = scriptedCodex(testCase.codex ?? {}, trace);
228
+ let connected = true;
229
+ let sendMode = 'ok';
230
+ const ipc = {
231
+ send: (message) => {
232
+ if (sendMode === 'throw') {
233
+ throw new Error(testCase.sendThrowMessage ?? 'scripted send failure');
234
+ }
235
+ sent.push(message);
236
+ return true;
237
+ },
238
+ get connected() {
239
+ return connected;
240
+ },
241
+ on: (event, callback) => {
242
+ if (event === 'message') {
243
+ ipcMessageHandlers.push(callback);
244
+ }
245
+ return ipc;
246
+ },
247
+ once: (event, callback) => {
248
+ if (event === 'disconnect') {
249
+ disconnectHandlers.push(callback);
250
+ }
251
+ return ipc;
252
+ },
253
+ };
254
+ const ipcMessageHandlers = [];
255
+ const disconnectHandlers = [];
256
+
257
+ const run = async () => {
258
+ bindThreadCodexWorkerIpc(codex.surface, ipc);
259
+ for (const step of testCase.script ?? []) {
260
+ if (step.step === 'parentMessage') {
261
+ for (const handler of ipcMessageHandlers) {
262
+ handler(step.message);
263
+ }
264
+ } else if (step.step === 'notification') {
265
+ codex.emitNotification(step.notification);
266
+ } else if (step.step === 'serverRequest') {
267
+ codex.emitServerRequest(step.request);
268
+ } else if (step.step === 'disconnectChannel') {
269
+ connected = false;
270
+ } else if (step.step === 'sendMode') {
271
+ sendMode = step.mode;
272
+ } else if (step.step === 'disconnectEvent') {
273
+ for (const handler of disconnectHandlers.splice(0)) {
274
+ handler();
275
+ }
276
+ }
277
+ await drainMicrotasks();
278
+ }
279
+ return { sent, trace };
280
+ };
281
+ return run().catch((error) => ({
282
+ threw: true,
283
+ message: error instanceof Error ? error.message : String(error),
284
+ sent,
285
+ trace,
286
+ }));
287
+ }
288
+
289
+ async function handleIpcParent(testCase) {
290
+ const trace = [];
291
+ const sent = [];
292
+ let connected = true;
293
+ let sendMode = 'ok';
294
+ const messageHandlers = [];
295
+ const exitHandlers = [];
296
+ const disconnectHandlers = [];
297
+ const child = {
298
+ send: (message) => {
299
+ if (sendMode === 'throw') {
300
+ throw new Error(testCase.sendThrowMessage ?? 'scripted send failure');
301
+ }
302
+ sent.push(message);
303
+ return true;
304
+ },
305
+ get connected() {
306
+ return connected;
307
+ },
308
+ on: (event, callback) => {
309
+ if (event === 'message') messageHandlers.push(callback);
310
+ return child;
311
+ },
312
+ once: (event, callback) => {
313
+ if (event === 'exit') exitHandlers.push(callback);
314
+ if (event === 'disconnect') disconnectHandlers.push(callback);
315
+ return child;
316
+ },
317
+ off: (event, callback) => {
318
+ const pool =
319
+ event === 'message'
320
+ ? messageHandlers
321
+ : event === 'exit'
322
+ ? exitHandlers
323
+ : disconnectHandlers;
324
+ const index = pool.indexOf(callback);
325
+ if (index >= 0) pool.splice(index, 1);
326
+ return child;
327
+ },
328
+ };
329
+
330
+ const client = connectThreadCodexWorkerIpc(child);
331
+ client.onNotification((notification) => trace.push({ t: 'notification', notification }));
332
+ if (testCase.requestHandlers !== undefined) {
333
+ for (const handler of testCase.requestHandlers) {
334
+ client.onRequest((request) => {
335
+ trace.push({ t: 'serverRequest', handler: handler.name, request });
336
+ if (handler.kind === 'answer') return handler.result;
337
+ if (handler.kind === 'reject') return Promise.reject(new Error(handler.message));
338
+ return undefined;
339
+ });
340
+ }
341
+ }
342
+
343
+ for (const step of testCase.script ?? []) {
344
+ if (step.step === 'request') {
345
+ void client.request(step.method, step.params ?? undefined).then(
346
+ (response) => trace.push({ t: 'requestResolved', response }),
347
+ (error) =>
348
+ trace.push({
349
+ t: 'requestRejected',
350
+ message: error instanceof Error ? error.message : String(error),
351
+ })
352
+ );
353
+ } else if (step.step === 'notify') {
354
+ try {
355
+ client.notify(step.method, step.params ?? undefined);
356
+ trace.push({ t: 'notifyOk' });
357
+ } catch (error) {
358
+ trace.push({
359
+ t: 'notifyThrew',
360
+ message: error instanceof Error ? error.message : String(error),
361
+ });
362
+ }
363
+ } else if (step.step === 'childMessage') {
364
+ for (const handler of [...messageHandlers]) {
365
+ handler(step.message);
366
+ }
367
+ } else if (step.step === 'lateNotificationSubscribe') {
368
+ client.onNotification((notification) =>
369
+ trace.push({ t: 'lateNotification', notification })
370
+ );
371
+ } else if (step.step === 'exitEvent') {
372
+ for (const handler of exitHandlers.splice(0)) handler();
373
+ } else if (step.step === 'disconnectEvent') {
374
+ for (const handler of disconnectHandlers.splice(0)) handler();
375
+ } else if (step.step === 'disconnectChannel') {
376
+ connected = false;
377
+ } else if (step.step === 'sendMode') {
378
+ sendMode = step.mode;
379
+ } else if (step.step === 'isClosed') {
380
+ trace.push({ t: 'isClosed', value: client.isClosed?.() ?? null });
381
+ } else if (step.step === 'close') {
382
+ client.close();
383
+ trace.push({ t: 'closed' });
384
+ }
385
+ await drainMicrotasks();
386
+ }
387
+ return { sent, trace };
388
+ }
389
+
390
+ async function handleRespawn(testCase) {
391
+ const script = [...(testCase.script ?? [])];
392
+ const trace = [];
393
+ const attempt = (attemptNumber) => {
394
+ const step = script.shift() ?? { kind: 'ready' };
395
+ trace.push({ t: 'attempt', attemptNumber, kind: step.kind });
396
+ if (step.kind === 'ready') {
397
+ return Promise.resolve({ kind: 'ready', handle: { marker: 'handle-' + attemptNumber } });
398
+ }
399
+ return Promise.resolve({
400
+ kind: 'exited-before-ready',
401
+ code: step.code ?? null,
402
+ signal: step.signal ?? null,
403
+ capturedOutput: step.capturedOutput ?? '',
404
+ });
405
+ };
406
+ try {
407
+ const handle = await spawnThreadRunnerWithBoundedRespawnForTest(attempt, {
408
+ maxAttempts: testCase.maxAttempts,
409
+ backoffMs: 0,
410
+ sleep: () => Promise.resolve(),
411
+ onRespawn: (info) =>
412
+ trace.push({ t: 'respawn', attemptNumber: info.attemptNumber }),
413
+ });
414
+ return { trace, handle };
415
+ } catch (error) {
416
+ return {
417
+ trace,
418
+ threw: error instanceof Error ? error.message : String(error),
419
+ };
420
+ }
421
+ }
422
+
423
+ function handleOutputRouting(testCase) {
424
+ const trace = [];
425
+ let captured = '';
426
+ const capture = { active: true };
427
+ let tui = false;
428
+ const handler = createThreadWorkerOutputChunkHandler({
429
+ tuiActive: () => tui,
430
+ terminal: { write: (chunk) => trace.push({ t: 'terminal', chunk: String(chunk) }) },
431
+ writeAuditEvent: (event, data) => trace.push({ t: 'audit', event, data }),
432
+ pid: testCase.pid ?? undefined,
433
+ streamName: testCase.streamName ?? 'stderr',
434
+ capture,
435
+ appendCapturedOutput: (text) => {
436
+ captured += text;
437
+ },
438
+ });
439
+ for (const step of testCase.script ?? []) {
440
+ if (step.step === 'chunk') {
441
+ tui = step.tuiActive === true;
442
+ capture.active = step.captureActive === true;
443
+ handler(step.text);
444
+ } else if (step.step === 'route') {
445
+ trace.push({
446
+ t: 'route',
447
+ route: routeThreadWorkerOutputChunk({
448
+ tuiActive: step.tuiActive === true,
449
+ captureActive: step.captureActive === true,
450
+ }),
451
+ });
452
+ }
453
+ }
454
+ return { trace, captured };
455
+ }
456
+
457
+ function handleLiveness(testCase) {
458
+ const config = testCase.config ?? defaultWorkerLivenessConfig;
459
+ let state;
460
+ const trace = [];
461
+ for (const step of testCase.schedule ?? []) {
462
+ switch (step.op) {
463
+ case 'create':
464
+ state = createWorkerLivenessState(step.now);
465
+ break;
466
+ case 'pingSent':
467
+ recordPingSent(state, step.now);
468
+ break;
469
+ case 'pong':
470
+ recordPong(state, step.now);
471
+ break;
472
+ case 'probeSent':
473
+ recordProbeSent(state, step.now);
474
+ break;
475
+ case 'probeReply':
476
+ recordProbeReply(state, step.now);
477
+ break;
478
+ case 'tick':
479
+ tickPongDeadline(state, config, step.now);
480
+ break;
481
+ case 'verdict':
482
+ trace.push({ t: 'verdict', now: step.now, value: workerLivenessVerdict(state, config, step.now) });
483
+ break;
484
+ case 'shouldSendPing':
485
+ trace.push({ t: 'shouldSendPing', now: step.now, value: shouldSendPing(state, config, step.now) });
486
+ break;
487
+ }
488
+ }
489
+ return { trace };
490
+ }
491
+
492
+ function handlePreflight(testCase) {
493
+ const dir = scratch();
494
+ const sub = (value) => substitute(value, dirPlaceholder, dir);
495
+ for (const relPath of testCase.mkdirs ?? []) {
496
+ mkdirSync(join(dir, relPath), { recursive: true });
497
+ }
498
+ const result = outcome(() =>
499
+ threadWorkerStartPreflightFailureForTest({
500
+ cwd: sub(testCase.cwd),
501
+ allowedCwds: sub(testCase.allowedCwds ?? []),
502
+ })
503
+ );
504
+ return substitute(result, dir, dirPlaceholder);
505
+ }
506
+
507
+ const savedEnv = {};
508
+
509
+ function setCaseEnv(env) {
510
+ for (const [key, value] of Object.entries(env ?? {})) {
511
+ savedEnv[key] = process.env[key];
512
+ if (value === null) {
513
+ delete process.env[key];
514
+ } else {
515
+ process.env[key] = value;
516
+ }
517
+ }
518
+ }
519
+
520
+ function restoreCaseEnv() {
521
+ for (const [key, value] of Object.entries(savedEnv)) {
522
+ if (value === undefined) {
523
+ delete process.env[key];
524
+ } else {
525
+ process.env[key] = value;
526
+ }
527
+ delete savedEnv[key];
528
+ }
529
+ }
530
+
531
+ let indexModulePromise;
532
+
533
+ // parseThreadCodexWorkerArgs lives in the CLI entry module, whose
534
+ // main-module gate compares import.meta.url with argv[1]; neutralize
535
+ // argv[1] BEFORE the lazy import so the gate can never fire inside the
536
+ // oracle.
537
+ function loadIndexModule() {
538
+ if (indexModulePromise === undefined) {
539
+ process.argv[1] = '/nonexistent/linzumi-worker-spawn-oracle-argv1';
540
+ indexModulePromise = import('./src/index');
541
+ }
542
+ return indexModulePromise;
543
+ }
544
+
545
+ async function handleParseWorkerArgs(testCase) {
546
+ const dir = scratch();
547
+ const sub = (value) => substitute(value, dirPlaceholder, dir);
548
+ for (const relPath of testCase.mkdirs ?? []) {
549
+ mkdirSync(join(dir, relPath), { recursive: true });
550
+ }
551
+ const { parseThreadCodexWorkerArgs } = await loadIndexModule();
552
+ setCaseEnv(sub(testCase.env ?? {}));
553
+ try {
554
+ const options = parseThreadCodexWorkerArgs(sub(testCase.args ?? []));
555
+ // Project the worker-relevant slice (the codec's contract surface).
556
+ return substitute(
557
+ {
558
+ value: {
559
+ kandanUrl: options.kandanUrl,
560
+ token: options.token,
561
+ runnerId: options.runnerId,
562
+ workspaceSlug: options.workspaceSlug ?? null,
563
+ cwd: options.cwd,
564
+ codexBin: options.codexBin,
565
+ vmExecution: options.vmExecution === true,
566
+ executionMode: options.executionMode ?? null,
567
+ egressMode: options.egressMode ?? null,
568
+ projectPublicId: options.projectPublicId ?? null,
569
+ goldenImagePublicId: options.goldenImagePublicId ?? null,
570
+ fast: options.fast === true,
571
+ logFile: options.logFile ?? null,
572
+ allowedCwds: options.allowedCwds,
573
+ missingAllowedCwds: options.missingAllowedCwds ?? [],
574
+ allowedForwardPorts: options.allowedForwardPorts ?? [],
575
+ runtimeDefaults: options.runtimeDefaults ?? null,
576
+ codexModelProvider: options.codexModelProvider ?? null,
577
+ linzumiAgentMcpToken: options.linzumiAgentMcpToken ?? null,
578
+ kandanThreadId: options.threadProcess?.kandanThreadId ?? null,
579
+ debugTrace: options.debugTrace ?? null,
580
+ },
581
+ },
582
+ dir,
583
+ dirPlaceholder
584
+ );
585
+ } catch (error) {
586
+ return substitute(
587
+ { threw: error instanceof Error ? error.message : String(error) },
588
+ dir,
589
+ dirPlaceholder
590
+ );
591
+ } finally {
592
+ restoreCaseEnv();
593
+ }
594
+ }
595
+
596
+ async function handle(testCase) {
597
+ switch (testCase.op) {
598
+ case 'cliArgs':
599
+ return outcome(() => threadRunnerCliArgsForTest(testCase.options));
600
+ case 'redactedCliArgs':
601
+ return outcome(() => redactedThreadRunnerCliArgsForTest(testCase.args));
602
+ case 'envOverlay':
603
+ return outcome(() =>
604
+ threadRunnerWorkerEnvOverlay(testCase.options, {
605
+ devServicesParentPid: testCase.devServicesParentPid,
606
+ forwardableOpenAiApiKey: testCase.forwardableOpenAiApiKey ?? undefined,
607
+ })
608
+ );
609
+ case 'nodeArgs':
610
+ return outcome(() =>
611
+ threadRunnerNodeArgsForTest(testCase.scriptPath, testCase.args, testCase.execArgv)
612
+ );
613
+ case 'scriptPath':
614
+ return outcome(() =>
615
+ threadRunnerScriptPathForTest(testCase.scriptPath ?? undefined, testCase.cwd)
616
+ );
617
+ case 'readyMessage':
618
+ return outcome(() =>
619
+ threadRunnerReadyMessageForTest(testCase.message, testCase.expectedKandanThreadId)
620
+ );
621
+ case 'sessionClosedGuard':
622
+ return outcome(() => isThreadCodexWorkerSessionClosedMessage(testCase.message));
623
+ case 'readyTimeoutMs':
624
+ return outcome(() =>
625
+ threadRunnerReadyTimeoutMsForTest({
626
+ threadRunnerReadyTimeoutMs: testCase.configured ?? undefined,
627
+ })
628
+ );
629
+ case 'readyTimeoutError':
630
+ return outcome(
631
+ () =>
632
+ threadRunnerReadyTimeoutErrorForTest(testCase.kandanThreadId, testCase.timeoutMs).message
633
+ );
634
+ case 'boundedOutput':
635
+ return outcome(() => boundedWorkerOutputForTest(testCase.value));
636
+ case 'exitBeforeReadyMessage':
637
+ return outcome(() =>
638
+ threadRunnerExitBeforeReadyMessageForTest({
639
+ kind: 'exited-before-ready',
640
+ code: testCase.code ?? null,
641
+ signal: testCase.signal ?? null,
642
+ capturedOutput: testCase.capturedOutput ?? '',
643
+ })
644
+ );
645
+ case 'preflight':
646
+ return handlePreflight(testCase);
647
+ case 'preflightLine':
648
+ return outcome(() => threadWorkerPreflightFailureLineForTest(testCase.capturedOutput));
649
+ case 'stillborn':
650
+ return outcome(() => ({
651
+ isStillborn: isStillbornWorkerErrorForTest(new Error(testCase.message)),
652
+ failedReason: stillbornWorkerFailedReasonForTest(new Error(testCase.message)),
653
+ terminal: threadStartTerminalFailureForTest(new Error(testCase.message)),
654
+ }));
655
+ case 'respawn':
656
+ return handleRespawn(testCase);
657
+ case 'outputRouting':
658
+ return handleOutputRouting(testCase);
659
+ case 'liveness':
660
+ return handleLiveness(testCase);
661
+ case 'ipcWorker':
662
+ return handleIpcWorker(testCase);
663
+ case 'ipcParent':
664
+ return handleIpcParent(testCase);
665
+ case 'remoteConfig':
666
+ return outcome(() => remoteCodexHarnessWorkerConfigFromJson(testCase.json));
667
+ case 'remoteConfigEnv':
668
+ return outcome(() => remoteCodexHarnessWorkerConfigFromEnv(testCase.env ?? {}));
669
+ case 'remoteEntrypoint':
670
+ return outcome(() => resolveLinzumiCliEntrypoint(testCase.entrypoint ?? undefined));
671
+ case 'remoteJoinPayload':
672
+ return outcome(() => {
673
+ const payload = remoteHarnessRunnerJoinPayload(testCase.config);
674
+ payload.runnerPid = 0;
675
+ payload.version = '<version>';
676
+ return payload;
677
+ });
678
+ case 'parseWorkerArgs':
679
+ return handleParseWorkerArgs(testCase);
680
+ default:
681
+ return { threw: true, message: 'unknown op ' + String(testCase.op) };
682
+ }
683
+ }
684
+
685
+ // --- ipc-parent mode: the REAL TS parent half over REAL process IPC ------------------
686
+ // "node <oracle> ipc-parent <childEntry> [childArg...]" spawns the child
687
+ // with an IPC channel, drives the REAL connectThreadCodexWorkerIpc through
688
+ // the fixed cross-impl choreography, and prints one JSON result: the raw
689
+ // frames the child sent (cue messages excluded), the parent-observed
690
+ // events, and the child's own trace. Run against the TS ipc-child AND the
691
+ // ReScript child probe, the outputs must be byte-identical - the same
692
+ // commander parent drives both workers identically.
693
+ async function runIpcParent() {
694
+ const { spawn } = await import('node:child_process');
695
+ const childArgs = process.argv.slice(3);
696
+ const child = spawn(process.execPath, childArgs, {
697
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
698
+ });
699
+ const frames = [];
700
+ const parentEvents = [];
701
+ let childTrace = null;
702
+ const cueWaiters = new Map();
703
+ const waitForCue = (cue) =>
704
+ new Promise((resolve) => {
705
+ cueWaiters.set(cue, resolve);
706
+ });
707
+ child.on('message', (message) => {
708
+ if (message !== null && typeof message === 'object' && message.cue !== undefined) {
709
+ const waiter = cueWaiters.get(message.cue);
710
+ if (waiter !== undefined) {
711
+ cueWaiters.delete(message.cue);
712
+ waiter(message);
713
+ }
714
+ return;
715
+ }
716
+ frames.push(message);
717
+ });
718
+
719
+ await waitForCue('child_ready');
720
+ const client = connectThreadCodexWorkerIpc(child);
721
+ client.onNotification((notification) =>
722
+ parentEvents.push({ t: 'notification', notification })
723
+ );
724
+ client.onRequest((request) => {
725
+ parentEvents.push({ t: 'serverRequest', request });
726
+ return { decision: 'approved' };
727
+ });
728
+
729
+ try {
730
+ const readResponse = await client.request('thread/read', { threadId: 't-cross' });
731
+ parentEvents.push({ t: 'requestResolved', response: readResponse });
732
+ } catch (error) {
733
+ parentEvents.push({
734
+ t: 'requestRejected',
735
+ message: error instanceof Error ? error.message : String(error),
736
+ });
737
+ }
738
+ try {
739
+ await client.request('boom/method');
740
+ parentEvents.push({ t: 'requestResolved', response: null });
741
+ } catch (error) {
742
+ parentEvents.push({
743
+ t: 'requestRejected',
744
+ message: error instanceof Error ? error.message : String(error),
745
+ });
746
+ }
747
+ client.notify('turn/interrupt', { reason: 'cross-impl' });
748
+ const notified = waitForCue('notification_emitted');
749
+ child.send({
750
+ cue: 'emit_notification',
751
+ notification: { method: 'thread/event', params: { kind: 'delta', n: 1 } },
752
+ });
753
+ await notified;
754
+ const settled = waitForCue('server_request_settled');
755
+ child.send({
756
+ cue: 'emit_server_request',
757
+ request: { id: 21, method: 'applyPatchApproval', params: { call: 'c-cross' } },
758
+ });
759
+ await settled;
760
+ const reported = waitForCue('trace');
761
+ child.send({ cue: 'report' });
762
+ childTrace = (await reported).trace;
763
+ child.send({ cue: 'exit' });
764
+ await new Promise((resolve) => child.once('exit', resolve));
765
+ process.stdout.write(JSON.stringify({ frames, parentEvents, childTrace }));
766
+ }
767
+
768
+ if (process.argv[2] === 'ipc-child') {
769
+ await runIpcChild();
770
+ } else if (process.argv[2] === 'ipc-parent') {
771
+ await runIpcParent();
772
+ } else {
773
+ let stdin = '';
774
+ process.stdin.setEncoding('utf8');
775
+ for await (const chunk of process.stdin) {
776
+ stdin += chunk;
777
+ }
778
+ const cases = JSON.parse(stdin);
779
+ const results = [];
780
+ for (const testCase of cases) {
781
+ results.push(await handle(testCase));
782
+ }
783
+ process.stdout.write(JSON.stringify(results));
784
+ }
785
+ `;
786
+
787
+ await mkdir(join(packageRoot, '.differential'), { recursive: true });
788
+
789
+ // runner.ts's graph resolves assets/ relative to the bundle at import time
790
+ // (same requirement as build-differential-entry.mjs).
791
+ await mkdir(join(packageRoot, '.differential/assets'), { recursive: true });
792
+ const { copyFile } = await import('node:fs/promises');
793
+ await copyFile(
794
+ join(packageRoot, 'src/assets/linzumi-logo.svg'),
795
+ join(packageRoot, '.differential/assets/linzumi-logo.svg')
796
+ );
797
+
798
+ await build({
799
+ stdin: {
800
+ contents: driverSource,
801
+ resolveDir: packageRoot,
802
+ sourcefile: 'worker-spawn-parity-driver.ts',
803
+ loader: 'ts',
804
+ },
805
+ bundle: true,
806
+ platform: 'node',
807
+ target: 'node20',
808
+ format: 'esm',
809
+ outfile: join(packageRoot, '.differential/worker-spawn-parity-oracle.mjs'),
810
+ minify: false,
811
+ sourcemap: false,
812
+ legalComments: 'none',
813
+ external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
814
+ });