@linzumi/cli 1.0.111 → 1.0.112
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/index.js +1 -1
- 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 +250 -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/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -1473,7 +1473,7 @@ To kick the agent off:
|
|
|
1473
1473
|
`,L=Buffer.byteLength(P,"utf8");if(L>s)return l("runner_diagnostics.spool_entry_dropped",{reason:"entry_too_large",entry_bytes:L,max_entry_bytes:s}),!1;jSe(n,{recursive:!0}),m(u)+L>i&&f(),_(L);let D=w()?`
|
|
1474
1474
|
`:"";return $Se(u,`${D}${P}`,"utf8"),!0}catch(R){return l("runner_diagnostics.spool_failed",{error:R instanceof Error?R.message:String(R)}),!1}},v=k=>{let R=[],P=zSe(k,"utf8");for(let L of P.split(`
|
|
1475
1475
|
`))if(L.trim()!=="")try{let D=JSON.parse(L);D!==null&&typeof D=="object"&&R.push({parsed:D,raw:L})}catch{}return R};return{append:S,drain:async k=>{if(c)return{uploadedBatches:0,uploadedEntries:0,remainingBatches:p().length,skipped:!0};c=!0;let R=0,P=0;try{f(),_(0);let L=a()-o;for(let D of p()){let j;try{j=v(D)}catch(Z){l("runner_diagnostics.spool_batch_unreadable",{file:D,error:Z instanceof Error?Z.message:String(Z)}),y(D,"unreadable");continue}let z=j.filter(Z=>{let me=tke(Z.parsed);return me===void 0||me>=L});if(z.length===0){y(D,"empty_or_expired");continue}let E=hP(D,".jsonl"),B=z.map(Z=>Z.raw).join(`
|
|
1476
|
-
`),q=z.map(Z=>Z.parsed.spooled_at).filter(Z=>typeof Z=="string"),W={bundle_name:"runner-diagnostics-spool",exported_at:new Date(a()).toISOString(),idempotency_key:E,diagnostics:{kind:"runner_diagnostic_log",spool_batch:!0,idempotency_key:E,entry_count:z.length,...q.length===0?{}:{first_spooled_at:q[0],last_spooled_at:q[q.length-1]}},telemetry_ndjson:B};try{await k(W)}catch(Z){l("runner_diagnostics.spool_upload_failed",{file:D,remaining_batches:p().length,error:Z instanceof Error?Z.message:String(Z)});break}R+=1,P+=z.length;try{F9(D)}catch(Z){l("runner_diagnostics.spool_delete_failed",{file:D,error:Z instanceof Error?Z.message:String(Z)})}}}catch(L){l("runner_diagnostics.spool_drain_failed",{error:L instanceof Error?L.message:String(L)})}finally{c=!1}return{uploadedBatches:R,uploadedEntries:P,remainingBatches:p().length,skipped:!1}},pendingBatchFiles:p}}var KSe,VSe,WSe,HSe,GSe,z9,QSe,XSe,N9=F(()=>{"use strict";ax();KSe=32*1024*1024,VSe=10080*60*1e3,WSe=4*1024*1024,HSe=1024*1024,GSe="spool.jsonl",z9=/^batch-(\d{1,16})-[0-9a-f-]+\.jsonl$/;QSe=/\b([A-Z][A-Z0-9_]{1,})=("[^"\n]*"|'[^'\n]*'|[^\s"'`;,)\]}]+)/g,XSe=/(?:\/(?:[A-Za-z0-9._@~+-]|%[0-9A-Fa-f]{2})+){2,}/g});var qt,yP,ta=F(()=>{"use strict";qt="1.0.
|
|
1476
|
+
`),q=z.map(Z=>Z.parsed.spooled_at).filter(Z=>typeof Z=="string"),W={bundle_name:"runner-diagnostics-spool",exported_at:new Date(a()).toISOString(),idempotency_key:E,diagnostics:{kind:"runner_diagnostic_log",spool_batch:!0,idempotency_key:E,entry_count:z.length,...q.length===0?{}:{first_spooled_at:q[0],last_spooled_at:q[q.length-1]}},telemetry_ndjson:B};try{await k(W)}catch(Z){l("runner_diagnostics.spool_upload_failed",{file:D,remaining_batches:p().length,error:Z instanceof Error?Z.message:String(Z)});break}R+=1,P+=z.length;try{F9(D)}catch(Z){l("runner_diagnostics.spool_delete_failed",{file:D,error:Z instanceof Error?Z.message:String(Z)})}}}catch(L){l("runner_diagnostics.spool_drain_failed",{error:L instanceof Error?L.message:String(L)})}finally{c=!1}return{uploadedBatches:R,uploadedEntries:P,remainingBatches:p().length,skipped:!1}},pendingBatchFiles:p}}var KSe,VSe,WSe,HSe,GSe,z9,QSe,XSe,N9=F(()=>{"use strict";ax();KSe=32*1024*1024,VSe=10080*60*1e3,WSe=4*1024*1024,HSe=1024*1024,GSe="spool.jsonl",z9=/^batch-(\d{1,16})-[0-9a-f-]+\.jsonl$/;QSe=/\b([A-Z][A-Z0-9_]{1,})=("[^"\n]*"|'[^'\n]*'|[^\s"'`;,)\]}]+)/g,XSe=/(?:\/(?:[A-Za-z0-9._@~+-]|%[0-9A-Fa-f]{2})+){2,}/g});var qt,yP,ta=F(()=>{"use strict";qt="1.0.112",yP=`linzumi ${qt}`});import{closeSync as B9,existsSync as _P,mkdirSync as rke,openSync as J9,readFileSync as K9,renameSync as oke,unlinkSync as _d,writeFileSync as ike,writeSync as V9}from"node:fs";import{dirname as W9,join as ske}from"node:path";function bP(e){return e instanceof ys||e instanceof Error&&e.name===G9&&"lockPath"in e&&"heldBy"in e}function wP(e,n=Wn(),t){let r=t===void 0?encodeURIComponent(e):ld(t);return ske(W9(n),"runners",`${r}.lock`)}function x_(e){let n=wP(e.machineId,e.configPath,e.linzumiUrl),t=e.isPidAlive??t3,r=e.now??(()=>new Date),o=e.killPid??_ke,i=r().toISOString(),s={version:1,machineId:e.machineId,runnerId:e.runnerId,pid:e.pid??process.pid,cwd:e.cwd,workspace:e.workspace,...e.linzumiUrl===void 0?{}:{linzumiUrl:zn(e.linzumiUrl)},...e.launchSource===void 0?{}:{launchSource:e.launchSource},startedAt:i,heartbeatAt:i,cliVersion:e.cliVersion??qt,...e.gracefulUpdateDrain===!0?{gracefulUpdateDrain:!0}:{}};cke(e.machineId,e.configPath,e.linzumiUrl,t,r,o,e.onTakeover),lke(n,s,t,r,o,e.onTakeover,e.beforeReadExistingLock,e.beforeReplaceStaleLock);let a=()=>Z9(n,s,r),d=setInterval(a,H9);return d.unref(),{path:n,record:s,heartbeat:a,release:()=>{clearInterval(d),e3(n,s)}}}function Z9(e,n,t){try{let r=b_(e);if(r===void 0||r.machineId!==n.machineId||r.runnerId!==n.runnerId||r.pid!==n.pid||r.startedAt!==n.startedAt)return;let o={...r,heartbeatAt:t().toISOString()},i=`${e}.tmp`;ike(i,`${JSON.stringify(o,null,2)}
|
|
1477
1477
|
`,"utf8"),oke(i,e)}catch{}}function bd(e,n,t){if(!n(e.pid))return{state:"dead"};let r=e.heartbeatAt??e.startedAt,o=Date.parse(r);return Number.isNaN(o)||t().getTime()-o>ake?{state:"wedged",reason:e.heartbeatAt===void 0?"missing_heartbeat":"stale_heartbeat"}:{state:"live"}}function Q9(e,n,t,r){return t!==__||e.launchSource!==__?!1:r.state!=="live"?!0:dke(n,e.cliVersion)>0}function dke(e,n){let t=U9(e),r=U9(n);for(let o=0;o<3;o++){let i=t.core[o],s=r.core[o];if(i===void 0||s===void 0)throw new Error(`compareCliVersions: unexpected undefined at core index ${o}`);if(i!==s)return i>s?1:-1}return t.prerelease.length===0&&r.prerelease.length===0?0:t.prerelease.length===0?1:r.prerelease.length===0?-1:uke(t.prerelease,r.prerelease)}function U9(e){let n=e.trim().replace(/^v/i,"").split("+",1)[0]??"",t=n.indexOf("-"),r=t===-1?n:n.slice(0,t),o=t===-1?"":n.slice(t+1),i=r.split("."),s=[xP(i[0]),xP(i[1]),xP(i[2])],a=o===""?[]:o.split(".").filter(d=>d!=="");return{core:s,prerelease:a}}function xP(e){let n=Number.parseInt(e??"",10);return Number.isInteger(n)&&n>=0?n:0}function uke(e,n){let t=Math.max(e.length,n.length);for(let r=0;r<t;r++){let o=e[r],i=n[r];if(o===void 0)return-1;if(i===void 0)return 1;let s=Number.parseInt(o,10),a=Number.parseInt(i,10),d=s.toString()===o,u=a.toString()===i;if(d&&u){if(s!==a)return s>a?1:-1}else{if(d!==u)return d?-1:1;if(o!==i)return o>i?1:-1}}return 0}function X9(e,n,t,r,o){try{r(n.pid)}catch(i){let s=i!==null&&typeof i=="object"&&"code"in i?i.code:void 0;if(s!=="ESRCH")throw s==="EPERM"||s==="EACCES"?new Error(`Cannot take over the runner lock at ${e}: its holder process (pid ${n.pid}) is owned by another user and cannot be stopped. Stop that process, or remove the lock file, then retry.`):i}o?.({lockPath:e,holderPid:n.pid,holderRunnerId:n.runnerId,reason:t})}function cke(e,n,t,r,o,i,s){if(t===void 0||yr(t)!==yr(Jt))return;let a=wP(e,n),d=b_(a);if(d!==void 0){if(bd(d,r,o).state==="live")throw new ys(a,d);Y9(a,r,()=>{let u=_P(a)?vP(a):void 0;if(u===void 0)return;let c=bd(u,r,o);if(c.state==="live")throw new ys(a,u);c.state==="wedged"&&X9(a,u,c.reason,i,s),_d(a)})}}function lke(e,n,t,r,o,i,s,a){if(y_(e,n))return;s?.();let d=b_(e);if(d===void 0){if(y_(e,n))return;throw new Error(`another Linzumi runner lock appeared while starting: ${e}`)}if(bd(d,t,r).state==="live")throw new ys(e,d);a?.(),Y9(e,t,()=>{let u=_P(e)?vP(e):void 0;if(u!==void 0){let c=bd(u,t,r);if(c.state==="live")throw new ys(e,u);c.state==="wedged"&&X9(e,u,c.reason,o,i),_d(e)}if(!y_(e,n))throw new Error(`another Linzumi runner lock appeared while starting: ${e}`)})}function y_(e,n){rke(W9(e),{recursive:!0});try{let t=J9(e,"wx");try{V9(t,`${JSON.stringify(n,null,2)}
|
|
1478
1478
|
`)}finally{B9(t)}return!0}catch(t){if(Vc(t,"EEXIST"))return!1;throw t}}function Y9(e,n,t){let r=`${e}.replace`;for(;;)try{let o=J9(r,"wx");try{V9(o,`${process.pid}
|
|
1479
1479
|
`),t()}finally{B9(o),_d(r)}return}catch(o){if(Vc(o,"EEXIST")){let i=pke(r);if(i===void 0)continue;if(n(i))throw new Error(["another Linzumi runner is already replacing a stale runner lock",`lock: ${e}`,"Wait for that startup to finish, then retry."].join(`
|
package/package.json
CHANGED
|
@@ -0,0 +1,586 @@
|
|
|
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 three bounded
|
|
4
|
+
// commander-core seams of the ReScript commander port, the sibling of
|
|
5
|
+
// build-queue-parity-oracle.mjs (cluster 5) /
|
|
6
|
+
// build-transport-parity-oracle.mjs (cluster 4). ONE bundle imports the
|
|
7
|
+
// REAL commander-core exports from './src/runner' (the whole module graph
|
|
8
|
+
// rides along - large by design, exactly like the transport/config oracles
|
|
9
|
+
// that already import runner exports) behind the one-shot batch driver: a
|
|
10
|
+
// JSON array of cases on stdin, a JSON array of results on stdout.
|
|
11
|
+
//
|
|
12
|
+
// The three seams:
|
|
13
|
+
// 1. Control-dispatch dedup + commit-then-advance confirmation
|
|
14
|
+
// (staleStartInstanceWindowMs / staleStartInstanceAgeMs /
|
|
15
|
+
// shouldSkipReplayedStartInstanceForKnownSession /
|
|
16
|
+
// shouldSkipReplayedStartTurn / commitAppliedStartTurn /
|
|
17
|
+
// dispatchControlConfirmingStartTurn / settleControlDispatch).
|
|
18
|
+
// 2. The connection-staleness tracker (constants +
|
|
19
|
+
// createConnectionStalenessTracker) driven by scripted clocks and
|
|
20
|
+
// scripted escalation callbacks.
|
|
21
|
+
// 3. The app-server KILL/RESPAWN ladder (createCodexTurnFailureGuard,
|
|
22
|
+
// runnerOwnsRespawnableAppServer, awaitLossReportsThenExit, the
|
|
23
|
+
// crash-loop constants/store/classifier, appServerLossCwdDiagnosis).
|
|
24
|
+
//
|
|
25
|
+
// Determinism: clocks are scripted per case (every seam takes an injected
|
|
26
|
+
// now/nowMs), timers are scripted callbacks, scripted promises resolve in
|
|
27
|
+
// program order with explicit microtask flushes, and scratch paths are
|
|
28
|
+
// normalized through the "$DIR" placeholder in both directions. The
|
|
29
|
+
// turn-failure guard's lastActivityAtMs is wall-clock-stamped inside the TS
|
|
30
|
+
// original, so both impls report it as a PRESENCE flag only.
|
|
31
|
+
//
|
|
32
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
33
|
+
// payloads; nothing here logs case contents.
|
|
34
|
+
//
|
|
35
|
+
// The output is a local test artifact (.differential/ is gitignored, never
|
|
36
|
+
// published) and stays unminified for debuggability.
|
|
37
|
+
import { copyFile, mkdir } from 'node:fs/promises';
|
|
38
|
+
import { dirname, join } from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
import { build } from 'esbuild';
|
|
41
|
+
|
|
42
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
43
|
+
|
|
44
|
+
const driverSource = `
|
|
45
|
+
import {
|
|
46
|
+
mkdtempSync,
|
|
47
|
+
readdirSync,
|
|
48
|
+
readFileSync,
|
|
49
|
+
realpathSync,
|
|
50
|
+
statSync,
|
|
51
|
+
writeFileSync,
|
|
52
|
+
} from 'node:fs';
|
|
53
|
+
import { tmpdir } from 'node:os';
|
|
54
|
+
import { join, relative } from 'node:path';
|
|
55
|
+
import {
|
|
56
|
+
appServerCrashLoopExitCode,
|
|
57
|
+
appServerCrashLoopExitLimit,
|
|
58
|
+
appServerCrashLoopWindowMs,
|
|
59
|
+
appServerLossCwdDiagnosis,
|
|
60
|
+
awaitLossReportsThenExit,
|
|
61
|
+
commitAppliedStartTurn,
|
|
62
|
+
connectionStaleForcedReconnectAfterMs,
|
|
63
|
+
connectionStaleForcedReconnectRetryMs,
|
|
64
|
+
connectionStaleHardResetAfterUninitiatedDispatches,
|
|
65
|
+
connectionStaleHardResetFailureLimit,
|
|
66
|
+
connectionStaleThresholdMs,
|
|
67
|
+
connectionUnrecoverableAfterMs,
|
|
68
|
+
createCodexTurnFailureGuard,
|
|
69
|
+
createConnectionStalenessTracker,
|
|
70
|
+
defaultAppServerExitLoopStatePath,
|
|
71
|
+
dispatchControlConfirmingStartTurn,
|
|
72
|
+
fileAppServerExitLoopStore,
|
|
73
|
+
recordAppServerExitAndClassify,
|
|
74
|
+
runnerOwnsRespawnableAppServer,
|
|
75
|
+
settleControlDispatch,
|
|
76
|
+
shouldSkipReplayedStartInstanceForKnownSession,
|
|
77
|
+
shouldSkipReplayedStartTurn,
|
|
78
|
+
staleStartInstanceAgeMs,
|
|
79
|
+
staleStartInstanceWindowMs,
|
|
80
|
+
} from './src/runner';
|
|
81
|
+
|
|
82
|
+
// --- Placeholder + snapshot plumbing --------------------------------------------
|
|
83
|
+
|
|
84
|
+
const dirPlaceholder = '$DIR';
|
|
85
|
+
|
|
86
|
+
function scratch() {
|
|
87
|
+
// realpath so macOS /var -> /private/var never splits the two impls'
|
|
88
|
+
// placeholder substitution.
|
|
89
|
+
return realpathSync(mkdtempSync(join(tmpdir(), 'linzumi-core-oracle-')));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function substitute(value, from, to) {
|
|
93
|
+
if (typeof value === 'string') {
|
|
94
|
+
return value.split(from).join(to);
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
return value.map((entry) => substitute(entry, from, to));
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === 'object' && value !== null) {
|
|
100
|
+
return Object.fromEntries(
|
|
101
|
+
Object.entries(value).map(([key, entry]) => [
|
|
102
|
+
substitute(key, from, to),
|
|
103
|
+
substitute(entry, from, to),
|
|
104
|
+
])
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function snapshotFiles(dir) {
|
|
111
|
+
const files = {};
|
|
112
|
+
const walk = (current) => {
|
|
113
|
+
for (const name of readdirSync(current).sort()) {
|
|
114
|
+
const full = join(current, name);
|
|
115
|
+
if (statSync(full).isDirectory()) {
|
|
116
|
+
walk(full);
|
|
117
|
+
} else {
|
|
118
|
+
files[relative(dir, full)] = readFileSync(full, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
walk(dir);
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function outcome(run) {
|
|
127
|
+
try {
|
|
128
|
+
const value = run();
|
|
129
|
+
return value === undefined ? { defined: false } : { defined: true, value };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
threw: true,
|
|
133
|
+
message: error instanceof Error ? error.message : String(error),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectingLog(events) {
|
|
139
|
+
return (event, payload) => {
|
|
140
|
+
events.push({ trace: 'log', event, payload });
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Deterministic microtask flush: enough passes that a then-chain of any
|
|
145
|
+
// scripted depth settles before the trace is read on both impls.
|
|
146
|
+
async function flushMicrotasks() {
|
|
147
|
+
for (let i = 0; i < 8; i += 1) {
|
|
148
|
+
await Promise.resolve();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Slice 1: control-dispatch dedup + commit-then-advance -----------------------
|
|
153
|
+
|
|
154
|
+
async function handleControlDispatch(testCase) {
|
|
155
|
+
switch (testCase.fn) {
|
|
156
|
+
case 'constants':
|
|
157
|
+
return { staleStartInstanceWindowMs };
|
|
158
|
+
case 'staleAge':
|
|
159
|
+
return outcome(() =>
|
|
160
|
+
staleStartInstanceAgeMs(testCase.control, testCase.nowMs)
|
|
161
|
+
);
|
|
162
|
+
case 'skipKnownSession': {
|
|
163
|
+
const lookups = [];
|
|
164
|
+
const known = new Set(testCase.knownThreadIds ?? []);
|
|
165
|
+
const result = outcome(() =>
|
|
166
|
+
shouldSkipReplayedStartInstanceForKnownSession(
|
|
167
|
+
testCase.control,
|
|
168
|
+
(threadId) => {
|
|
169
|
+
lookups.push(threadId);
|
|
170
|
+
return known.has(threadId);
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
return { result, lookups };
|
|
175
|
+
}
|
|
176
|
+
case 'skipStartTurn':
|
|
177
|
+
return outcome(() =>
|
|
178
|
+
shouldSkipReplayedStartTurn(
|
|
179
|
+
testCase.applied ?? undefined,
|
|
180
|
+
testCase.controlSeq
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
case 'commit': {
|
|
184
|
+
const calls = [];
|
|
185
|
+
const store = {
|
|
186
|
+
onAdvance: (key, seq) => calls.push({ call: 'onAdvance', key, seq }),
|
|
187
|
+
flush: () => calls.push({ call: 'flush' }),
|
|
188
|
+
};
|
|
189
|
+
const result = outcome(() =>
|
|
190
|
+
commitAppliedStartTurn(store, testCase.watermarkKey, testCase.controlSeq)
|
|
191
|
+
);
|
|
192
|
+
return { result, calls };
|
|
193
|
+
}
|
|
194
|
+
case 'dispatchConfirm': {
|
|
195
|
+
const trace = [];
|
|
196
|
+
const dispatch = () => {
|
|
197
|
+
trace.push({ trace: 'dispatch_called' });
|
|
198
|
+
return testCase.dispatch.kind === 'resolve'
|
|
199
|
+
? Promise.resolve(testCase.dispatch.value ?? null)
|
|
200
|
+
: Promise.reject(
|
|
201
|
+
new Error(String(testCase.dispatch.message ?? 'scripted dispatch rejection'))
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
const commit =
|
|
205
|
+
testCase.hasCommit === true
|
|
206
|
+
? () => trace.push({ trace: 'commit_called' })
|
|
207
|
+
: undefined;
|
|
208
|
+
try {
|
|
209
|
+
const value = await dispatchControlConfirmingStartTurn(dispatch, commit);
|
|
210
|
+
trace.push({ trace: 'resolved', value: value ?? null });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
trace.push({
|
|
213
|
+
trace: 'rejected',
|
|
214
|
+
message: error instanceof Error ? error.message : String(error),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return { trace };
|
|
218
|
+
}
|
|
219
|
+
case 'settle': {
|
|
220
|
+
const trace = [];
|
|
221
|
+
const log = collectingLog(trace);
|
|
222
|
+
const handler = testCase.handler ?? { kind: 'syncUndefined' };
|
|
223
|
+
const handle = () => {
|
|
224
|
+
trace.push({ trace: 'handle_called' });
|
|
225
|
+
switch (handler.kind) {
|
|
226
|
+
case 'syncThrow':
|
|
227
|
+
throw new Error(String(handler.message ?? 'scripted sync throw'));
|
|
228
|
+
case 'syncObject':
|
|
229
|
+
return handler.value ?? {};
|
|
230
|
+
case 'syncUndefined':
|
|
231
|
+
return undefined;
|
|
232
|
+
case 'asyncResolve':
|
|
233
|
+
return Promise.resolve(handler.value ?? null);
|
|
234
|
+
case 'asyncReject':
|
|
235
|
+
return Promise.reject(
|
|
236
|
+
new Error(String(handler.message ?? 'scripted async rejection'))
|
|
237
|
+
);
|
|
238
|
+
default:
|
|
239
|
+
throw new Error('unknown settle handler kind ' + String(handler.kind));
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const onRejected =
|
|
243
|
+
testCase.onRejected === undefined
|
|
244
|
+
? undefined
|
|
245
|
+
: (error) => {
|
|
246
|
+
trace.push({
|
|
247
|
+
trace: 'on_rejected',
|
|
248
|
+
message: error instanceof Error ? error.message : String(error),
|
|
249
|
+
});
|
|
250
|
+
return testCase.onRejected;
|
|
251
|
+
};
|
|
252
|
+
settleControlDispatch(
|
|
253
|
+
testCase.controlType ?? 'scripted_control',
|
|
254
|
+
handle,
|
|
255
|
+
() => trace.push({ trace: 'ack' }),
|
|
256
|
+
log,
|
|
257
|
+
onRejected
|
|
258
|
+
);
|
|
259
|
+
trace.push({ trace: 'settle_returned' });
|
|
260
|
+
await flushMicrotasks();
|
|
261
|
+
return { trace };
|
|
262
|
+
}
|
|
263
|
+
default:
|
|
264
|
+
return { threw: true, message: 'unknown controlDispatch fn ' + String(testCase.fn) };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- Slice 2: connection-staleness tracker ----------------------------------------
|
|
269
|
+
|
|
270
|
+
function handleStaleness(testCase) {
|
|
271
|
+
if (testCase.fn === 'constants') {
|
|
272
|
+
return {
|
|
273
|
+
connectionStaleThresholdMs,
|
|
274
|
+
connectionStaleForcedReconnectAfterMs,
|
|
275
|
+
connectionStaleForcedReconnectRetryMs,
|
|
276
|
+
connectionStaleHardResetAfterUninitiatedDispatches,
|
|
277
|
+
connectionStaleHardResetFailureLimit,
|
|
278
|
+
connectionUnrecoverableAfterMs,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// fn === 'scenario': a scripted clock + scripted escalation callbacks.
|
|
282
|
+
const trace = [];
|
|
283
|
+
const log = collectingLog(trace);
|
|
284
|
+
let nowMs = testCase.startMs ?? 0;
|
|
285
|
+
const escalationSpec = testCase.escalation;
|
|
286
|
+
const reconnectReturns = [...(escalationSpec?.reconnectReturns ?? [])];
|
|
287
|
+
const hardResetReturns = [...(escalationSpec?.hardResetReturns ?? [])];
|
|
288
|
+
const escalation =
|
|
289
|
+
escalationSpec === undefined
|
|
290
|
+
? undefined
|
|
291
|
+
: {
|
|
292
|
+
...(escalationSpec.hasReconnect === true
|
|
293
|
+
? {
|
|
294
|
+
onReconnectNeeded: (details) => {
|
|
295
|
+
trace.push({ trace: 'reconnect_needed', details });
|
|
296
|
+
const scripted = reconnectReturns.shift();
|
|
297
|
+
return scripted === null || scripted === undefined
|
|
298
|
+
? undefined
|
|
299
|
+
: scripted;
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
: {}),
|
|
303
|
+
...(escalationSpec.hasHardReset === true
|
|
304
|
+
? {
|
|
305
|
+
onHardResetNeeded: (details) => {
|
|
306
|
+
trace.push({ trace: 'hard_reset_needed', details });
|
|
307
|
+
const scripted = hardResetReturns.shift();
|
|
308
|
+
return scripted === null || scripted === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: scripted;
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
: {}),
|
|
314
|
+
...(escalationSpec.forcedReconnectAfterMs === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { forcedReconnectAfterMs: escalationSpec.forcedReconnectAfterMs }),
|
|
317
|
+
...(escalationSpec.forcedReconnectRetryMs === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: { forcedReconnectRetryMs: escalationSpec.forcedReconnectRetryMs }),
|
|
320
|
+
onUnrecoverable: (details) => {
|
|
321
|
+
trace.push({ trace: 'unrecoverable', details });
|
|
322
|
+
},
|
|
323
|
+
...(escalationSpec.afterMs === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { afterMs: escalationSpec.afterMs }),
|
|
326
|
+
};
|
|
327
|
+
const tracker = createConnectionStalenessTracker(
|
|
328
|
+
log,
|
|
329
|
+
() => nowMs,
|
|
330
|
+
testCase.thresholdMs ?? undefined,
|
|
331
|
+
escalation
|
|
332
|
+
);
|
|
333
|
+
for (const step of testCase.steps ?? []) {
|
|
334
|
+
switch (step.fn) {
|
|
335
|
+
case 'advance':
|
|
336
|
+
nowMs += step.byMs;
|
|
337
|
+
break;
|
|
338
|
+
case 'ack':
|
|
339
|
+
tracker.recordServerAck();
|
|
340
|
+
break;
|
|
341
|
+
case 'check':
|
|
342
|
+
tracker.check();
|
|
343
|
+
break;
|
|
344
|
+
case 'isStale':
|
|
345
|
+
trace.push({ trace: 'is_stale', value: tracker.isStale() });
|
|
346
|
+
break;
|
|
347
|
+
default:
|
|
348
|
+
trace.push({ trace: 'unknown_step', fn: String(step.fn) });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { trace };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Slice 3: respawn ladder ---------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
async function handleRespawn(testCase) {
|
|
357
|
+
switch (testCase.fn) {
|
|
358
|
+
case 'constants':
|
|
359
|
+
return {
|
|
360
|
+
appServerCrashLoopWindowMs,
|
|
361
|
+
appServerCrashLoopExitLimit,
|
|
362
|
+
appServerCrashLoopExitCode,
|
|
363
|
+
};
|
|
364
|
+
case 'ownsRespawnable':
|
|
365
|
+
return outcome(() =>
|
|
366
|
+
runnerOwnsRespawnableAppServer({
|
|
367
|
+
codexUrl: testCase.codexUrl ?? undefined,
|
|
368
|
+
threadProcessRole: testCase.threadProcessRole ?? undefined,
|
|
369
|
+
ownsStartedAppServer: testCase.ownsStartedAppServer === true,
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
case 'statePath':
|
|
373
|
+
return outcome(() =>
|
|
374
|
+
defaultAppServerExitLoopStatePath(testCase.runnerId, testCase.env ?? {})
|
|
375
|
+
);
|
|
376
|
+
case 'cwdDiagnosis': {
|
|
377
|
+
const existsMode = testCase.existsMode ?? 'real';
|
|
378
|
+
const existsFn =
|
|
379
|
+
existsMode === 'real'
|
|
380
|
+
? undefined
|
|
381
|
+
: () => {
|
|
382
|
+
if (existsMode === 'throw') {
|
|
383
|
+
throw new Error('scripted exists probe failure');
|
|
384
|
+
}
|
|
385
|
+
return existsMode === 'true';
|
|
386
|
+
};
|
|
387
|
+
return outcome(() =>
|
|
388
|
+
existsFn === undefined
|
|
389
|
+
? appServerLossCwdDiagnosis(testCase.cwd)
|
|
390
|
+
: appServerLossCwdDiagnosis(testCase.cwd, existsFn)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
case 'exitLoopScenario': {
|
|
394
|
+
const dir = scratch();
|
|
395
|
+
const path = join(dir, 'exit-loop.json');
|
|
396
|
+
if (testCase.initialContents !== undefined) {
|
|
397
|
+
writeFileSync(path, testCase.initialContents, 'utf8');
|
|
398
|
+
}
|
|
399
|
+
const writeErrors = [];
|
|
400
|
+
const store = fileAppServerExitLoopStore(path, (message) =>
|
|
401
|
+
writeErrors.push(message)
|
|
402
|
+
);
|
|
403
|
+
const results = (testCase.steps ?? []).map((step) => {
|
|
404
|
+
switch (step.fn) {
|
|
405
|
+
case 'record':
|
|
406
|
+
return outcome(() =>
|
|
407
|
+
recordAppServerExitAndClassify(
|
|
408
|
+
store,
|
|
409
|
+
step.nowMs,
|
|
410
|
+
step.windowMs ?? undefined,
|
|
411
|
+
step.limit ?? undefined
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
case 'load':
|
|
415
|
+
return outcome(() => store.load());
|
|
416
|
+
case 'save':
|
|
417
|
+
return outcome(() => store.save(step.record));
|
|
418
|
+
default:
|
|
419
|
+
return { threw: true, message: 'unknown exitLoop fn ' + String(step.fn) };
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
return substitute(
|
|
423
|
+
{ results, writeErrors, files: snapshotFiles(dir) },
|
|
424
|
+
dir,
|
|
425
|
+
dirPlaceholder
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
case 'guardScenario': {
|
|
429
|
+
const trace = [];
|
|
430
|
+
const log = collectingLog(trace);
|
|
431
|
+
const sessionSpecs = testCase.sessions ?? [];
|
|
432
|
+
const sessions = sessionSpecs.map((spec, index) => ({
|
|
433
|
+
handleCodexNotification: (method, params) => {
|
|
434
|
+
trace.push({ trace: 'session_dispatch', session: index, method, params });
|
|
435
|
+
if (spec.throws === true) {
|
|
436
|
+
throw new Error('scripted session dispatch failure ' + String(index));
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
}));
|
|
440
|
+
const guard = createCodexTurnFailureGuard({
|
|
441
|
+
sessions: () => sessions,
|
|
442
|
+
log,
|
|
443
|
+
...(testCase.hasOnActiveThreadsFailed === true
|
|
444
|
+
? {
|
|
445
|
+
onActiveThreadsFailed: (threadIds) => {
|
|
446
|
+
trace.push({ trace: 'active_threads_failed', threadIds: [...threadIds] });
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
});
|
|
451
|
+
for (const step of testCase.steps ?? []) {
|
|
452
|
+
switch (step.fn) {
|
|
453
|
+
case 'observe':
|
|
454
|
+
guard.observeNotification(step.method, step.params ?? {});
|
|
455
|
+
break;
|
|
456
|
+
case 'fail':
|
|
457
|
+
guard.failActiveTurns(String(step.cause ?? 'scripted cause'));
|
|
458
|
+
break;
|
|
459
|
+
case 'failPerTurn': {
|
|
460
|
+
const causes = step.causes ?? {};
|
|
461
|
+
guard.failActiveTurns(
|
|
462
|
+
(turn) =>
|
|
463
|
+
causes[turn.threadId + '|' + turn.turnId] ??
|
|
464
|
+
String(step.defaultCause ?? 'scripted default cause')
|
|
465
|
+
);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case 'count':
|
|
469
|
+
trace.push({ trace: 'count', value: guard.activeTurnCount() });
|
|
470
|
+
break;
|
|
471
|
+
case 'turns':
|
|
472
|
+
trace.push({ trace: 'turns', value: guard.activeTurns().map((t) => ({ ...t })) });
|
|
473
|
+
break;
|
|
474
|
+
case 'lastActivity':
|
|
475
|
+
// Wall-clock stamped inside the TS original: presence only.
|
|
476
|
+
trace.push({
|
|
477
|
+
trace: 'last_activity_defined',
|
|
478
|
+
value: guard.lastActivityAtMs() !== undefined,
|
|
479
|
+
});
|
|
480
|
+
break;
|
|
481
|
+
default:
|
|
482
|
+
trace.push({ trace: 'unknown_step', fn: String(step.fn) });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return { trace };
|
|
486
|
+
}
|
|
487
|
+
case 'awaitLossReports': {
|
|
488
|
+
const trace = [];
|
|
489
|
+
const pushes = (testCase.pushes ?? []).map((push) => {
|
|
490
|
+
if (push.kind === 'resolve') {
|
|
491
|
+
return Promise.resolve(undefined);
|
|
492
|
+
}
|
|
493
|
+
if (push.kind === 'reject') {
|
|
494
|
+
const rejected = Promise.reject(
|
|
495
|
+
new Error(String(push.message ?? 'scripted push rejection'))
|
|
496
|
+
);
|
|
497
|
+
// Pre-observed so the scripted rejection can never trip the
|
|
498
|
+
// unhandled-rejection handler before allSettled adopts it.
|
|
499
|
+
rejected.catch(() => {});
|
|
500
|
+
return rejected;
|
|
501
|
+
}
|
|
502
|
+
// 'never': a push whose ack never comes back.
|
|
503
|
+
return new Promise(() => {});
|
|
504
|
+
});
|
|
505
|
+
await awaitLossReportsThenExit({
|
|
506
|
+
pushes,
|
|
507
|
+
exit: () => trace.push({ trace: 'exit' }),
|
|
508
|
+
safetyCapMs: testCase.safetyCapMs,
|
|
509
|
+
setTimeoutFn: (callback, ms) => {
|
|
510
|
+
trace.push({ trace: 'cap_scheduled', ms });
|
|
511
|
+
if (testCase.capFires === true) {
|
|
512
|
+
setImmediate(callback);
|
|
513
|
+
}
|
|
514
|
+
return undefined;
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
trace.push({ trace: 'returned' });
|
|
518
|
+
return { trace };
|
|
519
|
+
}
|
|
520
|
+
default:
|
|
521
|
+
return { threw: true, message: 'unknown respawn fn ' + String(testCase.fn) };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// --- Dispatch --------------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
async function handle(testCase) {
|
|
528
|
+
switch (testCase.op) {
|
|
529
|
+
case 'controlDispatch':
|
|
530
|
+
return await handleControlDispatch(testCase);
|
|
531
|
+
case 'staleness':
|
|
532
|
+
return handleStaleness(testCase);
|
|
533
|
+
case 'respawn':
|
|
534
|
+
return await handleRespawn(testCase);
|
|
535
|
+
default:
|
|
536
|
+
return { threw: true, message: 'unknown op ' + String(testCase.op) };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let stdin = '';
|
|
541
|
+
process.stdin.setEncoding('utf8');
|
|
542
|
+
for await (const chunk of process.stdin) {
|
|
543
|
+
stdin += chunk;
|
|
544
|
+
}
|
|
545
|
+
const cases = JSON.parse(stdin);
|
|
546
|
+
const results = [];
|
|
547
|
+
for (const testCase of cases) {
|
|
548
|
+
results.push(await handle(testCase));
|
|
549
|
+
}
|
|
550
|
+
// Exit only after stdout is fully flushed: the runner graph can leave
|
|
551
|
+
// unref'd handles alive, so the driver exits explicitly - but a bare
|
|
552
|
+
// process.exit(0) truncates large result arrays mid-pipe.
|
|
553
|
+
process.stdout.write(JSON.stringify(results), () => {
|
|
554
|
+
process.exit(0);
|
|
555
|
+
});
|
|
556
|
+
`;
|
|
557
|
+
|
|
558
|
+
await mkdir(join(packageRoot, '.differential'), { recursive: true });
|
|
559
|
+
|
|
560
|
+
// runner.ts's graph resolves assets/ relative to the bundle at import time
|
|
561
|
+
// (same requirement as build-transport-parity-oracle.mjs).
|
|
562
|
+
await mkdir(join(packageRoot, '.differential/assets'), { recursive: true });
|
|
563
|
+
await copyFile(
|
|
564
|
+
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|
|
565
|
+
join(packageRoot, '.differential/assets/linzumi-logo.svg')
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
await build({
|
|
569
|
+
stdin: {
|
|
570
|
+
contents: driverSource,
|
|
571
|
+
resolveDir: packageRoot,
|
|
572
|
+
sourcefile: 'core-commander-seams-parity-driver.ts',
|
|
573
|
+
loader: 'ts',
|
|
574
|
+
},
|
|
575
|
+
bundle: true,
|
|
576
|
+
platform: 'node',
|
|
577
|
+
target: 'node20',
|
|
578
|
+
format: 'esm',
|
|
579
|
+
outfile: join(packageRoot, '.differential/core-commander-seams-parity-oracle.mjs'),
|
|
580
|
+
minify: false,
|
|
581
|
+
sourcemap: false,
|
|
582
|
+
legalComments: 'none',
|
|
583
|
+
// runner.ts pulls the wider commander graph; the same runtime externals
|
|
584
|
+
// as the differential entry build stay external.
|
|
585
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
586
|
+
});
|