@linzumi/cli 1.0.109 → 1.0.111
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
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.111",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,308 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 8, harness-claude-adapter / workstream H.3).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the claude-adapter
|
|
4
|
+
// cluster of the ReScript commander port, the sibling of
|
|
5
|
+
// build-harness-codex-parity-oracle.mjs (cluster 7). The oracle runs the
|
|
6
|
+
// REAL TS claude session driver (startClaudeCodeSession + the resume
|
|
7
|
+
// validation + the canUseTool bridge) with '@anthropic-ai/claude-agent-sdk'
|
|
8
|
+
// ALIASED to the M2.3 scripted stub
|
|
9
|
+
// (packages/commander-harness/src/scripted/ScriptedClaudeAgentSdk.res.mjs)
|
|
10
|
+
// - the exact import-seam substitution the stub was built for - and prints
|
|
11
|
+
// the stub recorder's transcript for one named scenario. The dual-impl
|
|
12
|
+
// differential in @linzumi/commander-harness
|
|
13
|
+
// (HarnessClaudeDifferentialConformance) runs the same scenario through
|
|
14
|
+
// the ReScript adapter in-process and requires the two double transcripts
|
|
15
|
+
// byte-identical.
|
|
16
|
+
//
|
|
17
|
+
// Determinism: the scenarios are scripted data, the driver's only waits
|
|
18
|
+
// are milestone polls over the in-process transcript, and the recorder
|
|
19
|
+
// rows carry no wall-clock values.
|
|
20
|
+
//
|
|
21
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
22
|
+
// payloads. The output is a local test artifact (.differential/ is
|
|
23
|
+
// gitignored, never published) and stays unminified for debuggability.
|
|
24
|
+
import { mkdir } from 'node:fs/promises';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { build } from 'esbuild';
|
|
28
|
+
|
|
29
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
30
|
+
const stubModulePath = join(
|
|
31
|
+
packageRoot,
|
|
32
|
+
'..',
|
|
33
|
+
'commander-harness',
|
|
34
|
+
'src',
|
|
35
|
+
'scripted',
|
|
36
|
+
'ScriptedClaudeAgentSdk.res.mjs'
|
|
37
|
+
);
|
|
38
|
+
const scriptModulePath = join(
|
|
39
|
+
packageRoot,
|
|
40
|
+
'..',
|
|
41
|
+
'commander-harness',
|
|
42
|
+
'src',
|
|
43
|
+
'scripted',
|
|
44
|
+
'ScriptedClaudeAgentSdkScript.res.mjs'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const driverSource = `
|
|
48
|
+
import {
|
|
49
|
+
startClaudeCodeSession,
|
|
50
|
+
validateClaudeCodeResumeSession,
|
|
51
|
+
} from './src/claudeCodeSession';
|
|
52
|
+
import type { ClaudeCodeTranscriptEvent } from './src/claudeCodeSession';
|
|
53
|
+
// The scenario side-channel of the SAME stub instance the alias bundles in
|
|
54
|
+
// (esbuild dedupes by resolved path, so install/recorder observe the
|
|
55
|
+
// aliased SDK the session driver consumes).
|
|
56
|
+
import { install, recorder } from '${scriptModulePath.replace(/\\\\/g, '/')}';
|
|
57
|
+
|
|
58
|
+
const cwd = '/scripted/workspace';
|
|
59
|
+
const model = 'scripted-model-alpha';
|
|
60
|
+
|
|
61
|
+
// The recorded API-provenance error text (must stay byte-identical to the
|
|
62
|
+
// ReScript differential leg's scenario data).
|
|
63
|
+
const apiErrorText =
|
|
64
|
+
'API Error: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"},"request_id":"req_scripted42"}';
|
|
65
|
+
|
|
66
|
+
const scenarios: Record<string, unknown> = {
|
|
67
|
+
'clean-turn': {
|
|
68
|
+
queries: [{ TAG: 'CleanText', assistantText: 'scripted claude answer' }],
|
|
69
|
+
startupBehavior: 'StartupOk',
|
|
70
|
+
sessionInfo: 'SessionFound',
|
|
71
|
+
sessionFirstPrompts: [],
|
|
72
|
+
},
|
|
73
|
+
'approval-accept': {
|
|
74
|
+
queries: [
|
|
75
|
+
{
|
|
76
|
+
TAG: 'ToolUseWithPermission',
|
|
77
|
+
toolName: 'Bash',
|
|
78
|
+
toolInputJson: '{"command":"curl https://api.example.test/v1/voices"}',
|
|
79
|
+
assistantText: 'voice roster fetched',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
startupBehavior: 'StartupOk',
|
|
83
|
+
sessionInfo: 'SessionFound',
|
|
84
|
+
sessionFirstPrompts: [],
|
|
85
|
+
},
|
|
86
|
+
'approval-decline': {
|
|
87
|
+
queries: [
|
|
88
|
+
{
|
|
89
|
+
TAG: 'ToolUseWithPermission',
|
|
90
|
+
toolName: 'Bash',
|
|
91
|
+
toolInputJson: '{"command":"curl https://api.example.test/v1/voices"}',
|
|
92
|
+
assistantText: 'voice roster fetched',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
startupBehavior: 'StartupOk',
|
|
96
|
+
sessionInfo: 'SessionFound',
|
|
97
|
+
sessionFirstPrompts: [],
|
|
98
|
+
},
|
|
99
|
+
error: {
|
|
100
|
+
queries: [{ TAG: 'ResultApiError', errorText: apiErrorText }],
|
|
101
|
+
startupBehavior: 'StartupOk',
|
|
102
|
+
sessionInfo: 'SessionFound',
|
|
103
|
+
sessionFirstPrompts: [],
|
|
104
|
+
},
|
|
105
|
+
interrupt: {
|
|
106
|
+
queries: [{ TAG: 'MidTurnPark', assistantText: 'partial answer, then' }],
|
|
107
|
+
startupBehavior: 'StartupOk',
|
|
108
|
+
sessionInfo: 'SessionFound',
|
|
109
|
+
sessionFirstPrompts: [],
|
|
110
|
+
interruptBehavior: 'YieldInterruptResult',
|
|
111
|
+
},
|
|
112
|
+
'resume-attach': {
|
|
113
|
+
queries: [{ TAG: 'CleanText', assistantText: 'resumed answer' }],
|
|
114
|
+
startupBehavior: 'StartupOk',
|
|
115
|
+
sessionInfo: 'SessionFound',
|
|
116
|
+
sessionFirstPrompts: [],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// The runner-shaped streaming input queue (the live multi-turn channel:
|
|
121
|
+
// only the owner closes it).
|
|
122
|
+
function createInputQueue() {
|
|
123
|
+
const pending: unknown[] = [];
|
|
124
|
+
const waiters: Array<(step: { done: boolean; value: unknown }) => void> = [];
|
|
125
|
+
let closed = false;
|
|
126
|
+
return {
|
|
127
|
+
messages: {
|
|
128
|
+
[Symbol.asyncIterator]() {
|
|
129
|
+
return {
|
|
130
|
+
next(): Promise<{ done: boolean; value: unknown }> {
|
|
131
|
+
if (pending.length > 0) {
|
|
132
|
+
return Promise.resolve({ done: false, value: pending.shift() });
|
|
133
|
+
}
|
|
134
|
+
if (closed) {
|
|
135
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
136
|
+
}
|
|
137
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
push(message: unknown) {
|
|
143
|
+
const waiter = waiters.shift();
|
|
144
|
+
if (waiter !== undefined) {
|
|
145
|
+
waiter({ done: false, value: message });
|
|
146
|
+
} else {
|
|
147
|
+
pending.push(message);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
close() {
|
|
151
|
+
closed = true;
|
|
152
|
+
while (waiters.length > 0) {
|
|
153
|
+
waiters.shift()!({ done: true, value: undefined });
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function userMessage(text: string) {
|
|
160
|
+
return {
|
|
161
|
+
type: 'user' as const,
|
|
162
|
+
message: { role: 'user' as const, content: text },
|
|
163
|
+
parent_tool_use_id: null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
|
+
|
|
169
|
+
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
170
|
+
while (!predicate()) {
|
|
171
|
+
await sleep(5);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function transcript(): string[] {
|
|
176
|
+
return (recorder as { events: Array<{ label: string; detail: string }> }).events.map(
|
|
177
|
+
(event) => event.label + '|' + event.detail
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function run(): Promise<void> {
|
|
182
|
+
const scenarioName = process.argv[2];
|
|
183
|
+
const scenario = scenarios[scenarioName ?? ''];
|
|
184
|
+
if (scenario === undefined) {
|
|
185
|
+
throw new Error('unknown scenario: ' + String(scenarioName));
|
|
186
|
+
}
|
|
187
|
+
install(scenario);
|
|
188
|
+
|
|
189
|
+
const events: ClaudeCodeTranscriptEvent[] = [];
|
|
190
|
+
const has = (type: string) => events.some((event) => event.type === type);
|
|
191
|
+
let controls:
|
|
192
|
+
| { interrupt: () => Promise<void> }
|
|
193
|
+
| undefined;
|
|
194
|
+
const queue = createInputQueue();
|
|
195
|
+
|
|
196
|
+
if (scenarioName === 'resume-attach') {
|
|
197
|
+
await validateClaudeCodeResumeSession({
|
|
198
|
+
cwd,
|
|
199
|
+
sessionId: 'scripted-claude-session-resumed-7',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const sessionPromise = startClaudeCodeSession({
|
|
204
|
+
cwd,
|
|
205
|
+
prompt: '',
|
|
206
|
+
streamingInput: queue,
|
|
207
|
+
developerInstructions: undefined,
|
|
208
|
+
model,
|
|
209
|
+
permissionMode: 'default',
|
|
210
|
+
...(scenarioName === 'resume-attach'
|
|
211
|
+
? { resumeSessionId: 'scripted-claude-session-resumed-7' }
|
|
212
|
+
: {}),
|
|
213
|
+
canUseTool: async () =>
|
|
214
|
+
scenarioName === 'approval-decline'
|
|
215
|
+
? { type: 'deny' as const, message: 'not on this machine' }
|
|
216
|
+
: { type: 'allow' as const },
|
|
217
|
+
onSessionControls: (liveControls) => {
|
|
218
|
+
controls = liveControls;
|
|
219
|
+
},
|
|
220
|
+
onTranscriptEvent: (event) => {
|
|
221
|
+
events.push(event);
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
// The session settles by scenario design; failures are the error leg's
|
|
225
|
+
// expected outcome and are re-checked below.
|
|
226
|
+
const settled = sessionPromise.then(
|
|
227
|
+
() => undefined,
|
|
228
|
+
(error: unknown) => (error instanceof Error ? error.message : String(error))
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
queue.push(userMessage('do the thing'));
|
|
232
|
+
|
|
233
|
+
switch (scenarioName) {
|
|
234
|
+
case 'error': {
|
|
235
|
+
// The TS session driver THROWS on the result error after emitting
|
|
236
|
+
// session_failed; the transcript snapshot is taken AT the terminal
|
|
237
|
+
// (the abandoned generator never closes the stub query - the
|
|
238
|
+
// documented TS lifecycle - so the close rows are not part of this
|
|
239
|
+
// scenario's comparison surface).
|
|
240
|
+
await waitFor(() => has('session_failed'));
|
|
241
|
+
const failure = await settled;
|
|
242
|
+
if (failure === undefined) {
|
|
243
|
+
throw new Error('the error scenario must fail the session');
|
|
244
|
+
}
|
|
245
|
+
process.stdout.write(JSON.stringify(transcript()) + '\\n');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
case 'interrupt': {
|
|
249
|
+
await waitFor(() => has('assistant_message'));
|
|
250
|
+
await controls!.interrupt();
|
|
251
|
+
await waitFor(() => has('turn_interrupted'));
|
|
252
|
+
queue.close();
|
|
253
|
+
// An interrupted single-turn session settles as the documented
|
|
254
|
+
// no-text-response failure; the transcript is the comparison
|
|
255
|
+
// surface.
|
|
256
|
+
await settled;
|
|
257
|
+
process.stdout.write(JSON.stringify(transcript()) + '\\n');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
default: {
|
|
261
|
+
await waitFor(() => has('turn_completed'));
|
|
262
|
+
queue.close();
|
|
263
|
+
const failure = await settled;
|
|
264
|
+
if (failure !== undefined) {
|
|
265
|
+
throw new Error('scenario ' + scenarioName + ' must complete: ' + failure);
|
|
266
|
+
}
|
|
267
|
+
process.stdout.write(JSON.stringify(transcript()) + '\\n');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
run().then(
|
|
273
|
+
() => process.exit(0),
|
|
274
|
+
(error) => {
|
|
275
|
+
process.stderr.write(String(error && error.stack ? error.stack : error) + '\\n');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
const outDir = join(packageRoot, '.differential');
|
|
282
|
+
const outPath = join(outDir, 'harness-claude-parity-oracle.mjs');
|
|
283
|
+
|
|
284
|
+
await mkdir(outDir, { recursive: true });
|
|
285
|
+
|
|
286
|
+
await build({
|
|
287
|
+
stdin: {
|
|
288
|
+
contents: driverSource,
|
|
289
|
+
resolveDir: packageRoot,
|
|
290
|
+
sourcefile: 'harness-claude-parity-driver.ts',
|
|
291
|
+
loader: 'ts',
|
|
292
|
+
},
|
|
293
|
+
bundle: true,
|
|
294
|
+
platform: 'node',
|
|
295
|
+
target: 'node20',
|
|
296
|
+
format: 'esm',
|
|
297
|
+
outfile: outPath,
|
|
298
|
+
minify: false,
|
|
299
|
+
sourcemap: false,
|
|
300
|
+
legalComments: 'none',
|
|
301
|
+
logLevel: 'silent',
|
|
302
|
+
external: ['ws', 'undici', 'blessed'],
|
|
303
|
+
alias: {
|
|
304
|
+
'@anthropic-ai/claude-agent-sdk': stubModulePath,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
process.stdout.write(outPath + '\n');
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 9, pipeline).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the pipeline cluster
|
|
4
|
+
// of the ReScript commander port, the sibling of
|
|
5
|
+
// build-queue-parity-oracle.mjs (cluster 5) and
|
|
6
|
+
// build-harness-codex-parity-oracle.mjs (cluster 7). The oracle bundles
|
|
7
|
+
// the REAL TS pipeline surface - the pure transition core
|
|
8
|
+
// (src/pipeline/transition.ts + contracts + itemProjection, transitively
|
|
9
|
+
// the item-output rendering slice), the thread event-loop driver
|
|
10
|
+
// (threadEventLoop.ts), the stats reporter + turn-stall detector
|
|
11
|
+
// (statsReporter.ts), and the two recovery watchdog primitives
|
|
12
|
+
// (claudeCodeTurnStallWatchdog.ts / claudeCodeDispatchLivenessWatchdog.ts)
|
|
13
|
+
// - behind the cluster 1/2 one-shot BATCH driver (a JSON array of cases on
|
|
14
|
+
// stdin, a JSON array of results on stdout).
|
|
15
|
+
//
|
|
16
|
+
// Byte parity: transition effects are returned VERBATIM (the TS effect
|
|
17
|
+
// objects' own key order rides JSON.stringify), so the ReScript side's
|
|
18
|
+
// effectToJson must reproduce field presence AND insertion order exactly.
|
|
19
|
+
// Determinism: clocks are injected per case (now()/scripted advance ops),
|
|
20
|
+
// the driver's sweep interval is disabled (0) in loop cases, and watchdog
|
|
21
|
+
// timers ride a scripted timer seam fired in scheduling order - nothing
|
|
22
|
+
// here reads wall time.
|
|
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
|
+
classifyRejectionRetry,
|
|
37
|
+
classifyStallWindowEvidence,
|
|
38
|
+
createInitialLoopState,
|
|
39
|
+
idleStreamStallDisposition,
|
|
40
|
+
isUpstreamTerminal4xxHttpStatus,
|
|
41
|
+
maxConsecutiveEmptyTurns,
|
|
42
|
+
maxEmptyTurnAutoRetries,
|
|
43
|
+
maxIsolatedRejectionRetries,
|
|
44
|
+
maxReasoningStepsWithoutProgress,
|
|
45
|
+
maxTerminalTurnIds,
|
|
46
|
+
agentNarrationOnlyCompletionReason,
|
|
47
|
+
agentNarrationOnlyEmptyTurnCause,
|
|
48
|
+
autoRetriesBurnedSuffix,
|
|
49
|
+
completedWithoutAssistantOutputReason,
|
|
50
|
+
costCapAutoRetrySuppressedSuffix,
|
|
51
|
+
emptyTurnLoopGuardReason,
|
|
52
|
+
linzumiAgentReplyToolName,
|
|
53
|
+
midStreamResumeIncompleteSuffix,
|
|
54
|
+
midStreamResumeRateLimitSuppressedSuffix,
|
|
55
|
+
midStreamResumeStalledAgainSuffix,
|
|
56
|
+
reasoningLoopBreakerReason,
|
|
57
|
+
stallWindowEvidenceSignalClasses,
|
|
58
|
+
stallWindowRateLimited,
|
|
59
|
+
transition,
|
|
60
|
+
turnMappingFailedMethod,
|
|
61
|
+
turnMappingReadyMethod,
|
|
62
|
+
unsupportedCodexToolCallReasonPrefix,
|
|
63
|
+
upstreamHttpStatusRetryClass,
|
|
64
|
+
upstreamTurnErrorSummary,
|
|
65
|
+
} from './src/pipeline/transition';
|
|
66
|
+
import { pipelineItemKey, outboxEntryKinds } from './src/pipeline/contracts';
|
|
67
|
+
import {
|
|
68
|
+
explicitNotificationItemId,
|
|
69
|
+
notificationTurnId,
|
|
70
|
+
pipelineClientMessageId,
|
|
71
|
+
projectSnapshotItems,
|
|
72
|
+
} from './src/pipeline/itemProjection';
|
|
73
|
+
import {
|
|
74
|
+
createThreadEventLoop,
|
|
75
|
+
defaultBufferedTurnMaxAgeMs,
|
|
76
|
+
defaultBufferedTurnMaxEvents,
|
|
77
|
+
defaultBufferedTurnSweepIntervalMs,
|
|
78
|
+
} from './src/pipeline/threadEventLoop';
|
|
79
|
+
import {
|
|
80
|
+
createPipelineStatsReporter,
|
|
81
|
+
defaultPipelineStatsIntervalMs,
|
|
82
|
+
defaultPipelineTurnStalledThresholdMs,
|
|
83
|
+
openItemTurnStalledThresholdMultiplier,
|
|
84
|
+
} from './src/pipeline/statsReporter';
|
|
85
|
+
import {
|
|
86
|
+
createClaudeCodeTurnStallWatchdog,
|
|
87
|
+
defaultClaudeCodeTurnStallThresholdMs,
|
|
88
|
+
} from './src/claudeCodeTurnStallWatchdog';
|
|
89
|
+
import {
|
|
90
|
+
armClaudeCodeDispatchLivenessWatchdog,
|
|
91
|
+
defaultClaudeCodeDispatchLivenessWindowMs,
|
|
92
|
+
} from './src/claudeCodeDispatchLivenessWatchdog';
|
|
93
|
+
|
|
94
|
+
// Scripted deps table shared by transition + loop runs. sourceSeq
|
|
95
|
+
// mappings, burned-retry counts, resume attempts, and ladder receipts are
|
|
96
|
+
// mutated by ops so terminal-time reads see the scripted values.
|
|
97
|
+
function scriptedDeps(caseInput, tables) {
|
|
98
|
+
return {
|
|
99
|
+
threadKey: caseInput.threadKey ?? 'thread-parity',
|
|
100
|
+
instanceId: caseInput.instanceId ?? 'instance-parity',
|
|
101
|
+
sourceSeqForTurn: (turnId) => tables.turnSeqs.get(turnId),
|
|
102
|
+
autoRetryAttemptsForSeq: (seq) => tables.retryAttempts.get(seq) ?? 0,
|
|
103
|
+
midStreamResumeAttemptAtMsForSeq: (seq) => tables.resumeAttempts.get(seq),
|
|
104
|
+
ladderHistoryForSeq: (seq, fate) => tables.ladders.get(seq + ':' + fate),
|
|
105
|
+
linzumiAgentSession: caseInput.linzumiAgentSession === true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyTableOp(tables, op) {
|
|
110
|
+
if (op.op === 'mapTurn') tables.turnSeqs.set(op.turnId, op.seq);
|
|
111
|
+
if (op.op === 'unmapTurn') tables.turnSeqs.delete(op.turnId);
|
|
112
|
+
if (op.op === 'retryAttempts') tables.retryAttempts.set(op.seq, op.attempts);
|
|
113
|
+
if (op.op === 'resumeAttemptAt') tables.resumeAttempts.set(op.seq, op.atMs);
|
|
114
|
+
if (op.op === 'ladder') tables.ladders.set(op.seq + ':' + op.fate, op.ladder);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function newTables() {
|
|
118
|
+
return {
|
|
119
|
+
turnSeqs: new Map(),
|
|
120
|
+
retryAttempts: new Map(),
|
|
121
|
+
resumeAttempts: new Map(),
|
|
122
|
+
ladders: new Map(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function runTransitionCase(caseInput) {
|
|
127
|
+
const tables = newTables();
|
|
128
|
+
const state = createInitialLoopState();
|
|
129
|
+
const deps = scriptedDeps(caseInput, tables);
|
|
130
|
+
const steps = [];
|
|
131
|
+
for (const op of caseInput.ops ?? []) {
|
|
132
|
+
if (op.op === 'event') {
|
|
133
|
+
steps.push(transition(state, op.event, deps));
|
|
134
|
+
} else {
|
|
135
|
+
applyTableOp(tables, op);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { steps };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function runLoopCase(caseInput) {
|
|
142
|
+
const tables = newTables();
|
|
143
|
+
let nowMs = caseInput.startMs ?? 0;
|
|
144
|
+
const trace = [];
|
|
145
|
+
const outbox = {
|
|
146
|
+
enqueue: (entry) => {
|
|
147
|
+
trace.push({
|
|
148
|
+
t: 'enqueue',
|
|
149
|
+
kind: entry.kind,
|
|
150
|
+
data: entry.data,
|
|
151
|
+
coalesceKey: entry.coalesceKey ?? null,
|
|
152
|
+
resolution: entry.resolution,
|
|
153
|
+
});
|
|
154
|
+
return 0;
|
|
155
|
+
},
|
|
156
|
+
flush: async () => {
|
|
157
|
+
if (caseInput.flushFails === true) {
|
|
158
|
+
throw new Error('scripted flush failure');
|
|
159
|
+
}
|
|
160
|
+
trace.push({ t: 'flushed' });
|
|
161
|
+
},
|
|
162
|
+
pause: () => {},
|
|
163
|
+
resume: () => {},
|
|
164
|
+
metrics: () => {
|
|
165
|
+
throw new Error('not used');
|
|
166
|
+
},
|
|
167
|
+
close: async () => {},
|
|
168
|
+
};
|
|
169
|
+
const loop = createThreadEventLoop({
|
|
170
|
+
threadKey: caseInput.threadKey ?? 'thread-parity',
|
|
171
|
+
outbox,
|
|
172
|
+
sourceSeqForTurn: (turnId) => tables.turnSeqs.get(turnId),
|
|
173
|
+
autoRetryAttemptsForSeq: (seq) => tables.retryAttempts.get(seq) ?? 0,
|
|
174
|
+
midStreamResumeAttemptAtMsForSeq: (seq) => tables.resumeAttempts.get(seq),
|
|
175
|
+
ladderHistoryForSeq: (seq, fate) => tables.ladders.get(seq + ':' + fate),
|
|
176
|
+
onTurnTerminal: (turnId, outcome, reason, retryableEmptyTurn, autoResume, fate) => {
|
|
177
|
+
trace.push({
|
|
178
|
+
t: 'terminal',
|
|
179
|
+
turnId,
|
|
180
|
+
outcome,
|
|
181
|
+
reason: reason ?? null,
|
|
182
|
+
retryableEmptyTurn: retryableEmptyTurn === true,
|
|
183
|
+
autoResumeMidStreamStall: autoResume === true,
|
|
184
|
+
fate: fate ?? null,
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
requestSnapshot: (turnId) => {
|
|
188
|
+
trace.push({ t: 'requestSnapshot', turnId });
|
|
189
|
+
},
|
|
190
|
+
instanceId: caseInput.instanceId ?? 'instance-parity',
|
|
191
|
+
log: (event, fields) => {
|
|
192
|
+
trace.push({ t: 'log', event, fields });
|
|
193
|
+
},
|
|
194
|
+
linzumiAgentSession: caseInput.linzumiAgentSession === true,
|
|
195
|
+
now: () => nowMs,
|
|
196
|
+
bufferedTurnMaxAgeMs: caseInput.bufferedTurnMaxAgeMs,
|
|
197
|
+
bufferedTurnMaxEvents: caseInput.bufferedTurnMaxEvents,
|
|
198
|
+
// Determinism: the quiet-period sweep timer is disabled; the age cap
|
|
199
|
+
// still applies on every processed event.
|
|
200
|
+
bufferedTurnSweepIntervalMs: 0,
|
|
201
|
+
});
|
|
202
|
+
const metricsJson = () => {
|
|
203
|
+
const metrics = loop.metrics();
|
|
204
|
+
return {
|
|
205
|
+
queueDepth: metrics.queueDepth,
|
|
206
|
+
phase: metrics.phase,
|
|
207
|
+
activeTurns: metrics.activeTurns.map((turn) => ({
|
|
208
|
+
turnId: turn.turnId,
|
|
209
|
+
sourceSeq: turn.sourceSeq,
|
|
210
|
+
startedAtMs: turn.startedAtMs,
|
|
211
|
+
lastActivityAtMs: turn.lastActivityAtMs ?? null,
|
|
212
|
+
lastActivityKind: turn.lastActivityKind ?? null,
|
|
213
|
+
openItems: (turn.openItems ?? []).map((item) => ({
|
|
214
|
+
kind: item.kind,
|
|
215
|
+
startedAtMs: item.startedAtMs,
|
|
216
|
+
})),
|
|
217
|
+
})),
|
|
218
|
+
bufferedTurns: metrics.bufferedTurns.map((turn) => ({
|
|
219
|
+
turnId: turn.turnId,
|
|
220
|
+
bufferedEventCount: turn.bufferedEventCount,
|
|
221
|
+
firstBufferedAtMs: turn.firstBufferedAtMs,
|
|
222
|
+
lastActivityAtMs: turn.lastActivityAtMs ?? null,
|
|
223
|
+
lastActivityKind: turn.lastActivityKind ?? null,
|
|
224
|
+
openItems: (turn.openItems ?? []).map((item) => ({
|
|
225
|
+
kind: item.kind,
|
|
226
|
+
startedAtMs: item.startedAtMs,
|
|
227
|
+
})),
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
const run = async () => {
|
|
232
|
+
for (const op of caseInput.ops ?? []) {
|
|
233
|
+
if (op.op === 'submit') loop.submit(op.event);
|
|
234
|
+
else if (op.op === 'advance') nowMs = op.toMs;
|
|
235
|
+
else if (op.op === 'metrics') trace.push({ t: 'metrics', value: metricsJson() });
|
|
236
|
+
else if (op.op === 'noteActivity') loop.noteTurnActivity(op.turnId, op.kind);
|
|
237
|
+
else if (op.op === 'drainAndClose') {
|
|
238
|
+
await loop.drainAndClose(op.timeoutMs);
|
|
239
|
+
trace.push({ t: 'drained' });
|
|
240
|
+
} else applyTableOp(tables, op);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
return run().then(() => ({ trace }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function runStatsCase(caseInput) {
|
|
247
|
+
let nowMs = caseInput.startMs ?? 0;
|
|
248
|
+
let outboxMetrics = caseInput.outbox;
|
|
249
|
+
let loopMetrics = caseInput.loop;
|
|
250
|
+
let waits = caseInput.waits ?? [];
|
|
251
|
+
const trace = [];
|
|
252
|
+
const reporter = createPipelineStatsReporter({
|
|
253
|
+
threadKey: caseInput.threadKey ?? 'thread-parity',
|
|
254
|
+
outboxMetrics: () => ({
|
|
255
|
+
...outboxMetrics,
|
|
256
|
+
oldestEntryAgeMs: outboxMetrics.oldestEntryAgeMs ?? undefined,
|
|
257
|
+
pushLatency: outboxMetrics.pushLatency ?? undefined,
|
|
258
|
+
}),
|
|
259
|
+
loopMetrics: () => ({
|
|
260
|
+
queueDepth: loopMetrics.queueDepth,
|
|
261
|
+
phase: loopMetrics.phase,
|
|
262
|
+
activeTurns: loopMetrics.activeTurns.map((turn) => ({
|
|
263
|
+
...turn,
|
|
264
|
+
lastActivityAtMs: turn.lastActivityAtMs ?? undefined,
|
|
265
|
+
lastActivityKind: turn.lastActivityKind ?? undefined,
|
|
266
|
+
openItems: turn.openItems ?? [],
|
|
267
|
+
})),
|
|
268
|
+
bufferedTurns: loopMetrics.bufferedTurns.map((turn) => ({
|
|
269
|
+
...turn,
|
|
270
|
+
lastActivityAtMs: turn.lastActivityAtMs ?? undefined,
|
|
271
|
+
lastActivityKind: turn.lastActivityKind ?? undefined,
|
|
272
|
+
openItems: turn.openItems ?? [],
|
|
273
|
+
})),
|
|
274
|
+
}),
|
|
275
|
+
log: (event, fields) => trace.push({ t: 'log', event, fields }),
|
|
276
|
+
intervalMs: 0,
|
|
277
|
+
turnStalledThresholdMs: caseInput.thresholdMs,
|
|
278
|
+
turnWaitStates: () => waits.map((wait) => ({ turnId: wait.turnId, kind: wait.kind })),
|
|
279
|
+
now: () => nowMs,
|
|
280
|
+
});
|
|
281
|
+
for (const op of caseInput.ops ?? []) {
|
|
282
|
+
if (op.op === 'set') {
|
|
283
|
+
if (op.outbox !== undefined) outboxMetrics = op.outbox;
|
|
284
|
+
if (op.loop !== undefined) loopMetrics = op.loop;
|
|
285
|
+
if (op.waits !== undefined) waits = op.waits;
|
|
286
|
+
} else if (op.op === 'sample') {
|
|
287
|
+
nowMs = op.nowMs ?? nowMs;
|
|
288
|
+
reporter.sample();
|
|
289
|
+
} else if (op.op === 'stop') {
|
|
290
|
+
reporter.stop();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { trace };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Scripted timer seam shared by the watchdog runs: timers fire in
|
|
297
|
+
// scheduling order once the scripted clock passes their due instant.
|
|
298
|
+
function scriptedTimers(clock, trace, label) {
|
|
299
|
+
let nextId = 1;
|
|
300
|
+
const pending = [];
|
|
301
|
+
return {
|
|
302
|
+
setTimer: (callback, delayMs) => {
|
|
303
|
+
const id = nextId;
|
|
304
|
+
nextId += 1;
|
|
305
|
+
trace.push({ t: label + '.set', id, delayMs, dueAtMs: clock.nowMs + delayMs });
|
|
306
|
+
pending.push({ id, dueAtMs: clock.nowMs + delayMs, callback });
|
|
307
|
+
return id;
|
|
308
|
+
},
|
|
309
|
+
clearTimer: (handle) => {
|
|
310
|
+
const index = pending.findIndex((timer) => timer.id === handle);
|
|
311
|
+
if (index >= 0) {
|
|
312
|
+
trace.push({ t: label + '.clear', id: pending[index].id });
|
|
313
|
+
pending.splice(index, 1);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
fireDue: () => {
|
|
317
|
+
let fired = true;
|
|
318
|
+
while (fired) {
|
|
319
|
+
fired = false;
|
|
320
|
+
const index = pending.findIndex((timer) => timer.dueAtMs <= clock.nowMs);
|
|
321
|
+
if (index >= 0) {
|
|
322
|
+
const timer = pending.splice(index, 1)[0];
|
|
323
|
+
trace.push({ t: label + '.fire', id: timer.id });
|
|
324
|
+
timer.callback();
|
|
325
|
+
fired = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function runTurnStallWatchdogCase(caseInput) {
|
|
333
|
+
const clock = { nowMs: caseInput.startMs ?? 0 };
|
|
334
|
+
const trace = [];
|
|
335
|
+
const timers = scriptedTimers(clock, trace, 'timer');
|
|
336
|
+
let turnActive = caseInput.turnActive === true;
|
|
337
|
+
let watchdog;
|
|
338
|
+
const outcome = (() => {
|
|
339
|
+
try {
|
|
340
|
+
watchdog = createClaudeCodeTurnStallWatchdog({
|
|
341
|
+
thresholdMs: caseInput.thresholdMs,
|
|
342
|
+
isTurnActive: () => turnActive,
|
|
343
|
+
onStall: (info) => trace.push({ t: 'stall', silentMs: info.silentMs }),
|
|
344
|
+
now: () => clock.nowMs,
|
|
345
|
+
setTimer: timers.setTimer,
|
|
346
|
+
clearTimer: timers.clearTimer,
|
|
347
|
+
});
|
|
348
|
+
return { ok: true };
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
351
|
+
}
|
|
352
|
+
})();
|
|
353
|
+
if (outcome.ok) {
|
|
354
|
+
for (const op of caseInput.ops ?? []) {
|
|
355
|
+
if (op.op === 'advance') {
|
|
356
|
+
clock.nowMs = op.toMs;
|
|
357
|
+
timers.fireDue();
|
|
358
|
+
} else if (op.op === 'kick') watchdog.kick();
|
|
359
|
+
else if (op.op === 'stop') watchdog.stop();
|
|
360
|
+
else if (op.op === 'setActive') turnActive = op.active === true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { outcome, trace };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function runDispatchWatchdogCase(caseInput) {
|
|
367
|
+
const clock = { nowMs: caseInput.startMs ?? 0 };
|
|
368
|
+
const trace = [];
|
|
369
|
+
const timers = scriptedTimers(clock, trace, 'timer');
|
|
370
|
+
let delivered = caseInput.delivered === true;
|
|
371
|
+
let sessionClosed = caseInput.sessionClosed === true;
|
|
372
|
+
let lastActivityMs = caseInput.lastActivityMs ?? 0;
|
|
373
|
+
let pendingBlocking = caseInput.pendingBlocking === true;
|
|
374
|
+
let watchdog;
|
|
375
|
+
const outcome = (() => {
|
|
376
|
+
try {
|
|
377
|
+
watchdog = armClaudeCodeDispatchLivenessWatchdog({
|
|
378
|
+
windowMs: caseInput.windowMs,
|
|
379
|
+
isDelivered: () => delivered,
|
|
380
|
+
isSessionClosed: () => sessionClosed,
|
|
381
|
+
lastSdkActivityMs: () => lastActivityMs,
|
|
382
|
+
hasPendingBlockingRequest: () => pendingBlocking,
|
|
383
|
+
onWedged: (info) => trace.push({ t: 'wedged', waitedMs: info.waitedMs, cause: info.cause }),
|
|
384
|
+
now: () => clock.nowMs,
|
|
385
|
+
setTimer: timers.setTimer,
|
|
386
|
+
clearTimer: timers.clearTimer,
|
|
387
|
+
});
|
|
388
|
+
return { ok: true };
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
391
|
+
}
|
|
392
|
+
})();
|
|
393
|
+
if (outcome.ok) {
|
|
394
|
+
for (const op of caseInput.ops ?? []) {
|
|
395
|
+
if (op.op === 'advance') {
|
|
396
|
+
clock.nowMs = op.toMs;
|
|
397
|
+
timers.fireDue();
|
|
398
|
+
} else if (op.op === 'setDelivered') delivered = op.value === true;
|
|
399
|
+
else if (op.op === 'setSessionClosed') sessionClosed = op.value === true;
|
|
400
|
+
else if (op.op === 'setActivity') lastActivityMs = op.atMs;
|
|
401
|
+
else if (op.op === 'setPendingBlocking') pendingBlocking = op.value === true;
|
|
402
|
+
else if (op.op === 'stop') watchdog.stop();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { outcome, trace };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function runCase(input) {
|
|
409
|
+
switch (input.kind) {
|
|
410
|
+
case 'constants':
|
|
411
|
+
return {
|
|
412
|
+
outboxEntryKinds,
|
|
413
|
+
turnMappingReadyMethod,
|
|
414
|
+
turnMappingFailedMethod,
|
|
415
|
+
maxTerminalTurnIds,
|
|
416
|
+
maxEmptyTurnAutoRetries,
|
|
417
|
+
maxConsecutiveEmptyTurns,
|
|
418
|
+
maxReasoningStepsWithoutProgress,
|
|
419
|
+
maxIsolatedRejectionRetries,
|
|
420
|
+
completedWithoutAssistantOutputReason,
|
|
421
|
+
linzumiAgentReplyToolName,
|
|
422
|
+
agentNarrationOnlyCompletionReason,
|
|
423
|
+
agentNarrationOnlyEmptyTurnCause,
|
|
424
|
+
emptyTurnLoopGuardReason,
|
|
425
|
+
unsupportedCodexToolCallReasonPrefix,
|
|
426
|
+
midStreamResumeRateLimitSuppressedSuffix,
|
|
427
|
+
costCapAutoRetrySuppressedSuffix,
|
|
428
|
+
stallWindowEvidenceSignalClasses,
|
|
429
|
+
defaultBufferedTurnMaxAgeMs,
|
|
430
|
+
defaultBufferedTurnMaxEvents,
|
|
431
|
+
defaultBufferedTurnSweepIntervalMs,
|
|
432
|
+
defaultPipelineStatsIntervalMs,
|
|
433
|
+
defaultPipelineTurnStalledThresholdMs,
|
|
434
|
+
openItemTurnStalledThresholdMultiplier,
|
|
435
|
+
defaultClaudeCodeTurnStallThresholdMs,
|
|
436
|
+
defaultClaudeCodeDispatchLivenessWindowMs,
|
|
437
|
+
};
|
|
438
|
+
case 'reasoningLoopBreakerReason':
|
|
439
|
+
return reasoningLoopBreakerReason(input.steps);
|
|
440
|
+
case 'autoRetriesBurnedSuffix':
|
|
441
|
+
return autoRetriesBurnedSuffix(input.attempts);
|
|
442
|
+
case 'midStreamResumeStalledAgainSuffix':
|
|
443
|
+
return midStreamResumeStalledAgainSuffix(input.attemptAtMs);
|
|
444
|
+
case 'midStreamResumeIncompleteSuffix':
|
|
445
|
+
return midStreamResumeIncompleteSuffix(input.attemptAtMs);
|
|
446
|
+
case 'upstreamHttpStatusRetryClass':
|
|
447
|
+
return upstreamHttpStatusRetryClass(input.status);
|
|
448
|
+
case 'isUpstreamTerminal4xxHttpStatus':
|
|
449
|
+
return isUpstreamTerminal4xxHttpStatus(input.status ?? undefined);
|
|
450
|
+
case 'classifyStallWindowEvidence':
|
|
451
|
+
return classifyStallWindowEvidence({
|
|
452
|
+
lastUpstreamErrorReason: input.turn.lastUpstreamErrorReason ?? undefined,
|
|
453
|
+
lastErrorDiagnosticText: input.turn.lastErrorDiagnosticText ?? undefined,
|
|
454
|
+
lastUpstreamErrorHttpStatus: input.turn.lastUpstreamErrorHttpStatus ?? undefined,
|
|
455
|
+
upstreamRejectedSeen: input.turn.upstreamRejectedSeen === true,
|
|
456
|
+
providerOutageFlaggedSeen: input.turn.providerOutageFlaggedSeen === true,
|
|
457
|
+
toolFatalSeen: input.turn.toolFatalSeen === true,
|
|
458
|
+
errorNotificationCount: input.turn.errorNotificationCount ?? 0,
|
|
459
|
+
unattributedErrorNotificationCount:
|
|
460
|
+
input.turn.unattributedErrorNotificationCount ?? 0,
|
|
461
|
+
modelInteractionSeen: input.turn.modelInteractionSeen === true,
|
|
462
|
+
});
|
|
463
|
+
case 'stallWindowRateLimited':
|
|
464
|
+
return stallWindowRateLimited({
|
|
465
|
+
lastUpstreamErrorReason: input.turn.lastUpstreamErrorReason ?? undefined,
|
|
466
|
+
lastErrorDiagnosticText: input.turn.lastErrorDiagnosticText ?? undefined,
|
|
467
|
+
lastUpstreamErrorHttpStatus: input.turn.lastUpstreamErrorHttpStatus ?? undefined,
|
|
468
|
+
upstreamRejectedSeen: input.turn.upstreamRejectedSeen === true,
|
|
469
|
+
providerOutageFlaggedSeen: input.turn.providerOutageFlaggedSeen === true,
|
|
470
|
+
toolFatalSeen: input.turn.toolFatalSeen === true,
|
|
471
|
+
errorNotificationCount: input.turn.errorNotificationCount ?? 0,
|
|
472
|
+
unattributedErrorNotificationCount:
|
|
473
|
+
input.turn.unattributedErrorNotificationCount ?? 0,
|
|
474
|
+
modelInteractionSeen: input.turn.modelInteractionSeen === true,
|
|
475
|
+
});
|
|
476
|
+
case 'idleStreamStallDisposition': {
|
|
477
|
+
const disposition = idleStreamStallDisposition(
|
|
478
|
+
input.method,
|
|
479
|
+
input.params,
|
|
480
|
+
{
|
|
481
|
+
lastUpstreamErrorReason: input.turn.lastUpstreamErrorReason ?? undefined,
|
|
482
|
+
lastErrorDiagnosticText: input.turn.lastErrorDiagnosticText ?? undefined,
|
|
483
|
+
lastUpstreamErrorHttpStatus: input.turn.lastUpstreamErrorHttpStatus ?? undefined,
|
|
484
|
+
upstreamRejectedSeen: input.turn.upstreamRejectedSeen === true,
|
|
485
|
+
providerOutageFlaggedSeen: input.turn.providerOutageFlaggedSeen === true,
|
|
486
|
+
toolFatalSeen: input.turn.toolFatalSeen === true,
|
|
487
|
+
errorNotificationCount: input.turn.errorNotificationCount ?? 0,
|
|
488
|
+
unattributedErrorNotificationCount:
|
|
489
|
+
input.turn.unattributedErrorNotificationCount ?? 0,
|
|
490
|
+
modelInteractionSeen: input.turn.modelInteractionSeen === true,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
costCap402Seen: input.session.costCap402Seen === true,
|
|
494
|
+
midStreamResumeAttemptAtMs: input.session.midStreamResumeAttemptAtMs ?? undefined,
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
return {
|
|
498
|
+
autoRetryPreFirstByteStall: disposition.autoRetryPreFirstByteStall,
|
|
499
|
+
messageStateRetryable: disposition.messageStateRetryable,
|
|
500
|
+
costCapAutoRetrySuppressed: disposition.costCapAutoRetrySuppressed,
|
|
501
|
+
messageStateFate: disposition.messageStateFate ?? null,
|
|
502
|
+
autoResumeMidStreamStall: disposition.autoResumeMidStreamStall,
|
|
503
|
+
midStreamResumeRateLimitSuppressed: disposition.midStreamResumeRateLimitSuppressed,
|
|
504
|
+
midStreamResumeAttemptBurnedAtMs:
|
|
505
|
+
disposition.midStreamResumeAttemptBurnedAtMs ?? null,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
case 'classifyRejectionRetry': {
|
|
509
|
+
const disposition = classifyRejectionRetry({
|
|
510
|
+
providerOutageFlagged: input.providerOutageFlagged === true,
|
|
511
|
+
priorConsecutiveIsolatedRejections: input.priorConsecutiveIsolatedRejections,
|
|
512
|
+
stormDetail: input.stormDetail ?? undefined,
|
|
513
|
+
rejectionDetail: input.rejectionDetail ?? undefined,
|
|
514
|
+
});
|
|
515
|
+
return {
|
|
516
|
+
kind: disposition.kind,
|
|
517
|
+
retryable: disposition.retryable,
|
|
518
|
+
reason: disposition.reason,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
case 'upstreamTurnErrorSummary':
|
|
522
|
+
return upstreamTurnErrorSummary(input.params) ?? null;
|
|
523
|
+
case 'itemKey':
|
|
524
|
+
return pipelineItemKey({
|
|
525
|
+
turnId: input.identity.turnId,
|
|
526
|
+
itemId: input.identity.itemId ?? undefined,
|
|
527
|
+
itemIndex: input.identity.itemIndex,
|
|
528
|
+
streamKind: input.identity.streamKind,
|
|
529
|
+
});
|
|
530
|
+
case 'clientMessageId':
|
|
531
|
+
return pipelineClientMessageId(
|
|
532
|
+
input.instanceId,
|
|
533
|
+
input.turnId,
|
|
534
|
+
input.streamKind,
|
|
535
|
+
input.wireItemKey
|
|
536
|
+
);
|
|
537
|
+
case 'notificationTurnId':
|
|
538
|
+
return notificationTurnId(input.params) ?? null;
|
|
539
|
+
case 'explicitItemId':
|
|
540
|
+
return explicitNotificationItemId(input.params) ?? null;
|
|
541
|
+
case 'projectSnapshotItems':
|
|
542
|
+
return projectSnapshotItems(input.items).map((proj) => ({
|
|
543
|
+
snapshotIndex: proj.snapshotIndex,
|
|
544
|
+
itemId: proj.itemId ?? null,
|
|
545
|
+
streamKind: proj.streamKind,
|
|
546
|
+
body: proj.body,
|
|
547
|
+
structured: proj.structured,
|
|
548
|
+
}));
|
|
549
|
+
case 'transitionRun':
|
|
550
|
+
return runTransitionCase(input);
|
|
551
|
+
case 'loopRun':
|
|
552
|
+
return runLoopCase(input);
|
|
553
|
+
case 'statsRun':
|
|
554
|
+
return runStatsCase(input);
|
|
555
|
+
case 'turnStallWatchdogRun':
|
|
556
|
+
return runTurnStallWatchdogCase(input);
|
|
557
|
+
case 'dispatchWatchdogRun':
|
|
558
|
+
return runDispatchWatchdogCase(input);
|
|
559
|
+
default:
|
|
560
|
+
return { error: 'unknown case kind ' + String(input.kind) };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function runBatch() {
|
|
565
|
+
const chunks = [];
|
|
566
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
567
|
+
const cases = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
568
|
+
const results = [];
|
|
569
|
+
for (const testCase of cases) {
|
|
570
|
+
results.push(await runCase(testCase));
|
|
571
|
+
}
|
|
572
|
+
process.stdout.write(JSON.stringify(results), () => {
|
|
573
|
+
process.exit(0);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
runBatch().then(
|
|
578
|
+
() => {},
|
|
579
|
+
(error) => {
|
|
580
|
+
process.stderr.write(String(error && error.stack ? error.stack : error) + '\\n');
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
`;
|
|
585
|
+
|
|
586
|
+
const outDir = join(packageRoot, '.differential');
|
|
587
|
+
const outPath = join(outDir, 'pipeline-parity-oracle.mjs');
|
|
588
|
+
|
|
589
|
+
await mkdir(outDir, { recursive: true });
|
|
590
|
+
|
|
591
|
+
await build({
|
|
592
|
+
stdin: {
|
|
593
|
+
contents: driverSource,
|
|
594
|
+
resolveDir: packageRoot,
|
|
595
|
+
sourcefile: 'pipeline-parity-driver.ts',
|
|
596
|
+
loader: 'ts',
|
|
597
|
+
},
|
|
598
|
+
bundle: true,
|
|
599
|
+
platform: 'node',
|
|
600
|
+
target: 'node20',
|
|
601
|
+
format: 'esm',
|
|
602
|
+
outfile: outPath,
|
|
603
|
+
minify: false,
|
|
604
|
+
sourcemap: false,
|
|
605
|
+
legalComments: 'none',
|
|
606
|
+
logLevel: 'silent',
|
|
607
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
process.stdout.write(outPath + '\n');
|