@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
@@ -58,7 +58,7 @@ Install the CLI or run it with `npx`:
58
58
  ```bash
59
59
  npm install -g @linzumi/cli@latest
60
60
  npx -y @linzumi/cli@latest signup
61
- npx -y @linzumi/cli@1.0.102 --version
61
+ npx -y @linzumi/cli@1.0.104 --version
62
62
  linzumi --version
63
63
  ```
64
64
 
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.102",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)}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;