@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,288 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 10, commander core) +
|
|
3
|
+
// plans/2026-07-11-runner-generation-overlap-zero-downtime-updates.md
|
|
4
|
+
// (the ported surface's own spec, CLI half #2981).
|
|
5
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the generation-overlap
|
|
6
|
+
// slice of the ReScript commander port, the sibling of
|
|
7
|
+
// build-pipeline-parity-oracle.mjs (cluster 9) and
|
|
8
|
+
// build-session-registry-parity-oracle.mjs (cluster 6). The oracle bundles
|
|
9
|
+
// the REAL TS generation-overlap module (src/generationOverlap.ts) - env
|
|
10
|
+
// flag/readers, join-hello envelope, the supervisor<->child IPC codec, the
|
|
11
|
+
// per-process overlap runtime, the dispatch gate's held-control queues,
|
|
12
|
+
// and the cursor-persistence freeze - behind the cluster 1/2 one-shot
|
|
13
|
+
// BATCH driver (a JSON array of cases on stdin, a JSON array of results on
|
|
14
|
+
// stdout).
|
|
15
|
+
//
|
|
16
|
+
// Byte parity: envelopes, parsed/built IPC frames, and every scripted
|
|
17
|
+
// trace entry are returned VERBATIM (the TS objects' own key order rides
|
|
18
|
+
// JSON.stringify), so the ReScript side must reproduce field presence AND
|
|
19
|
+
// insertion order exactly - the hello and IPC payloads are frozen wire
|
|
20
|
+
// bytes. Determinism: nothing here reads env, clocks, or process.send -
|
|
21
|
+
// env dicts are scripted per case, the runtime's send seam is injected,
|
|
22
|
+
// and runtime/gate/freeze flows are driven by scripted op arrays.
|
|
23
|
+
//
|
|
24
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
25
|
+
// payloads. The output is a local test artifact (.differential/ is
|
|
26
|
+
// gitignored, never published) and stays unminified for debuggability.
|
|
27
|
+
import { mkdir } from 'node:fs/promises';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { build } from 'esbuild';
|
|
31
|
+
|
|
32
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
33
|
+
|
|
34
|
+
const driverSource = `
|
|
35
|
+
import {
|
|
36
|
+
buildGenerationIpcMessage,
|
|
37
|
+
createGenerationDispatchGate,
|
|
38
|
+
createGenerationOverlapRuntime,
|
|
39
|
+
defaultGenerationStillbornWindowMs,
|
|
40
|
+
generationEpochEnvKey,
|
|
41
|
+
generationHandoffPidEnvKey,
|
|
42
|
+
generationHelloEnvelope,
|
|
43
|
+
generationOverlapFlagEnvKey,
|
|
44
|
+
generationStillbornWindowEnvKey,
|
|
45
|
+
isGenerationOverlapEnabled,
|
|
46
|
+
parseGenerationIpcMessage,
|
|
47
|
+
readGenerationEpoch,
|
|
48
|
+
readGenerationHandoffPid,
|
|
49
|
+
resolveGenerationStillbornWindowMs,
|
|
50
|
+
withGenerationCursorFreeze,
|
|
51
|
+
} from './src/generationOverlap';
|
|
52
|
+
|
|
53
|
+
// Scripted runtime flow: hooks/log/send all trace-recording, ops drive the
|
|
54
|
+
// REAL createGenerationOverlapRuntime.
|
|
55
|
+
function runRuntimeCase(caseInput) {
|
|
56
|
+
const trace = [];
|
|
57
|
+
let sendOk = true;
|
|
58
|
+
const runtime = createGenerationOverlapRuntime({
|
|
59
|
+
env: caseInput.env,
|
|
60
|
+
log: (event, fields) => trace.push({ t: 'log', event, fields }),
|
|
61
|
+
send: (message) => {
|
|
62
|
+
trace.push({ t: 'send', message, ok: sendOk });
|
|
63
|
+
return sendOk;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
if (runtime === undefined) {
|
|
67
|
+
return { created: false, trace };
|
|
68
|
+
}
|
|
69
|
+
if (caseInput.installHooks !== false) {
|
|
70
|
+
runtime.hooks.onOverlapDrain = (incomingEpoch) =>
|
|
71
|
+
trace.push({ t: 'hook.overlapDrain', incomingEpoch });
|
|
72
|
+
runtime.hooks.onOverlapAbort = () => trace.push({ t: 'hook.overlapAbort' });
|
|
73
|
+
runtime.hooks.onPinsReleased = (released) =>
|
|
74
|
+
trace.push({ t: 'hook.pinsReleased', released });
|
|
75
|
+
runtime.hooks.onPromote = () => trace.push({ t: 'hook.promote' });
|
|
76
|
+
}
|
|
77
|
+
for (const op of caseInput.ops ?? []) {
|
|
78
|
+
if (op.op === 'ipc') runtime.handleIpcMessage(op.value);
|
|
79
|
+
else if (op.op === 'beginDrain') runtime.beginOutgoingDrain(op.threadIds);
|
|
80
|
+
else if (op.op === 'send') {
|
|
81
|
+
const message = parseGenerationIpcMessage(op.message);
|
|
82
|
+
if (message === undefined) trace.push({ t: 'sendSkipped' });
|
|
83
|
+
else runtime.sendToSupervisor(message);
|
|
84
|
+
} else if (op.op === 'sendResult') sendOk = op.ok === true;
|
|
85
|
+
else if (op.op === 'pinOutgoing') runtime.outgoingPins.pin(op.threadId);
|
|
86
|
+
else if (op.op === 'releaseOutgoing')
|
|
87
|
+
trace.push({
|
|
88
|
+
t: 'released',
|
|
89
|
+
threadId: op.threadId,
|
|
90
|
+
released: runtime.outgoingPins.release(op.threadId),
|
|
91
|
+
});
|
|
92
|
+
else if (op.op === 'seams') {
|
|
93
|
+
runtime.requestUpdateDrain('parity-trigger');
|
|
94
|
+
trace.push({
|
|
95
|
+
t: 'seams',
|
|
96
|
+
cancel: runtime.cancelUpdateDrain('parity-reason'),
|
|
97
|
+
promote: runtime.promoteRunnerLock(),
|
|
98
|
+
});
|
|
99
|
+
} else if (op.op === 'query')
|
|
100
|
+
trace.push({
|
|
101
|
+
t: 'query',
|
|
102
|
+
epoch: runtime.epoch,
|
|
103
|
+
handoffFromPid: runtime.handoffFromPid ?? null,
|
|
104
|
+
outgoingDrainActive: runtime.outgoingDrainActive(),
|
|
105
|
+
pinsKnown: runtime.pinsKnown(),
|
|
106
|
+
role: runtime.currentRole() ?? null,
|
|
107
|
+
outgoingPins: runtime.outgoingPins.snapshot(),
|
|
108
|
+
incomingPins: runtime.incomingPins.snapshot(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { created: true, trace };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Scripted gate flow: controls are plain JSON envelopes; the extractors
|
|
115
|
+
// mirror the runner wiring (threadIdOf / controlTypeOf injected).
|
|
116
|
+
function runGateCase(caseInput) {
|
|
117
|
+
const trace = [];
|
|
118
|
+
const runtime = createGenerationOverlapRuntime({
|
|
119
|
+
env: caseInput.env,
|
|
120
|
+
log: (event, fields) => trace.push({ t: 'log', event, fields }),
|
|
121
|
+
send: () => true,
|
|
122
|
+
});
|
|
123
|
+
if (runtime === undefined) {
|
|
124
|
+
return { created: false, trace };
|
|
125
|
+
}
|
|
126
|
+
const gate = createGenerationDispatchGate({
|
|
127
|
+
runtime,
|
|
128
|
+
threadIdOf: (control) =>
|
|
129
|
+
typeof control.threadId === 'string' ? control.threadId : undefined,
|
|
130
|
+
controlTypeOf: (control) =>
|
|
131
|
+
typeof control.type === 'string' ? control.type : '',
|
|
132
|
+
dispatch: (control, ack) => {
|
|
133
|
+
trace.push({
|
|
134
|
+
t: 'dispatched',
|
|
135
|
+
id: control.id ?? null,
|
|
136
|
+
threadId: typeof control.threadId === 'string' ? control.threadId : null,
|
|
137
|
+
});
|
|
138
|
+
ack();
|
|
139
|
+
},
|
|
140
|
+
log: (event, fields) => trace.push({ t: 'log', event, fields }),
|
|
141
|
+
});
|
|
142
|
+
for (const op of caseInput.ops ?? []) {
|
|
143
|
+
if (op.op === 'control') {
|
|
144
|
+
const control = op.control;
|
|
145
|
+
gate.dispatch(control, () =>
|
|
146
|
+
trace.push({ t: 'ack', id: control.id ?? null })
|
|
147
|
+
);
|
|
148
|
+
} else if (op.op === 'ipc') runtime.handleIpcMessage(op.value);
|
|
149
|
+
else if (op.op === 'beginDrain') runtime.beginOutgoingDrain(op.threadIds);
|
|
150
|
+
else if (op.op === 'dispatchUnpinned')
|
|
151
|
+
trace.push({ t: 'dispatchUnpinned', count: gate.dispatchUnpinned() });
|
|
152
|
+
else if (op.op === 'redispatch')
|
|
153
|
+
trace.push({ t: 'redispatch', count: gate.redispatchDeferredToIncoming() });
|
|
154
|
+
else if (op.op === 'held') {
|
|
155
|
+
const counts = gate.heldCounts();
|
|
156
|
+
trace.push({
|
|
157
|
+
t: 'held',
|
|
158
|
+
deferredToIncoming: counts.deferredToIncoming,
|
|
159
|
+
deferredPinned: counts.deferredPinned,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { created: true, trace };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Scripted cursor-freeze flow: the underlying store is trace-recording;
|
|
167
|
+
// ops toggle the frozen predicate and drive the wrapped store.
|
|
168
|
+
function runFreezeCase(caseInput) {
|
|
169
|
+
const trace = [];
|
|
170
|
+
let frozen = false;
|
|
171
|
+
const store = {
|
|
172
|
+
initial: (topic) => (caseInput.initialValues ?? {})[topic],
|
|
173
|
+
onAdvance: (topic, seq) => trace.push({ t: 'store.onAdvance', topic, seq }),
|
|
174
|
+
reset:
|
|
175
|
+
caseInput.hasReset === false
|
|
176
|
+
? undefined
|
|
177
|
+
: (topic, seq) => trace.push({ t: 'store.reset', topic, seq }),
|
|
178
|
+
flush: () => trace.push({ t: 'store.flush' }),
|
|
179
|
+
epoch: () =>
|
|
180
|
+
typeof caseInput.storeEpoch === 'string' ? caseInput.storeEpoch : undefined,
|
|
181
|
+
adoptEpoch: (epoch) => {
|
|
182
|
+
trace.push({ t: 'store.adoptEpoch', epoch });
|
|
183
|
+
return caseInput.adoptResult === true;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const wrapped = withGenerationCursorFreeze(store, () => frozen);
|
|
187
|
+
for (const op of caseInput.ops ?? []) {
|
|
188
|
+
if (op.op === 'setFrozen') frozen = op.value === true;
|
|
189
|
+
else if (op.op === 'initial')
|
|
190
|
+
trace.push({ t: 'initial', topic: op.topic, value: wrapped.initial(op.topic) ?? null });
|
|
191
|
+
else if (op.op === 'advance') wrapped.onAdvance(op.topic, op.seq);
|
|
192
|
+
else if (op.op === 'reset') {
|
|
193
|
+
if (wrapped.reset === undefined) trace.push({ t: 'resetUnavailable' });
|
|
194
|
+
else wrapped.reset(op.topic, op.seq);
|
|
195
|
+
} else if (op.op === 'flush') wrapped.flush();
|
|
196
|
+
else if (op.op === 'epoch')
|
|
197
|
+
trace.push({ t: 'epoch', value: wrapped.epoch() ?? null });
|
|
198
|
+
else if (op.op === 'adoptEpoch')
|
|
199
|
+
trace.push({ t: 'adoptEpoch', result: wrapped.adoptEpoch(op.epoch) });
|
|
200
|
+
}
|
|
201
|
+
return { trace };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function runCase(input) {
|
|
205
|
+
switch (input.kind) {
|
|
206
|
+
case 'constants':
|
|
207
|
+
return {
|
|
208
|
+
flagKey: generationOverlapFlagEnvKey,
|
|
209
|
+
epochKey: generationEpochEnvKey,
|
|
210
|
+
handoffPidKey: generationHandoffPidEnvKey,
|
|
211
|
+
stillbornWindowKey: generationStillbornWindowEnvKey,
|
|
212
|
+
defaultStillbornWindowMs: defaultGenerationStillbornWindowMs,
|
|
213
|
+
};
|
|
214
|
+
case 'envRead': {
|
|
215
|
+
if (input.which === 'enabled') return isGenerationOverlapEnabled(input.env);
|
|
216
|
+
if (input.which === 'epoch') return readGenerationEpoch(input.env) ?? null;
|
|
217
|
+
if (input.which === 'handoffPid')
|
|
218
|
+
return readGenerationHandoffPid(input.env) ?? null;
|
|
219
|
+
return resolveGenerationStillbornWindowMs(input.env);
|
|
220
|
+
}
|
|
221
|
+
case 'helloEnvelope':
|
|
222
|
+
return generationHelloEnvelope(input.env);
|
|
223
|
+
case 'parseIpc': {
|
|
224
|
+
const parsed = parseGenerationIpcMessage(input.value);
|
|
225
|
+
return parsed === undefined ? null : parsed;
|
|
226
|
+
}
|
|
227
|
+
case 'buildIpc': {
|
|
228
|
+
const parsed = parseGenerationIpcMessage(input.message);
|
|
229
|
+
return parsed === undefined ? { unparsable: true } : buildGenerationIpcMessage(parsed);
|
|
230
|
+
}
|
|
231
|
+
case 'runtimeRun':
|
|
232
|
+
return runRuntimeCase(input);
|
|
233
|
+
case 'gateRun':
|
|
234
|
+
return runGateCase(input);
|
|
235
|
+
case 'freezeRun':
|
|
236
|
+
return runFreezeCase(input);
|
|
237
|
+
default:
|
|
238
|
+
return { error: 'unknown case kind ' + String(input.kind) };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function runBatch() {
|
|
243
|
+
const chunks = [];
|
|
244
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
245
|
+
const cases = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
246
|
+
const results = [];
|
|
247
|
+
for (const testCase of cases) {
|
|
248
|
+
results.push(runCase(testCase));
|
|
249
|
+
}
|
|
250
|
+
process.stdout.write(JSON.stringify(results), () => {
|
|
251
|
+
process.exit(0);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
runBatch().then(
|
|
256
|
+
() => {},
|
|
257
|
+
(error) => {
|
|
258
|
+
process.stderr.write(String(error && error.stack ? error.stack : error) + '\\n');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
const outDir = join(packageRoot, '.differential');
|
|
265
|
+
const outPath = join(outDir, 'core-generation-overlap-parity-oracle.mjs');
|
|
266
|
+
|
|
267
|
+
await mkdir(outDir, { recursive: true });
|
|
268
|
+
|
|
269
|
+
await build({
|
|
270
|
+
stdin: {
|
|
271
|
+
contents: driverSource,
|
|
272
|
+
resolveDir: packageRoot,
|
|
273
|
+
sourcefile: 'core-generation-overlap-parity-driver.ts',
|
|
274
|
+
loader: 'ts',
|
|
275
|
+
},
|
|
276
|
+
bundle: true,
|
|
277
|
+
platform: 'node',
|
|
278
|
+
target: 'node20',
|
|
279
|
+
format: 'esm',
|
|
280
|
+
outfile: outPath,
|
|
281
|
+
minify: false,
|
|
282
|
+
sourcemap: false,
|
|
283
|
+
legalComments: 'none',
|
|
284
|
+
logLevel: 'silent',
|
|
285
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
process.stdout.write(outPath + '\n');
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 10, runner core).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the pipeline
|
|
4
|
+
// INTEGRATION module of the ReScript commander port, the sibling of
|
|
5
|
+
// build-pipeline-parity-oracle.mjs (cluster 9) and
|
|
6
|
+
// build-queue-parity-oracle.mjs (cluster 5). The oracle bundles the REAL
|
|
7
|
+
// TS pipeline integration module (src/pipeline/integration.ts - the
|
|
8
|
+
// per-session loop + outbox lifecycle, the outbox entry builders, and the
|
|
9
|
+
// completion-snapshot resolution helpers) plus the real phoenix push-error
|
|
10
|
+
// factory and the turnTrace per-thread traceparent registry, behind the
|
|
11
|
+
// cluster 1/2 one-shot BATCH driver (a JSON array of cases on stdin, a
|
|
12
|
+
// JSON array of results on stdout).
|
|
13
|
+
//
|
|
14
|
+
// Byte parity: builder results and snapshot-helper outputs are returned
|
|
15
|
+
// VERBATIM (the TS object literals' own key order rides JSON.stringify),
|
|
16
|
+
// and scripted SESSION runs return their full event trace - pushes with
|
|
17
|
+
// complete payloads, acks/failures, snapshot resolutions, terminal
|
|
18
|
+
// callbacks, log events, flushes, metrics counters, close - so the
|
|
19
|
+
// ReScript side must reproduce field presence AND insertion order exactly.
|
|
20
|
+
// Determinism: hosts are fully scripted (push outcomes, snapshot outcomes,
|
|
21
|
+
// wire contexts, envelope shapes, recovered turns, runtime metadata);
|
|
22
|
+
// retry cadences are configured to millisecond-scale delays so the traces
|
|
23
|
+
// pin the LOGGED delay values while real time stays tiny; the stats
|
|
24
|
+
// interval is disabled (0) in session runs; traces capture only
|
|
25
|
+
// deterministic metric counters (never wall-clock ages).
|
|
26
|
+
//
|
|
27
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
28
|
+
// payloads. The output is a local test artifact (.differential/ is
|
|
29
|
+
// gitignored, never published) and stays unminified for debuggability.
|
|
30
|
+
import { mkdir } from 'node:fs/promises';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { build } from 'esbuild';
|
|
34
|
+
|
|
35
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
36
|
+
|
|
37
|
+
const driverSource = `
|
|
38
|
+
import {
|
|
39
|
+
createSessionPipeline,
|
|
40
|
+
createSessionPipelineBuilders,
|
|
41
|
+
defaultOutboxMaxInFlightPushes,
|
|
42
|
+
isConnectionLostPushError,
|
|
43
|
+
isConnectionWaitPushError,
|
|
44
|
+
mergeSnapshotItems,
|
|
45
|
+
preReduceSnapshotItems,
|
|
46
|
+
rawSnapshotItemsForTurn,
|
|
47
|
+
} from './src/pipeline/integration';
|
|
48
|
+
import { outboxEntryKinds } from './src/pipeline/contracts';
|
|
49
|
+
import { phoenixPushErrors } from './src/phoenix';
|
|
50
|
+
import {
|
|
51
|
+
rememberTurnTraceparent,
|
|
52
|
+
__resetTurnTraceparentsForTest,
|
|
53
|
+
} from './src/turnTrace';
|
|
54
|
+
|
|
55
|
+
function sleep(ms) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
setTimeout(resolve, ms);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The scripted row-command envelope shared with the ReScript conformance
|
|
62
|
+
// side (both implement this exact shape over the same case JSON, so the
|
|
63
|
+
// seam itself is identical while the builder's spread/override semantics
|
|
64
|
+
// around it are what the comparison pins).
|
|
65
|
+
function scriptedEnvelope(caseInput) {
|
|
66
|
+
return (eventType, sessionId, sourceMessageSeq, extraMetadata) => ({
|
|
67
|
+
...(caseInput.envelopeBase ?? {}),
|
|
68
|
+
type: eventType,
|
|
69
|
+
thread: sessionId,
|
|
70
|
+
...(sourceMessageSeq === undefined ? {} : { envelope_seq: sourceMessageSeq }),
|
|
71
|
+
...(extraMetadata === undefined ? {} : { local_runner_metadata: extraMetadata }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function wireOf(spec) {
|
|
76
|
+
return {
|
|
77
|
+
workspaceSlug: spec.workspaceSlug,
|
|
78
|
+
channelSlug: spec.channelSlug,
|
|
79
|
+
kandanThreadId: spec.threadId ?? undefined,
|
|
80
|
+
codexThreadId: spec.harnessSessionId ?? undefined,
|
|
81
|
+
rootSeq: spec.rootSeq ?? undefined,
|
|
82
|
+
...(spec.leaseEpoch === undefined ? {} : { leaseEpoch: spec.leaseEpoch }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function seedTraceparents(caseInput) {
|
|
87
|
+
__resetTurnTraceparentsForTest();
|
|
88
|
+
for (const entry of caseInput.traceparents ?? []) {
|
|
89
|
+
rememberTurnTraceparent(entry.threadId, entry.traceparent);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runBuildersCase(caseInput) {
|
|
94
|
+
seedTraceparents(caseInput);
|
|
95
|
+
const recovered = new Set(caseInput.recoveredTurns ?? []);
|
|
96
|
+
const wires = (caseInput.wires ?? []).map(wireOf);
|
|
97
|
+
let wireIndex = 0;
|
|
98
|
+
const builders = createSessionPipelineBuilders({
|
|
99
|
+
instanceId: caseInput.instanceId ?? 'instance-parity',
|
|
100
|
+
wire: () => wires[wireIndex],
|
|
101
|
+
runnerPayload: scriptedEnvelope(caseInput),
|
|
102
|
+
isRecoveredTurn: (turnId) => recovered.has(turnId),
|
|
103
|
+
});
|
|
104
|
+
const context = {
|
|
105
|
+
remember: () => {},
|
|
106
|
+
recall: () => undefined,
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
results: (caseInput.calls ?? []).map((call) => {
|
|
110
|
+
wireIndex = call.wireIndex ?? 0;
|
|
111
|
+
const builder = builders[call.kind];
|
|
112
|
+
if (builder === undefined) {
|
|
113
|
+
return { missingBuilder: true };
|
|
114
|
+
}
|
|
115
|
+
return builder.build(call.data ?? {}, context);
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scriptedFailure(spec) {
|
|
121
|
+
switch (spec.factory) {
|
|
122
|
+
case 'socketClosed':
|
|
123
|
+
return phoenixPushErrors.socketClosed();
|
|
124
|
+
case 'reconnectFailed':
|
|
125
|
+
return phoenixPushErrors.reconnectFailed();
|
|
126
|
+
case 'socketNotOpen':
|
|
127
|
+
return phoenixPushErrors.socketNotOpen();
|
|
128
|
+
case 'clientClosedWait':
|
|
129
|
+
return phoenixPushErrors.clientClosed('wait_for_connection');
|
|
130
|
+
case 'clientClosedReply':
|
|
131
|
+
return phoenixPushErrors.clientClosed('reply');
|
|
132
|
+
case 'notConnected':
|
|
133
|
+
return phoenixPushErrors.notConnected();
|
|
134
|
+
case 'connectionWaitTimeout':
|
|
135
|
+
return phoenixPushErrors.connectionWaitTimeout(spec.timeoutMs ?? 5, spec.event ?? 'evt');
|
|
136
|
+
case 'replyTimeout':
|
|
137
|
+
return phoenixPushErrors.replyTimeout(spec.timeoutMs ?? 5, spec.event ?? 'evt');
|
|
138
|
+
default:
|
|
139
|
+
return new Error(spec.message ?? 'scripted push failure');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function runClassifierCase(caseInput) {
|
|
144
|
+
const failure = scriptedFailure(caseInput.failure ?? {});
|
|
145
|
+
return {
|
|
146
|
+
lost: isConnectionLostPushError(failure),
|
|
147
|
+
wait: isConnectionWaitPushError(failure),
|
|
148
|
+
message: failure.message,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runSessionCase(caseInput) {
|
|
153
|
+
seedTraceparents(caseInput);
|
|
154
|
+
const trace = [];
|
|
155
|
+
const recovered = new Set(caseInput.recoveredTurns ?? []);
|
|
156
|
+
const wires = (caseInput.wires ?? []).map(wireOf);
|
|
157
|
+
let wireIndex = 0;
|
|
158
|
+
const turnSeqs = new Map();
|
|
159
|
+
const retryAttempts = new Map();
|
|
160
|
+
const resumeAttempts = new Map();
|
|
161
|
+
const ladders = new Map();
|
|
162
|
+
const runtimeMetadata = new Map();
|
|
163
|
+
for (const entry of caseInput.runtimeMetadata ?? []) {
|
|
164
|
+
runtimeMetadata.set(entry.seq, entry.metadata);
|
|
165
|
+
}
|
|
166
|
+
const pushScript = [...(caseInput.pushScript ?? [])];
|
|
167
|
+
const snapshotScript = new Map();
|
|
168
|
+
for (const entry of caseInput.snapshotScript ?? []) {
|
|
169
|
+
const queue = snapshotScript.get(entry.turnId) ?? [];
|
|
170
|
+
queue.push(entry);
|
|
171
|
+
snapshotScript.set(entry.turnId, queue);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pipeline = createSessionPipeline({
|
|
175
|
+
instanceId: caseInput.instanceId ?? 'instance-parity',
|
|
176
|
+
threadKey: caseInput.threadKey ?? 'thread-parity',
|
|
177
|
+
persistDir: undefined,
|
|
178
|
+
push: async (event, payload, _timeoutMs, onSent) => {
|
|
179
|
+
trace.push({ t: 'push', event, payload });
|
|
180
|
+
onSent?.();
|
|
181
|
+
const step = pushScript.shift() ?? { kind: 'ack' };
|
|
182
|
+
if (step.delayMs !== undefined) {
|
|
183
|
+
await sleep(step.delayMs);
|
|
184
|
+
}
|
|
185
|
+
if (step.kind === 'ack') {
|
|
186
|
+
trace.push({ t: 'ack' });
|
|
187
|
+
return step.reply ?? { ok: true };
|
|
188
|
+
}
|
|
189
|
+
trace.push({ t: 'pushFail', mode: step.mode ?? 'plain' });
|
|
190
|
+
if (step.mode === 'wait') {
|
|
191
|
+
throw phoenixPushErrors.connectionWaitTimeout(5, event);
|
|
192
|
+
}
|
|
193
|
+
if (step.mode === 'lost') {
|
|
194
|
+
throw phoenixPushErrors.socketClosed();
|
|
195
|
+
}
|
|
196
|
+
throw new Error(step.message ?? 'scripted push failure');
|
|
197
|
+
},
|
|
198
|
+
wire: () => wires[wireIndex],
|
|
199
|
+
runnerPayload: scriptedEnvelope(caseInput),
|
|
200
|
+
runtimeMetadataForSourceSeq: (seq) =>
|
|
201
|
+
seq === undefined ? undefined : runtimeMetadata.get(seq),
|
|
202
|
+
sourceSeqForTurn: (turnId) => turnSeqs.get(turnId),
|
|
203
|
+
autoRetryAttemptsForSeq: (seq) => retryAttempts.get(seq) ?? 0,
|
|
204
|
+
midStreamResumeAttemptAtMsForSeq: (seq) => resumeAttempts.get(seq),
|
|
205
|
+
ladderHistoryForSeq: (seq, fate) => ladders.get(seq + ':' + fate),
|
|
206
|
+
isRecoveredTurn: (turnId) => recovered.has(turnId),
|
|
207
|
+
resolveSnapshot: async (turnId) => {
|
|
208
|
+
const queue = snapshotScript.get(turnId) ?? [];
|
|
209
|
+
const step = queue.shift() ?? { ok: true, items: [] };
|
|
210
|
+
trace.push({ t: 'resolveSnapshot', turnId, ok: step.ok === true });
|
|
211
|
+
if (step.delayMs !== undefined) {
|
|
212
|
+
await sleep(step.delayMs);
|
|
213
|
+
}
|
|
214
|
+
if (step.ok === true) {
|
|
215
|
+
return { items: step.items ?? [] };
|
|
216
|
+
}
|
|
217
|
+
throw new Error(step.message ?? 'scripted snapshot failure');
|
|
218
|
+
},
|
|
219
|
+
onTurnTerminal: (turnId, outcome, reason, retryableEmptyTurn, autoResume, fate) => {
|
|
220
|
+
trace.push({
|
|
221
|
+
t: 'terminal',
|
|
222
|
+
turnId,
|
|
223
|
+
outcome,
|
|
224
|
+
reason: reason ?? null,
|
|
225
|
+
retryableEmptyTurn: retryableEmptyTurn === true,
|
|
226
|
+
autoResumeMidStreamStall: autoResume === true,
|
|
227
|
+
fate: fate ?? null,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
log: (event, fields) => {
|
|
231
|
+
trace.push({ t: 'log', event, fields });
|
|
232
|
+
},
|
|
233
|
+
linzumiAgentSession: caseInput.linzumiAgentSession === true,
|
|
234
|
+
outboxRetryDelaysMs: caseInput.outboxRetryDelaysMs ?? [1, 2],
|
|
235
|
+
outboxMaxInFlightPushes: caseInput.maxInFlightPushes ?? 1,
|
|
236
|
+
snapshotRetryDelaysMs: caseInput.snapshotRetryDelaysMs ?? undefined,
|
|
237
|
+
// Determinism: the periodic stats interval is disabled; the reporter
|
|
238
|
+
// still stops on close in the same order as production.
|
|
239
|
+
statsIntervalMs: 0,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
for (const op of caseInput.ops ?? []) {
|
|
243
|
+
if (op.op === 'submit') pipeline.submitNotification(op.method, op.params ?? undefined);
|
|
244
|
+
else if (op.op === 'mapTurn') turnSeqs.set(op.turnId, op.seq);
|
|
245
|
+
else if (op.op === 'retryAttempts') retryAttempts.set(op.seq, op.attempts);
|
|
246
|
+
else if (op.op === 'resumeAttemptAt') resumeAttempts.set(op.seq, op.atMs);
|
|
247
|
+
else if (op.op === 'ladder') ladders.set(op.seq + ':' + op.fate, op.ladder);
|
|
248
|
+
else if (op.op === 'recover') recovered.add(op.turnId);
|
|
249
|
+
else if (op.op === 'notifyReady') pipeline.notifyTurnMappingReady(op.turnId);
|
|
250
|
+
else if (op.op === 'notifyFailed') pipeline.notifyTurnMappingFailed(op.turnId, op.reason);
|
|
251
|
+
else if (op.op === 'noteActivity') pipeline.noteTurnActivity(op.turnId, op.kind);
|
|
252
|
+
else if (op.op === 'lifecycle') pipeline.submitLifecycle(op.kind, op.reason);
|
|
253
|
+
else if (op.op === 'pause') pipeline.pause();
|
|
254
|
+
else if (op.op === 'resume') pipeline.resume();
|
|
255
|
+
else if (op.op === 'rebind') pipeline.rebindThreadKey(op.threadKey);
|
|
256
|
+
else if (op.op === 'setWire') wireIndex = op.index;
|
|
257
|
+
else if (op.op === 'sleep') await sleep(op.ms);
|
|
258
|
+
else if (op.op === 'metrics') {
|
|
259
|
+
const metrics = pipeline.metrics();
|
|
260
|
+
trace.push({
|
|
261
|
+
t: 'metrics',
|
|
262
|
+
value: {
|
|
263
|
+
depth: metrics.depth,
|
|
264
|
+
totalEnqueued: metrics.totalEnqueued,
|
|
265
|
+
totalAcked: metrics.totalAcked,
|
|
266
|
+
totalRetried: metrics.totalRetried,
|
|
267
|
+
totalDroppedBestEffort: metrics.totalDroppedBestEffort,
|
|
268
|
+
totalPermanentFailures: metrics.totalPermanentFailures,
|
|
269
|
+
paused: metrics.paused,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
} else if (op.op === 'flush') {
|
|
273
|
+
try {
|
|
274
|
+
await pipeline.flushOutbox(op.timeoutMs ?? undefined);
|
|
275
|
+
trace.push({ t: 'flushed', ok: true });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
trace.push({
|
|
278
|
+
t: 'flushed',
|
|
279
|
+
ok: false,
|
|
280
|
+
message: error instanceof Error ? error.message : String(error),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
} else if (op.op === 'close') {
|
|
284
|
+
await pipeline.close(op.timeoutMs ?? undefined);
|
|
285
|
+
trace.push({ t: 'closed' });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { trace };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function runCase(input) {
|
|
292
|
+
switch (input.kind) {
|
|
293
|
+
case 'constants':
|
|
294
|
+
return {
|
|
295
|
+
defaultOutboxMaxInFlightPushes,
|
|
296
|
+
outboxEntryKinds,
|
|
297
|
+
};
|
|
298
|
+
case 'builders':
|
|
299
|
+
return runBuildersCase(input);
|
|
300
|
+
case 'classifier':
|
|
301
|
+
return runClassifierCase(input);
|
|
302
|
+
case 'rawItems':
|
|
303
|
+
return rawSnapshotItemsForTurn(input.response, input.turnId);
|
|
304
|
+
case 'preReduce':
|
|
305
|
+
return preReduceSnapshotItems(input.items);
|
|
306
|
+
case 'merge':
|
|
307
|
+
return mergeSnapshotItems(input.readItems, input.sessionItems);
|
|
308
|
+
case 'sessionRun':
|
|
309
|
+
return runSessionCase(input);
|
|
310
|
+
default:
|
|
311
|
+
return { error: 'unknown case kind ' + String(input.kind) };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function runBatch() {
|
|
316
|
+
const chunks = [];
|
|
317
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
318
|
+
const cases = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
319
|
+
const results = [];
|
|
320
|
+
for (const testCase of cases) {
|
|
321
|
+
results.push(await runCase(testCase));
|
|
322
|
+
}
|
|
323
|
+
process.stdout.write(JSON.stringify(results), () => {
|
|
324
|
+
process.exit(0);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
runBatch().then(
|
|
329
|
+
() => {},
|
|
330
|
+
(error) => {
|
|
331
|
+
process.stderr.write(String(error && error.stack ? error.stack : error) + '\\n');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
const outDir = join(packageRoot, '.differential');
|
|
338
|
+
const outPath = join(outDir, 'core-integration-parity-oracle.mjs');
|
|
339
|
+
|
|
340
|
+
await mkdir(outDir, { recursive: true });
|
|
341
|
+
|
|
342
|
+
await build({
|
|
343
|
+
stdin: {
|
|
344
|
+
contents: driverSource,
|
|
345
|
+
resolveDir: packageRoot,
|
|
346
|
+
sourcefile: 'core-integration-parity-driver.ts',
|
|
347
|
+
loader: 'ts',
|
|
348
|
+
},
|
|
349
|
+
bundle: true,
|
|
350
|
+
platform: 'node',
|
|
351
|
+
target: 'node20',
|
|
352
|
+
format: 'esm',
|
|
353
|
+
outfile: outPath,
|
|
354
|
+
minify: false,
|
|
355
|
+
sourcemap: false,
|
|
356
|
+
legalComments: 'none',
|
|
357
|
+
logLevel: 'silent',
|
|
358
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
process.stdout.write(outPath + '\n');
|