@linzumi/cli 1.0.102 → 1.0.104
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
|
@@ -1471,7 +1471,7 @@ To kick the agent off:
|
|
|
1471
1471
|
`,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;uSe(n,{recursive:!0}),m(u)+L>i&&f(),_(L);let D=w()?`
|
|
1472
1472
|
`:"";return sSe(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=lSe(k,"utf8");for(let L of P.split(`
|
|
1473
1473
|
`))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 q=j.filter(Z=>{let fe=ISe(Z.parsed);return fe===void 0||fe>=L});if(q.length===0){y(D,"empty_or_expired");continue}let E=oP(D,".jsonl"),B=q.map(Z=>Z.raw).join(`
|
|
1474
|
-
`),z=q.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:q.length,...z.length===0?{}:{first_spooled_at:z[0],last_spooled_at:z[z.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+=q.length;try{h9(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 ySe,xSe,_Se,bSe,wSe,g9,SSe,kSe,x9=F(()=>{"use strict";Xy();ySe=32*1024*1024,xSe=10080*60*1e3,_Se=4*1024*1024,bSe=1024*1024,wSe="spool.jsonl",g9=/^batch-(\d{1,16})-[0-9a-f-]+\.jsonl$/;SSe=/\b([A-Z][A-Z0-9_]{1,})=("[^"\n]*"|'[^'\n]*'|[^\s"'`;,)\]}]+)/g,kSe=/(?:\/(?:[A-Za-z0-9._@~+-]|%[0-9A-Fa-f]{2})+){2,}/g});var jt,sP,ea=F(()=>{"use strict";jt="1.0.
|
|
1474
|
+
`),z=q.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:q.length,...z.length===0?{}:{first_spooled_at:z[0],last_spooled_at:z[z.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+=q.length;try{h9(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 ySe,xSe,_Se,bSe,wSe,g9,SSe,kSe,x9=F(()=>{"use strict";Xy();ySe=32*1024*1024,xSe=10080*60*1e3,_Se=4*1024*1024,bSe=1024*1024,wSe="spool.jsonl",g9=/^batch-(\d{1,16})-[0-9a-f-]+\.jsonl$/;SSe=/\b([A-Z][A-Z0-9_]{1,})=("[^"\n]*"|'[^'\n]*'|[^\s"'`;,)\]}]+)/g,kSe=/(?:\/(?:[A-Za-z0-9._@~+-]|%[0-9A-Fa-f]{2})+){2,}/g});var jt,sP,ea=F(()=>{"use strict";jt="1.0.104",sP=`linzumi ${jt}`});import{closeSync as b9,existsSync as dP,mkdirSync as RSe,openSync as w9,readFileSync as v9,renameSync as ESe,unlinkSync as gd,writeFileSync as DSe,writeSync as S9}from"node:fs";import{dirname as k9,join as MSe}from"node:path";function uP(e){return e instanceof hs||e instanceof Error&&e.name===T9&&"lockPath"in e&&"heldBy"in e}function cP(e,n=Wn(),t){let r=t===void 0?encodeURIComponent(e):dd(t);return MSe(k9(n),"runners",`${r}.lock`)}function u0(e){let n=cP(e.machineId,e.configPath,e.linzumiUrl),t=e.isPidAlive??A9,r=e.now??(()=>new Date),o=e.killPid??KSe,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:qn(e.linzumiUrl)},...e.launchSource===void 0?{}:{launchSource:e.launchSource},startedAt:i,heartbeatAt:i,cliVersion:e.cliVersion??jt,...e.gracefulUpdateDrain===!0?{gracefulUpdateDrain:!0}:{}};LSe(e.machineId,e.configPath,e.linzumiUrl,t,r,o,e.onTakeover),jSe(n,s,t,r,o,e.onTakeover,e.beforeReadExistingLock,e.beforeReplaceStaleLock);let a=()=>P9(n,s,r),d=setInterval(a,C9);return d.unref(),{path:n,record:s,heartbeat:a,release:()=>{clearInterval(d),D9(n,s)}}}function P9(e,n,t){try{let r=l0(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`;DSe(i,`${JSON.stringify(o,null,2)}
|
|
1475
1475
|
`,"utf8"),ESe(i,e)}catch{}}function yd(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>ASe?{state:"wedged",reason:e.heartbeatAt===void 0?"missing_heartbeat":"stale_heartbeat"}:{state:"live"}}function I9(e,n,t,r){return t!==c0||e.launchSource!==c0?!1:r.state!=="live"?!0:$Se(n,e.cliVersion)>0}function $Se(e,n){let t=_9(e),r=_9(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:OSe(t.prerelease,r.prerelease)}function _9(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=[aP(i[0]),aP(i[1]),aP(i[2])],a=o===""?[]:o.split(".").filter(d=>d!=="");return{core:s,prerelease:a}}function aP(e){let n=Number.parseInt(e??"",10);return Number.isInteger(n)&&n>=0?n:0}function OSe(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 R9(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 LSe(e,n,t,r,o,i,s){if(t===void 0||yr(t)!==yr(Bt))return;let a=cP(e,n),d=l0(a);if(d!==void 0){if(yd(d,r,o).state==="live")throw new hs(a,d);E9(a,r,()=>{let u=dP(a)?lP(a):void 0;if(u===void 0)return;let c=yd(u,r,o);if(c.state==="live")throw new hs(a,u);c.state==="wedged"&&R9(a,u,c.reason,i,s),gd(a)})}}function jSe(e,n,t,r,o,i,s,a){if(d0(e,n))return;s?.();let d=l0(e);if(d===void 0){if(d0(e,n))return;throw new Error(`another Linzumi runner lock appeared while starting: ${e}`)}if(yd(d,t,r).state==="live")throw new hs(e,d);a?.(),E9(e,t,()=>{let u=dP(e)?lP(e):void 0;if(u!==void 0){let c=yd(u,t,r);if(c.state==="live")throw new hs(e,u);c.state==="wedged"&&R9(e,u,c.reason,o,i),gd(e)}if(!d0(e,n))throw new Error(`another Linzumi runner lock appeared while starting: ${e}`)})}function d0(e,n){RSe(k9(e),{recursive:!0});try{let t=w9(e,"wx");try{S9(t,`${JSON.stringify(n,null,2)}
|
|
1476
1476
|
`)}finally{b9(t)}return!0}catch(t){if(Bc(t,"EEXIST"))return!1;throw t}}function E9(e,n,t){let r=`${e}.replace`;for(;;)try{let o=w9(r,"wx");try{S9(o,`${process.pid}
|
|
1477
1477
|
`),t()}finally{b9(o),gd(r)}return}catch(o){if(Bc(o,"EEXIST")){let i=FSe(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,569 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
2
|
+
// (M3 cluster 6, session-registry/leases).
|
|
3
|
+
// Relationship: Builds the TS-side PARITY ORACLE for the session-registry/
|
|
4
|
+
// leases cluster of the ReScript commander port, the sibling of
|
|
5
|
+
// build-queue-parity-oracle.mjs (cluster 5). The oracle bundles the REAL
|
|
6
|
+
// TS session/lease modules (runnerThreadSessionRegistry.ts,
|
|
7
|
+
// codexSessionOwnership.ts, leaseEpochGuard.ts, threadReconcilerLock.ts,
|
|
8
|
+
// the pin-registry/disposition slice of generationOverlap.ts, and the
|
|
9
|
+
// exported liveness classifiers from runner.ts - threadRunnerEntryLiveness
|
|
10
|
+
// + processIsAlive) behind the cluster 1/2 one-shot batch driver: a JSON
|
|
11
|
+
// array of cases on stdin, a JSON array of results on stdout. The ReScript
|
|
12
|
+
// package's RegistryParityConformance drives the SAME scripted inputs
|
|
13
|
+
// through the ported modules in its own scratch directories and asserts
|
|
14
|
+
// per-action outcome equivalence AND byte-identical registry files - the
|
|
15
|
+
// cross-impl round-trip contract that lets both commanders share
|
|
16
|
+
// ~/.linzumi through M4.
|
|
17
|
+
//
|
|
18
|
+
// Determinism: clocks are pinned per case (updatedAtMs/nowMs ride the
|
|
19
|
+
// inputs; the registry store takes nowMs on rebind), scripted lock
|
|
20
|
+
// sections use millisecond-scale sleeps sized so outcomes (not timings)
|
|
21
|
+
// are deterministic, timing NUMBERS inside diagnostics are normalized on
|
|
22
|
+
// both sides, and scratch paths are normalized through the "$DIR"
|
|
23
|
+
// placeholder in both directions. Worker-liveness pid probes use the
|
|
24
|
+
// driver's own live pid and a genuinely-dead child pid so the REAL
|
|
25
|
+
// process.kill(pid, 0) probe is exercised identically by both impls.
|
|
26
|
+
//
|
|
27
|
+
// SECRETS: the oracle only ever sees the conformance suite's synthetic
|
|
28
|
+
// payloads; nothing here logs case contents.
|
|
29
|
+
//
|
|
30
|
+
// The output is a local test artifact (.differential/ is gitignored, never
|
|
31
|
+
// published) and stays unminified for debuggability.
|
|
32
|
+
import { copyFile, mkdir } from 'node:fs/promises';
|
|
33
|
+
import { dirname, join } from 'node:path';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { build } from 'esbuild';
|
|
36
|
+
|
|
37
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
38
|
+
|
|
39
|
+
const driverSource = `
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
import {
|
|
42
|
+
mkdtempSync,
|
|
43
|
+
readdirSync,
|
|
44
|
+
readFileSync,
|
|
45
|
+
realpathSync,
|
|
46
|
+
statSync,
|
|
47
|
+
writeFileSync,
|
|
48
|
+
} from 'node:fs';
|
|
49
|
+
import { tmpdir } from 'node:os';
|
|
50
|
+
import { join, relative } from 'node:path';
|
|
51
|
+
import {
|
|
52
|
+
createRunnerThreadSessionRegistryStore,
|
|
53
|
+
defaultThreadSessionRehydrationMaxAgeMs,
|
|
54
|
+
reannounceThreadSessionCap,
|
|
55
|
+
reannounceableThreadSessions,
|
|
56
|
+
rehydrateThreadSessions,
|
|
57
|
+
runnerThreadSessionRegistryDir,
|
|
58
|
+
runnerThreadSessionRegistryPath,
|
|
59
|
+
} from './src/runnerThreadSessionRegistry';
|
|
60
|
+
import {
|
|
61
|
+
CodexSessionOwnershipConflictError,
|
|
62
|
+
findCodexSessionOwnerConflict,
|
|
63
|
+
sanitizeCodexSessionOwnership,
|
|
64
|
+
} from './src/codexSessionOwnership';
|
|
65
|
+
import {
|
|
66
|
+
createLeaseEpochRegistry,
|
|
67
|
+
isFencedClaim,
|
|
68
|
+
leaseEpochEnvelope,
|
|
69
|
+
readLeaseEpoch,
|
|
70
|
+
} from './src/leaseEpochGuard';
|
|
71
|
+
import {
|
|
72
|
+
ReconcilerLockQueuedTimeoutError,
|
|
73
|
+
ReconcilerLockTimeoutError,
|
|
74
|
+
createThreadReconcilerLock,
|
|
75
|
+
} from './src/threadReconcilerLock';
|
|
76
|
+
import {
|
|
77
|
+
createGenerationPinRegistry,
|
|
78
|
+
generationControlDisposition,
|
|
79
|
+
} from './src/generationOverlap';
|
|
80
|
+
import { processIsAlive, threadRunnerEntryLiveness } 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-registry-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, dir) {
|
|
139
|
+
return (event, payload) => {
|
|
140
|
+
events.push({ event, payload: substitute(payload, dir, dirPlaceholder) });
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Ownership pure transforms -----------------------------------------------------
|
|
145
|
+
|
|
146
|
+
function handleOwnership(testCase) {
|
|
147
|
+
switch (testCase.fn) {
|
|
148
|
+
case 'conflictMessage': {
|
|
149
|
+
const error = new CodexSessionOwnershipConflictError({
|
|
150
|
+
codexThreadId: testCase.codexThreadId,
|
|
151
|
+
ownerKandanThreadId: testCase.ownerThreadId,
|
|
152
|
+
requestingKandanThreadId: testCase.requestingThreadId,
|
|
153
|
+
});
|
|
154
|
+
return { message: error.message, name: error.name };
|
|
155
|
+
}
|
|
156
|
+
case 'findConflict':
|
|
157
|
+
return outcome(() =>
|
|
158
|
+
findCodexSessionOwnerConflict(testCase.records, testCase.bind)
|
|
159
|
+
);
|
|
160
|
+
case 'sanitize':
|
|
161
|
+
return outcome(() => sanitizeCodexSessionOwnership(testCase.records));
|
|
162
|
+
default:
|
|
163
|
+
return { threw: true, message: 'unknown ownership fn ' + String(testCase.fn) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Lease epoch registry -----------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function handleLease(testCase) {
|
|
170
|
+
switch (testCase.fn) {
|
|
171
|
+
case 'readLeaseEpoch':
|
|
172
|
+
return outcome(() => readLeaseEpoch(testCase.value));
|
|
173
|
+
case 'envelope':
|
|
174
|
+
return outcome(() => leaseEpochEnvelope(testCase.held ?? undefined));
|
|
175
|
+
case 'isFenced':
|
|
176
|
+
return outcome(() =>
|
|
177
|
+
isFencedClaim(testCase.held ?? undefined, testCase.control ?? undefined)
|
|
178
|
+
);
|
|
179
|
+
case 'scenario': {
|
|
180
|
+
const registry = createLeaseEpochRegistry();
|
|
181
|
+
const results = (testCase.actions ?? []).map((action) => {
|
|
182
|
+
switch (action.fn) {
|
|
183
|
+
case 'adopt':
|
|
184
|
+
return outcome(() => registry.adopt(action.threadId, action.epoch));
|
|
185
|
+
case 'current':
|
|
186
|
+
return outcome(() => registry.current(action.threadId));
|
|
187
|
+
case 'release':
|
|
188
|
+
return outcome(() => registry.release(action.threadId));
|
|
189
|
+
case 'snapshot':
|
|
190
|
+
return outcome(() => registry.snapshot());
|
|
191
|
+
default:
|
|
192
|
+
return { threw: true, message: 'unknown lease fn ' + String(action.fn) };
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return { results };
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
return { threw: true, message: 'unknown lease fn ' + String(testCase.fn) };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Registry store paths + scripted lifecycles --------------------------------------
|
|
203
|
+
|
|
204
|
+
function handleRegistryPaths(testCase) {
|
|
205
|
+
process.env.LINZUMI_SESSION_REGISTRY_DIR = testCase.envDir ?? '';
|
|
206
|
+
const result = {
|
|
207
|
+
dir: outcome(() => runnerThreadSessionRegistryDir()),
|
|
208
|
+
path: outcome(() => runnerThreadSessionRegistryPath(testCase.runnerId)),
|
|
209
|
+
};
|
|
210
|
+
delete process.env.LINZUMI_SESSION_REGISTRY_DIR;
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleRegistryScenario(testCase) {
|
|
215
|
+
const dir = scratch();
|
|
216
|
+
const path = join(dir, 'registry.json');
|
|
217
|
+
if (testCase.initialContents !== undefined) {
|
|
218
|
+
writeFileSync(path, testCase.initialContents, 'utf8');
|
|
219
|
+
}
|
|
220
|
+
const events = [];
|
|
221
|
+
const log = collectingLog(events, dir);
|
|
222
|
+
let store = createRunnerThreadSessionRegistryStore(path, log);
|
|
223
|
+
const results = (testCase.actions ?? []).map((action) => {
|
|
224
|
+
switch (action.fn) {
|
|
225
|
+
case 'upsert':
|
|
226
|
+
return outcome(() => store.upsert(action.record));
|
|
227
|
+
case 'remove':
|
|
228
|
+
return outcome(() => store.remove(action.threadId));
|
|
229
|
+
case 'rebind':
|
|
230
|
+
return outcome(() =>
|
|
231
|
+
store.rebindCodexThread(
|
|
232
|
+
action.threadId,
|
|
233
|
+
action.newHarnessSessionId,
|
|
234
|
+
action.nowMs ?? undefined
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
case 'list':
|
|
238
|
+
return outcome(() => store.list());
|
|
239
|
+
case 'reload': {
|
|
240
|
+
// Simulated restart: a fresh store re-reads (and re-sanitizes) the file.
|
|
241
|
+
store = createRunnerThreadSessionRegistryStore(path, log);
|
|
242
|
+
return { defined: false };
|
|
243
|
+
}
|
|
244
|
+
default:
|
|
245
|
+
return { threw: true, message: 'unknown registry fn ' + String(action.fn) };
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return substitute(
|
|
249
|
+
{ results, events, files: snapshotFiles(dir) },
|
|
250
|
+
dir,
|
|
251
|
+
dirPlaceholder
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- Rehydration orchestrator ---------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
async function handleRehydrate(testCase) {
|
|
258
|
+
const events = [];
|
|
259
|
+
const log = collectingLog(events, '\\u0000never');
|
|
260
|
+
const retired = [];
|
|
261
|
+
const attachCalls = [];
|
|
262
|
+
const attachScript = testCase.attach ?? {};
|
|
263
|
+
const unfinishedScript = testCase.unfinished ?? {};
|
|
264
|
+
const summary = await rehydrateThreadSessions({
|
|
265
|
+
records: testCase.records,
|
|
266
|
+
nowMs: testCase.nowMs,
|
|
267
|
+
maxAgeMs: testCase.maxAgeMs ?? defaultThreadSessionRehydrationMaxAgeMs,
|
|
268
|
+
pendingDurableRowCount: (record) =>
|
|
269
|
+
(testCase.pendingRows ?? {})[record.kandanThreadId] ?? 0,
|
|
270
|
+
...(testCase.useUnfinished === true
|
|
271
|
+
? {
|
|
272
|
+
hasUnfinishedWork: (record) => {
|
|
273
|
+
const value = unfinishedScript[record.kandanThreadId];
|
|
274
|
+
if (value === 'throw') {
|
|
275
|
+
throw new Error('scripted unfinished probe failure');
|
|
276
|
+
}
|
|
277
|
+
return value === true;
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
: {}),
|
|
281
|
+
attach: async (record) => {
|
|
282
|
+
attachCalls.push(record.kandanThreadId);
|
|
283
|
+
const value = attachScript[record.kandanThreadId] ?? 'failed';
|
|
284
|
+
if (value === 'throw') {
|
|
285
|
+
throw new Error('scripted attach failure');
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
},
|
|
289
|
+
retire: (record) => {
|
|
290
|
+
retired.push(record.kandanThreadId);
|
|
291
|
+
},
|
|
292
|
+
log,
|
|
293
|
+
});
|
|
294
|
+
return { summary, retired, attachCalls, events };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Re-announce selection --------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
function handleReannounce(testCase) {
|
|
300
|
+
const input = testCase.records.map((record) => ({ ...record }));
|
|
301
|
+
const entries = reannounceableThreadSessions(
|
|
302
|
+
input,
|
|
303
|
+
testCase.nowMs,
|
|
304
|
+
testCase.maxAgeMs ?? defaultThreadSessionRehydrationMaxAgeMs,
|
|
305
|
+
testCase.cap ?? reannounceThreadSessionCap
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
entries,
|
|
309
|
+
// The input must never be mutated (the filter-copies contract).
|
|
310
|
+
inputUnchanged:
|
|
311
|
+
JSON.stringify(input) === JSON.stringify(testCase.records),
|
|
312
|
+
defaults: {
|
|
313
|
+
maxAgeMs: defaultThreadSessionRehydrationMaxAgeMs,
|
|
314
|
+
cap: reannounceThreadSessionCap,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- Reconciler lock -----------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
function handleLockMessages(testCase) {
|
|
322
|
+
const running = new ReconcilerLockTimeoutError(
|
|
323
|
+
testCase.threadId,
|
|
324
|
+
testCase.timeoutMs
|
|
325
|
+
);
|
|
326
|
+
const queued = new ReconcilerLockQueuedTimeoutError({
|
|
327
|
+
threadId: testCase.threadId,
|
|
328
|
+
timeoutMs: testCase.timeoutMs,
|
|
329
|
+
queuedForMs: testCase.queuedForMs,
|
|
330
|
+
blockedSectionAgeMs: testCase.blockedSectionAgeMs ?? undefined,
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
running: { name: running.name, message: running.message },
|
|
334
|
+
queued: {
|
|
335
|
+
name: queued.name,
|
|
336
|
+
message: queued.message,
|
|
337
|
+
// The subclass relationship existing catches rely on.
|
|
338
|
+
isTimeoutSubclass: queued instanceof ReconcilerLockTimeoutError,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
344
|
+
|
|
345
|
+
async function handleLockScenario(testCase) {
|
|
346
|
+
const lock = createThreadReconcilerLock();
|
|
347
|
+
const started = [];
|
|
348
|
+
const settledOrder = [];
|
|
349
|
+
const callers = (testCase.sections ?? []).map((section) => {
|
|
350
|
+
const caller = lock
|
|
351
|
+
.runExclusive(
|
|
352
|
+
section.threadId,
|
|
353
|
+
async () => {
|
|
354
|
+
started.push(section.id);
|
|
355
|
+
if (section.sleepMs !== undefined) {
|
|
356
|
+
await sleep(section.sleepMs);
|
|
357
|
+
}
|
|
358
|
+
if (section.fail === true) {
|
|
359
|
+
throw new Error('scripted section failure ' + section.id);
|
|
360
|
+
}
|
|
361
|
+
return section.id;
|
|
362
|
+
},
|
|
363
|
+
section.deadlineMs ?? undefined
|
|
364
|
+
)
|
|
365
|
+
.then(
|
|
366
|
+
(value) => ({ id: section.id, outcome: 'completed', value }),
|
|
367
|
+
(error) => ({
|
|
368
|
+
id: section.id,
|
|
369
|
+
outcome:
|
|
370
|
+
error instanceof ReconcilerLockQueuedTimeoutError
|
|
371
|
+
? 'queued_timeout'
|
|
372
|
+
: error instanceof ReconcilerLockTimeoutError
|
|
373
|
+
? 'section_timeout'
|
|
374
|
+
: 'failed',
|
|
375
|
+
message:
|
|
376
|
+
error instanceof ReconcilerLockQueuedTimeoutError ||
|
|
377
|
+
error instanceof ReconcilerLockTimeoutError
|
|
378
|
+
? undefined
|
|
379
|
+
: error.message,
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
.then((result) => {
|
|
383
|
+
settledOrder.push(result.id);
|
|
384
|
+
return result;
|
|
385
|
+
});
|
|
386
|
+
return caller;
|
|
387
|
+
});
|
|
388
|
+
const results = await Promise.all(callers);
|
|
389
|
+
// Let any background sections settle so zombie runs (there must be none)
|
|
390
|
+
// are observable.
|
|
391
|
+
await sleep(testCase.settleMs ?? 150);
|
|
392
|
+
return {
|
|
393
|
+
results,
|
|
394
|
+
started,
|
|
395
|
+
settledOrder,
|
|
396
|
+
busyAfter: (testCase.sections ?? []).some((section) =>
|
|
397
|
+
lock.isBusy(section.threadId)
|
|
398
|
+
),
|
|
399
|
+
activeThreadCountAfter: lock.activeThreadCount(),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- Generation pins -------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
function handlePins(testCase) {
|
|
406
|
+
if (testCase.fn === 'disposition') {
|
|
407
|
+
const pinned = new Set(testCase.pinned ?? []);
|
|
408
|
+
return outcome(() =>
|
|
409
|
+
generationControlDisposition({
|
|
410
|
+
role: testCase.role,
|
|
411
|
+
threadId: testCase.threadId ?? undefined,
|
|
412
|
+
isPinned: (threadId) => pinned.has(threadId),
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const registry = createGenerationPinRegistry();
|
|
417
|
+
const results = (testCase.actions ?? []).map((action) => {
|
|
418
|
+
switch (action.fn) {
|
|
419
|
+
case 'pin':
|
|
420
|
+
return outcome(() => registry.pin(action.threadId));
|
|
421
|
+
case 'release':
|
|
422
|
+
return outcome(() => registry.release(action.threadId));
|
|
423
|
+
case 'isPinned':
|
|
424
|
+
return outcome(() => registry.isPinned(action.threadId));
|
|
425
|
+
case 'replaceAll':
|
|
426
|
+
return outcome(() => registry.replaceAll(action.threadIds));
|
|
427
|
+
case 'clear':
|
|
428
|
+
return outcome(() => registry.clear());
|
|
429
|
+
case 'snapshot':
|
|
430
|
+
return outcome(() => registry.snapshot());
|
|
431
|
+
case 'size':
|
|
432
|
+
return outcome(() => registry.size());
|
|
433
|
+
default:
|
|
434
|
+
return { threw: true, message: 'unknown pins fn ' + String(action.fn) };
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
return { results };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// --- Thread-worker liveness ---------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
// A genuinely-dead pid: spawnSync returns only after the child exits, so
|
|
443
|
+
// the pid no longer maps to a live process (the TS unit tests' technique).
|
|
444
|
+
let cachedDeadPid;
|
|
445
|
+
function deadPid() {
|
|
446
|
+
if (cachedDeadPid === undefined) {
|
|
447
|
+
const result = spawnSync(process.execPath, ['-e', '0']);
|
|
448
|
+
cachedDeadPid = result.pid;
|
|
449
|
+
}
|
|
450
|
+
return cachedDeadPid;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function resolvePid(spec) {
|
|
454
|
+
if (spec === 'live') return process.pid;
|
|
455
|
+
if (spec === 'dead') return deadPid();
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function fakeCodexClient(isClosed) {
|
|
460
|
+
return {
|
|
461
|
+
request: async () => ({ jsonrpc: '2.0', id: 1, result: {} }),
|
|
462
|
+
notify: () => undefined,
|
|
463
|
+
onNotification: () => undefined,
|
|
464
|
+
onRequest: () => undefined,
|
|
465
|
+
onClose: () => undefined,
|
|
466
|
+
isClosed: () => isClosed === true,
|
|
467
|
+
close: () => undefined,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function handleWorkerLiveness(testCase) {
|
|
472
|
+
if (testCase.fn === 'processIsAlive') {
|
|
473
|
+
return outcome(() => processIsAlive(resolvePid(testCase.pid)));
|
|
474
|
+
}
|
|
475
|
+
const entry =
|
|
476
|
+
testCase.entry.kind === 'starting'
|
|
477
|
+
? { kind: 'starting', promise: new Promise(() => undefined) }
|
|
478
|
+
: {
|
|
479
|
+
kind: 'running',
|
|
480
|
+
handle: {
|
|
481
|
+
kandanThreadId: testCase.entry.threadId ?? 'thread-1',
|
|
482
|
+
codex: fakeCodexClient(testCase.entry.ipcClosed),
|
|
483
|
+
close: async () => undefined,
|
|
484
|
+
...(testCase.entry.sessionClosed === undefined
|
|
485
|
+
? {}
|
|
486
|
+
: { sessionClosed: () => testCase.entry.sessionClosed === true }),
|
|
487
|
+
...(testCase.entry.pid === undefined
|
|
488
|
+
? {}
|
|
489
|
+
: { processPid: resolvePid(testCase.entry.pid) }),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
return outcome(() => threadRunnerEntryLiveness(entry));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- Dispatch --------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
async function handle(testCase) {
|
|
498
|
+
switch (testCase.op) {
|
|
499
|
+
case 'ownership':
|
|
500
|
+
return handleOwnership(testCase);
|
|
501
|
+
case 'lease':
|
|
502
|
+
return handleLease(testCase);
|
|
503
|
+
case 'registryPaths':
|
|
504
|
+
return handleRegistryPaths(testCase);
|
|
505
|
+
case 'registryScenario':
|
|
506
|
+
return handleRegistryScenario(testCase);
|
|
507
|
+
case 'rehydrate':
|
|
508
|
+
return await handleRehydrate(testCase);
|
|
509
|
+
case 'reannounce':
|
|
510
|
+
return handleReannounce(testCase);
|
|
511
|
+
case 'lockMessages':
|
|
512
|
+
return handleLockMessages(testCase);
|
|
513
|
+
case 'lockScenario':
|
|
514
|
+
return await handleLockScenario(testCase);
|
|
515
|
+
case 'pins':
|
|
516
|
+
return handlePins(testCase);
|
|
517
|
+
case 'workerLiveness':
|
|
518
|
+
return handleWorkerLiveness(testCase);
|
|
519
|
+
default:
|
|
520
|
+
return { threw: true, message: 'unknown op ' + String(testCase.op) };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let stdin = '';
|
|
525
|
+
process.stdin.setEncoding('utf8');
|
|
526
|
+
for await (const chunk of process.stdin) {
|
|
527
|
+
stdin += chunk;
|
|
528
|
+
}
|
|
529
|
+
const cases = JSON.parse(stdin);
|
|
530
|
+
const results = [];
|
|
531
|
+
for (const testCase of cases) {
|
|
532
|
+
results.push(await handle(testCase));
|
|
533
|
+
}
|
|
534
|
+
process.stdout.write(JSON.stringify(results), () => {
|
|
535
|
+
process.exit(0);
|
|
536
|
+
});
|
|
537
|
+
`;
|
|
538
|
+
|
|
539
|
+
await mkdir(join(packageRoot, '.differential'), { recursive: true });
|
|
540
|
+
|
|
541
|
+
// runner.ts's graph resolves assets/ relative to the bundle at import time
|
|
542
|
+
// (the hello-project scaffold's inlined logo), so the asset must sit next
|
|
543
|
+
// to the emitted oracle - the build-auth-parity-oracle precedent.
|
|
544
|
+
await mkdir(join(packageRoot, '.differential/assets'), { recursive: true });
|
|
545
|
+
await copyFile(
|
|
546
|
+
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|
|
547
|
+
join(packageRoot, '.differential/assets/linzumi-logo.svg')
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
await build({
|
|
551
|
+
stdin: {
|
|
552
|
+
contents: driverSource,
|
|
553
|
+
resolveDir: packageRoot,
|
|
554
|
+
sourcefile: 'session-registry-parity-driver.ts',
|
|
555
|
+
loader: 'ts',
|
|
556
|
+
},
|
|
557
|
+
bundle: true,
|
|
558
|
+
platform: 'node',
|
|
559
|
+
target: 'node20',
|
|
560
|
+
format: 'esm',
|
|
561
|
+
outfile: join(
|
|
562
|
+
packageRoot,
|
|
563
|
+
'.differential/session-registry-parity-oracle.mjs'
|
|
564
|
+
),
|
|
565
|
+
minify: false,
|
|
566
|
+
sourcemap: false,
|
|
567
|
+
legalComments: 'none',
|
|
568
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
569
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// publishes @linzumi/cli@<new-version> to npm.
|
|
13
13
|
//
|
|
14
14
|
// The package version is pinned in several places (package.json,
|
|
15
|
-
// src/version.ts, README.md, and version-pinning tests). This script rewrites
|
|
15
|
+
// src/version.ts, README.md, the public alias manifest, and version-pinning tests). This script rewrites
|
|
16
16
|
// every known site, FAILS LOUDLY if any expected pattern is missing, and then
|
|
17
17
|
// scans package.json/src/README/test/scripts/bin for any leftover
|
|
18
18
|
// old-version string - so a future, not-yet-listed pin site cannot silently
|
|
@@ -71,6 +71,7 @@ if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$/.test(newVersion)) {
|
|
|
71
71
|
|
|
72
72
|
const packageRoot =
|
|
73
73
|
explicitRoot ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
74
|
+
const aliasPackageRoot = join(dirname(packageRoot), 'linzumi');
|
|
74
75
|
|
|
75
76
|
let manifest;
|
|
76
77
|
|
|
@@ -96,6 +97,24 @@ if (oldVersion === newVersion) {
|
|
|
96
97
|
// the bump aborts; add new entries here when a new pin site appears (the
|
|
97
98
|
// leftover scan below is what tells you one appeared).
|
|
98
99
|
const knownSites = [
|
|
100
|
+
{
|
|
101
|
+
file: '../linzumi/package.json',
|
|
102
|
+
description: 'public alias manifest version',
|
|
103
|
+
pattern: `"version": "${oldVersion}"`,
|
|
104
|
+
replacement: `"version": "${newVersion}"`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
file: '../linzumi/package.json',
|
|
108
|
+
description: 'public alias exact canonical dependency',
|
|
109
|
+
pattern: `"@linzumi/cli": "${oldVersion}"`,
|
|
110
|
+
replacement: `"@linzumi/cli": "${newVersion}"`,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
file: '../../pnpm-lock.yaml',
|
|
114
|
+
description: 'public alias lockfile dependency specifier',
|
|
115
|
+
pattern: `specifier: ${oldVersion}\n version: link:../linzumi-cli`,
|
|
116
|
+
replacement: `specifier: ${newVersion}\n version: link:../linzumi-cli`,
|
|
117
|
+
},
|
|
99
118
|
{
|
|
100
119
|
file: 'package.json',
|
|
101
120
|
description: 'npm package manifest version',
|
|
@@ -209,6 +228,11 @@ function scanTargets() {
|
|
|
209
228
|
const roots = ['src', 'test', 'scripts', 'bin'];
|
|
210
229
|
const files = ['package.json', 'README.md'];
|
|
211
230
|
|
|
231
|
+
// Spec: plans/2026-07-12-linzumi-unscoped-npm-package-spec.md
|
|
232
|
+
// The alias version and canonical dependency are one release invariant.
|
|
233
|
+
files.push(relative(packageRoot, join(aliasPackageRoot, 'package.json')));
|
|
234
|
+
files.push('../../pnpm-lock.yaml');
|
|
235
|
+
|
|
212
236
|
for (const root of roots) {
|
|
213
237
|
const rootPath = join(packageRoot, root);
|
|
214
238
|
let entries;
|