@volter/world-core 3.0.155 → 3.0.157

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.
Files changed (150) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +204 -4
  6. package/dist/inject.cjs +150 -86
  7. package/dist/network-policy.cjs +11 -1
  8. package/dist/network-policy.d.cts +1 -0
  9. package/dist/pack-facts.d.cts +3 -0
  10. package/dist/required-credential-transport.cjs +29 -0
  11. package/dist/required-credential-transport.d.cts +3 -0
  12. package/dist/src/actions.d.ts +15 -0
  13. package/dist/src/actions.js +82 -21
  14. package/dist/src/ancestry.d.ts +5 -4
  15. package/dist/src/ancestry.js +53 -28
  16. package/dist/src/blob-store.d.ts +16 -0
  17. package/dist/src/blob-store.js +76 -1
  18. package/dist/src/changeset.js +1 -1
  19. package/dist/src/clickhouse/compression.d.ts +28 -0
  20. package/dist/src/clickhouse/compression.js +281 -0
  21. package/dist/src/clickhouse/index.d.ts +1 -0
  22. package/dist/src/clickhouse/index.js +1 -0
  23. package/dist/src/derived-core.d.ts +84 -7
  24. package/dist/src/derived-core.js +336 -61
  25. package/dist/src/derived-real.js +97 -15
  26. package/dist/src/derived.d.ts +5 -0
  27. package/dist/src/derived.js +3 -0
  28. package/dist/src/executor.d.ts +7 -0
  29. package/dist/src/executor.js +15 -1
  30. package/dist/src/fork.js +1 -1
  31. package/dist/src/git/lfs.d.ts +13 -3
  32. package/dist/src/git/lfs.js +41 -9
  33. package/dist/src/grpc-wire.js +2 -1
  34. package/dist/src/h2.js +63 -10
  35. package/dist/src/head.d.ts +5 -0
  36. package/dist/src/head.js +15 -0
  37. package/dist/src/history.d.ts +1 -1
  38. package/dist/src/history.js +7 -7
  39. package/dist/src/index.d.ts +5 -1
  40. package/dist/src/index.js +6 -1
  41. package/dist/src/live/ingest.js +9 -13
  42. package/dist/src/live/rtmp.js +7 -5
  43. package/dist/src/live/segmenter.js +7 -1
  44. package/dist/src/log.d.ts +23 -3
  45. package/dist/src/log.js +160 -37
  46. package/dist/src/machines.js +5 -1
  47. package/dist/src/managed-go-programs.d.ts +55 -0
  48. package/dist/src/managed-go-programs.js +133 -0
  49. package/dist/src/managed-original-inputs.d.ts +101 -0
  50. package/dist/src/managed-original-inputs.js +125 -0
  51. package/dist/src/mongodb/client.d.ts +8 -0
  52. package/dist/src/mongodb/client.js +27 -0
  53. package/dist/src/mongodb/index.d.ts +2 -0
  54. package/dist/src/mongodb/index.js +5 -0
  55. package/dist/src/mongodb/stream.d.ts +97 -0
  56. package/dist/src/mongodb/stream.js +206 -0
  57. package/dist/src/observe.js +1 -1
  58. package/dist/src/pack-fetch.d.ts +6 -1
  59. package/dist/src/pack-fetch.js +69 -14
  60. package/dist/src/pack-paths.d.ts +2 -1
  61. package/dist/src/pack-paths.js +6 -1
  62. package/dist/src/packRegistry.d.ts +10 -14
  63. package/dist/src/packRegistry.js +2 -13
  64. package/dist/src/protocol.d.ts +13 -0
  65. package/dist/src/protocol.js +13 -0
  66. package/dist/src/redis/engine.d.ts +10 -4
  67. package/dist/src/redis/engine.js +103 -32
  68. package/dist/src/redis/member-storage.js +20 -8
  69. package/dist/src/redis/service-times.d.ts +25 -0
  70. package/dist/src/redis/service-times.js +87 -0
  71. package/dist/src/redis/stream.d.ts +2 -1
  72. package/dist/src/redis/stream.js +109 -20
  73. package/dist/src/resource-blob.d.ts +4 -0
  74. package/dist/src/resource-blob.js +8 -1
  75. package/dist/src/runtime-state.d.ts +11 -0
  76. package/dist/src/runtime-state.js +13 -0
  77. package/dist/src/runtime.d.ts +7 -5
  78. package/dist/src/runtime.js +7 -4
  79. package/dist/src/serve-http.d.ts +2 -0
  80. package/dist/src/serve-http.js +73 -6
  81. package/dist/src/serve.d.ts +10 -0
  82. package/dist/src/serve.js +43 -3
  83. package/dist/src/sockets.d.ts +4 -1
  84. package/dist/src/sockets.js +59 -6
  85. package/dist/src/storage.d.ts +2 -1
  86. package/dist/src/storage.js +6 -3
  87. package/dist/src/vendor-call.js +58 -0
  88. package/dist/src/world-boot.d.ts +1 -0
  89. package/dist/src/world-boot.js +6 -0
  90. package/dist/src/world-clock.js +6 -2
  91. package/dist/src/world-store.d.ts +122 -7
  92. package/dist/src/world-store.js +959 -103
  93. package/dist/src/xml.d.ts +30 -0
  94. package/dist/src/xml.js +74 -0
  95. package/generated/pack-facts.json +204 -4
  96. package/inject.cjs +150 -86
  97. package/network-policy.cjs +11 -1
  98. package/network-policy.d.cts +1 -0
  99. package/pack-facts.d.cts +3 -0
  100. package/package.json +27 -1
  101. package/required-credential-transport.cjs +29 -0
  102. package/required-credential-transport.d.cts +3 -0
  103. package/src/actions.ts +78 -21
  104. package/src/ancestry.ts +46 -24
  105. package/src/blob-store.ts +59 -1
  106. package/src/changeset.ts +1 -1
  107. package/src/clickhouse/compression.ts +172 -0
  108. package/src/clickhouse/index.ts +2 -0
  109. package/src/derived-core.ts +356 -61
  110. package/src/derived-real.ts +80 -14
  111. package/src/derived.ts +9 -1
  112. package/src/executor.ts +17 -3
  113. package/src/fork.ts +1 -1
  114. package/src/git/lfs.ts +40 -9
  115. package/src/grpc-wire.ts +2 -1
  116. package/src/h2.ts +46 -5
  117. package/src/head.ts +15 -0
  118. package/src/history.ts +7 -7
  119. package/src/index.ts +7 -1
  120. package/src/live/ingest.ts +7 -3
  121. package/src/live/rtmp.ts +7 -5
  122. package/src/live/segmenter.ts +7 -1
  123. package/src/log.ts +147 -32
  124. package/src/machines.ts +5 -1
  125. package/src/managed-go-programs.ts +146 -0
  126. package/src/managed-original-inputs.ts +163 -0
  127. package/src/mongodb/client.ts +27 -0
  128. package/src/mongodb/index.ts +5 -0
  129. package/src/mongodb/stream.ts +223 -0
  130. package/src/observe.ts +1 -1
  131. package/src/pack-fetch.ts +66 -16
  132. package/src/pack-paths.ts +5 -1
  133. package/src/packRegistry.ts +3 -15
  134. package/src/protocol.ts +13 -0
  135. package/src/redis/engine.ts +100 -57
  136. package/src/redis/member-storage.ts +17 -9
  137. package/src/redis/service-times.ts +81 -0
  138. package/src/redis/stream.ts +66 -14
  139. package/src/resource-blob.ts +9 -1
  140. package/src/runtime-state.ts +14 -0
  141. package/src/runtime.ts +10 -3
  142. package/src/serve-http.ts +65 -6
  143. package/src/serve.ts +45 -3
  144. package/src/sockets.ts +39 -8
  145. package/src/storage.ts +5 -3
  146. package/src/vendor-call.ts +54 -0
  147. package/src/world-boot.ts +6 -0
  148. package/src/world-clock.ts +6 -2
  149. package/src/world-store.ts +768 -91
  150. package/src/xml.ts +71 -0
@@ -30,6 +30,8 @@ import {
30
30
  existsSync,
31
31
  fsyncSync,
32
32
  fstatSync,
33
+ linkSync,
34
+ ftruncateSync,
33
35
  lstatSync,
34
36
  mkdirSync,
35
37
  openSync,
@@ -42,16 +44,87 @@ import {
42
44
  statSync,
43
45
  unlinkSync,
44
46
  writeFileSync,
47
+ writeSync,
45
48
  } from 'node:fs';
49
+ import type { Dirent, Stats } from 'node:fs';
46
50
  import { hostname } from 'node:os';
51
+ import { randomUUID } from 'node:crypto';
47
52
  import { refuseReadOnlyWrite } from './request-scope.ts';
48
53
  import { basename, dirname, join, resolve } from 'node:path';
49
54
 
55
+ // WHAT A LOCK HOLD ASKS (architecture, "What a hold asks is counted"): the file
56
+ // store's questions counted inside an outermost lock hold and outside one, said at each outermost release where
57
+ // VOLTER_TWIN_LOG=1 (storage.ts twinLog's gate and line), and not counted otherwise. Each counter counts the calls of
58
+ // its own method: a readKept that misses its kept text, or meets a link, also counts the read it makes. A hold's
59
+ // `heldMs` is the time of the hold's body, run once the lock is taken and before it is released, as the lock's release
60
+ // line times its hold; the wait before the take is the wait line's `waitedMs`.
61
+ const QUESTION_KINDS = ['generation', 'read', 'readKept', 'stat', 'exists', 'list', 'append'] as const;
62
+ type QuestionCounts = Record<(typeof QUESTION_KINDS)[number], number>;
63
+ // Read once, where a process is: a realm with none counts nothing.
64
+ const questionsCounted = typeof process !== 'undefined' && process.env?.VOLTER_TWIN_LOG === '1';
65
+ const noQuestions = (): QuestionCounts => ({ generation: 0, read: 0, readKept: 0, stat: 0, exists: 0, list: 0, append: 0 });
66
+ let lockHoldDepth = 0;
67
+ let questionsHeld = noQuestions();
68
+ let questionsUnheld = noQuestions();
69
+ function asked(kind: (typeof QUESTION_KINDS)[number]): void {
70
+ if (questionsCounted) (lockHoldDepth > 0 ? questionsHeld : questionsUnheld)[kind] += 1;
71
+ }
72
+ function countedHold<T>(operation: string, fn: () => T): () => T {
73
+ if (!questionsCounted) return fn;
74
+ return () => {
75
+ const began = Date.now();
76
+ lockHoldDepth += 1;
77
+ try { return fn(); }
78
+ finally {
79
+ lockHoldDepth -= 1;
80
+ if (lockHoldDepth === 0) {
81
+ const held = questionsHeld, before = questionsUnheld;
82
+ questionsHeld = noQuestions(); questionsUnheld = noQuestions();
83
+ try { console.error('[twin:store-lock]', JSON.stringify({ operation, heldMs: Date.now() - began, held, before })); } catch { /* an instrument */ }
84
+ }
85
+ }
86
+ };
87
+ }
88
+
50
89
  /** Metadata a caller needs about a stored path. `isDirectory` distinguishes a JSON
51
90
  * sidecar from a per-service subdir (validate/scrub walk on it); `size`+`mtimeMs`
52
91
  * drive the append-dedupe index's cache-validity check in storage.ts. */
53
92
  export type WorldStat = { size: number; mtimeMs: number; isDirectory: boolean; isSymbolicLink?: boolean };
54
93
 
94
+ /** Diagnostic context only; it does not change lock acquisition or stale-owner policy. */
95
+ export type WorldLockOptions = { retainLiveOwner?: boolean; operation?: string; caller?: string };
96
+
97
+ /**
98
+ * Capture at the outermost lock helper, then carry the same caller through nested helpers.
99
+ * LIMIT THE CAPTURE TO THE FRAMES READ (browser-substrate S2 R44, AC-LC3): V8's third frame supplies the caller,
100
+ * while new Error().stack normally collects up to Error.stackTraceLimit frames (ten by default).
101
+ * Retained browser-substrate LibreChat World-realm warm-up profiles 73/74 from runs 36c6d279 and 991f63e7e
102
+ * attributed about 10-57 ms self/inclusive to storageLockScope over each full 13.3-13.4 second profile window.
103
+ * Samples are about 5 ms apart. Those totals include this helper's capture, string and regex work; they are
104
+ * neither isolated capture time nor the credential doors' share. The co-located host serves doors on one loop.
105
+ * Original profiles are under /Users/aaronclaw/heavy-lead/librechat/logs-kept/:
106
+ * librechat-sn-36c6d279-e9e2632a-004051/cpuprofile/ (SHA256 prefixes 04dfc4fe, 77df6e8e) and
107
+ * librechat-sn-991f63e7e/cpuprofile/ (be6c6a93, 2fe80e7c), realms 73 and 74 respectively.
108
+ * The limit is lowered to three only for Error's own writable numeric data property above three, and restored
109
+ * only when lowered. Absent, accessor, non-writable and lower limits receive no writes. Existing constructor
110
+ * and user stack-formatter failure paths remain; this diagnostic optimization changes no lock ownership rule.
111
+ */
112
+ export function storageLockScope(options: WorldLockOptions = {}): WorldLockOptions & { operation: string; caller: string } {
113
+ let frame: string | undefined;
114
+ if (!options.caller) {
115
+ const held = Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit');
116
+ const lowered = held !== undefined && 'value' in held && held.writable === true
117
+ && typeof held.value === 'number' && held.value > 3;
118
+ if (lowered) Error.stackTraceLimit = 3;
119
+ try { frame = new Error().stack?.split('\n')[3]; }
120
+ finally { if (lowered) Error.stackTraceLimit = held!.value as number; }
121
+ }
122
+ const location = frame?.slice(frame.lastIndexOf('(') + 1).trim().replace(/^at\s+/, '');
123
+ const site = location?.match(/^(.*):(\d+):\d+\)?$/);
124
+ const caller = options.caller ?? (site ? `${site[1]!.replace(/^file:\/\//, '')}:${site[2]}` : 'unknown:0');
125
+ return { ...options, caller, operation: options.operation ?? caller };
126
+ }
127
+
55
128
  /**
56
129
  * The synchronous persistence seam. Every path is an ABSOLUTE key the caller already
57
130
  * computed (via `worldPaths`/`worldStateRoot`); the store treats it as an opaque key,
@@ -92,12 +165,31 @@ export interface WorldStore {
92
165
  readRange?(path: string, start: number): string | null;
93
166
  /** Resolve filesystem aliases for durable ownership; absent on opaque-key stores. */
94
167
  canonicalPath?(path: string): string;
168
+ /** `read`, for a small file every operation needs and few change (a branch's pointer, the World's clock): the text
169
+ * kept from the last read while the path's own facts stand, so an unchanged file costs one check and no read.
170
+ * Optional: a store without it is read. */
171
+ readKept?(path: string): string | null;
172
+ /** The facts of the generation kept beside a tree's files in `dirPath` (TREE_FILES): one string that differs after
173
+ * any write of one of them through a store, by this process or another; `null` where the directory has none yet,
174
+ * which says nothing. Optional: a store without it is asked for each file's own facts. */
175
+ generation?(dirPath: string): string | null;
176
+ /** The generation in `dirPath` now, when exactly one write of a tree's file through a store has moved it since
177
+ * `before` (a generation this store answered for it); `null` when it cannot say so. A writer that read `before` just
178
+ * ahead of its own write, holding the directory's history lock, learns that its write was the only one (log.ts
179
+ * appendToBranchLog). Optional, beside `generation`. */
180
+ generationAfterOwnWrite?(dirPath: string, before: string): string | null;
95
181
  /** Run `fn` holding an exclusive lock on `lockPath`, releasing it afterwards. The fs
96
182
  * backend uses the historical cross-process reclaimable file lock; an in-memory,
97
183
  * single-instance backend is a synchronous pass-through. */
98
- withLock<T>(lockPath: string, fn: () => T, options?: { retainLiveOwner?: boolean }): T;
184
+ withLock<T>(lockPath: string, fn: () => T, options?: WorldLockOptions): T;
99
185
  }
100
186
 
187
+ /** The files a service's tree is folded from, by name in its state directory (log.ts: the parent log, the branch log,
188
+ * the branch's pointer), and the generation a store keeps beside them. */
189
+ export const TREE_FILES = { parent: 'events.jsonl', branch: 'actions.jsonl', pointer: 'branch.json' } as const;
190
+ export const TREE_GENERATION = 'tree.generation';
191
+ const TREE_FILE_NAMES: ReadonlySet<string> = new Set(Object.values(TREE_FILES));
192
+
101
193
  // ── FsWorldStore — the DEFAULT, byte-identical to the historical node:fs behavior ────
102
194
 
103
195
  let fsAtomicSequence = 0;
@@ -105,17 +197,359 @@ let fsAtomicSequence = 0;
105
197
  /** How long a lock may live before an acquirer treats it as abandoned. Above the 10s
106
198
  * wait timeout so a live-but-slow holder is never reclaimed out from under itself. */
107
199
  const LOCK_STALE_MS = 60_000;
200
+ /**
201
+ * How long an EMPTY lock file is a taker's, between its exclusive create and its holder's record (S2 R13). A taker
202
+ * writes the record in the call after the create (in a tab's file step, in the same request), and one whose record is
203
+ * not written gives the lock up and takes it again (withLock), so no live holder holds a lock with an empty record past
204
+ * that moment. An empty file older than this is a taker's that ended between the two calls (a process killed there,
205
+ * or a state directory copied while one was there), and it is no one's: before, it was waited on until its file was
206
+ * 60 s old, and a lock taken to keep its live owner (the ancestry coordinator) waited on it for ever. Twenty e946aa4c
207
+ * dry-run1: a 0-byte branch-ancestry.lock, the Redis twin's every write waiting 10 s on it inside its history lock,
208
+ * and Twenty's Redis client's 5 s connect timing out (FindAllViews 500, the walk's step 7).
209
+ */
210
+ // Five seconds: in a tab without the kernel's file step the create and the record are two requests of the kernel,
211
+ // and Twenty's Redis-twin handoffs have exceeded 1 s; and under the 10 s a
212
+ // waiter waits before it gives up, so a waiter reclaims such a file rather than timing out on it.
213
+ const EMPTY_RECORD_GRACE_MS = 5_000;
214
+ /** How many takes whose record could not be written are made before the failure is the caller's. */
215
+ const RECORD_ATTEMPTS = 3;
216
+
217
+ type LockHolder = {
218
+ pid: number; hostname: string; at: string; token?: string;
219
+ operation?: string; caller?: string;
220
+ waitingFor?: { path: string; operation: string; since: number };
221
+ };
222
+
223
+ /** The locks this process holds now, by path, and the token each was recorded with (withLock). */
224
+ const ownLocks = new Map<string, string>();
108
225
 
109
- type LockHolder = { pid: number; hostname: string; at: string };
226
+ type LockReading = {
227
+ /** The take's descriptor; null where a kernel's file step took it (fileSteps) and kept none. */
228
+ path: string; fd: number | null; holder: LockHolder;
229
+ /** The bytes of the record the descriptor last wrote: a shorter one is padded to it (recordLockReading). */
230
+ recordBytes?: number;
231
+ nestedWaits: Map<string, { path: string; operation: string; waitedMs: number }>;
232
+ };
233
+ const lockReadings: LockReading[] = [];
234
+
235
+ /**
236
+ * A KERNEL'S FILE STEPS (S2 R17d), where this process runs in a tab: browser-substrate's KernelVirtualFS offers them
237
+ * on the run's tree (`process[Symbol.for('tabnode.run.vfs')]`, under `Symbol.for('browser-substrate.kernel-file-steps')`),
238
+ * each one request of the kernel's worker that makes, by its own calls in the same order, what this store made as
239
+ * requests of their own: a lock's take (an exclusive create, its holder's record, a close), its release (the record
240
+ * read, its token compared, the unlink: SYS_unlink's, with the parent's permission and sticky-bit checks the path
241
+ * unlink it replaces did not make) and a tree file's append between its generation's two bytes. A write of a
242
+ * twin took three locks and an append, about thirty requests; these make it seven. Each step answers undefined where
243
+ * it is not taken (a path the tab keeps outside the kernel, parts too large for one request), and this store then
244
+ * makes its own calls. Absent on a host's filesystem, which is asked as before.
245
+ */
246
+ type KernelFileSteps = {
247
+ createExclusive(path: string, data: string, mode: number): boolean | undefined;
248
+ changeHolding(path: string, needle: string, replacement: string | undefined): { changed: true } | { changed: false; observed: string | null } | undefined;
249
+ appendBetween(markerPath: string, marker: string, path: string, data: string): boolean;
250
+ };
251
+ function fileSteps(): KernelFileSteps | undefined {
252
+ const tree = (process as unknown as Record<symbol, unknown>)[Symbol.for('tabnode.run.vfs')] as Record<symbol, unknown> | null | undefined;
253
+ const offered = tree?.[Symbol.for('browser-substrate.kernel-file-steps')];
254
+ return offered && typeof offered === 'object' ? offered as KernelFileSteps : undefined;
255
+ }
256
+ /** The bytes a holder's record holds for its token, as JSON.stringify writes them: a key and a string, no space. */
257
+ const tokenNeedle = (token: string): string => `"token":${JSON.stringify(token)}`;
258
+
259
+ /** A two-call take whose path did not hold its own record after it: another reclaimed it between the calls. */
260
+ const RECLAIMED_BEFORE_RECORD = 'VOLTER_LOCK_RECLAIMED';
261
+ /** A take whose record could not be written whole: nothing of it is held. */
262
+ const RECORD_NOT_WRITTEN = 'VOLTER_LOCK_UNRECORDED';
263
+ /** Where link(2) is not an exclusive create the store can use: the take falls back to two calls. */
264
+ const LINK_UNSUPPORTED = new Set(['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'ENOSYS', 'EXDEV', 'EMLINK', 'EINVAL']);
265
+
266
+ /**
267
+ * A FILE MADE EXCLUSIVELY WITH ITS RECORD ALREADY IN IT (S2 R13): a lock, or a lock's reclaim record, is never seen
268
+ * empty while its taker lives, so an empty one is always a dead taker's and is reclaimed (lockIsStale, recoveryIsStale).
269
+ * - In a tab: the kernel's file step makes it and writes the record in one request (fileSteps, S2 R17d).
270
+ * - On a host: the record is written whole to a private file of the same directory, which link(2) puts at the path,
271
+ * an exclusive create that fails EEXIST; the private name is then removed. The lock is opened for its descriptor.
272
+ * Never in a tab: tabnode's link copies over the path (no EEXIST), so it is no exclusive create there.
273
+ * - Neither (a tab whose kernel offers no step for the path, a host filesystem without link): two calls, the
274
+ * exclusive create and the record, and the path read back: held only if it holds this take's token. A record that
275
+ * is not written leaves the file removed only where it is still empty or this take's (read before it is removed).
276
+ * EEXIST is thrown as the create's; RECLAIMED_BEFORE_RECORD and RECORD_NOT_WRITTEN say a take not held.
277
+ */
278
+ function createRecorded(path: string, text: string, token: string, operation: string | undefined): { fd: number | null; bytes: number } {
279
+ const bytes = Buffer.byteLength(text);
280
+ const steps = fileSteps();
281
+ const stepped = steps?.createExclusive(path, text, 0o666);
282
+ if (stepped === true) return { fd: null, bytes };
283
+ if (stepped === false) {
284
+ // made in that one request and its record short. Removed only while the path still holds that short record (a
285
+ // prefix of this take's own text) or nothing: a stall past the grace may have let another reclaim it and take it
286
+ lockRecordLine('publish-failed', path, { operation });
287
+ let held: string | undefined;
288
+ try { held = readFileSync(path, 'utf8'); } catch { held = undefined; }
289
+ if (held !== undefined && (held === '' || text.startsWith(held))) { try { unlinkSync(path); } catch { /* gone */ } }
290
+ throw Object.assign(new Error(`${path}: its record was not written whole`), { code: RECORD_NOT_WRITTEN });
291
+ }
292
+ const inTab = (process as unknown as Record<symbol, unknown>)[Symbol.for('tabnode.run.vfs')] !== undefined;
293
+ if (!inTab) {
294
+ const temporary = `${path}.take.${process.pid}.${randomUUID()}`;
295
+ let linked = false;
296
+ try {
297
+ writeFileSync(temporary, text, { flag: 'wx' });
298
+ try { linkSync(temporary, path); linked = true; }
299
+ catch (error) {
300
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') sweepTakeFiles(path);
301
+ if (!LINK_UNSUPPORTED.has((error as NodeJS.ErrnoException).code ?? '')) throw error;
302
+ }
303
+ } finally { try { unlinkSync(temporary); } catch { /* not made, or gone */ } }
304
+ if (linked) {
305
+ let fd: number | null = null;
306
+ try { fd = openSync(path, 'r+'); } catch { fd = null; /* held; its record is then not rewritten */ }
307
+ return { fd, bytes };
308
+ }
309
+ }
310
+ const fd = openSync(path, 'wx');
311
+ try { writeFileSync(fd, text); }
312
+ catch (error) {
313
+ lockRecordLine('publish-failed', path, { fd, operation }, error);
314
+ try { closeSync(fd); } catch { /* not held */ }
315
+ let held = '';
316
+ try { held = readFileSync(path, 'utf8'); } catch { held = 'gone'; }
317
+ if (held === '' || held.includes(tokenNeedle(token))) { try { unlinkSync(path); } catch { /* gone */ } }
318
+ throw Object.assign(new Error(`${path}: its record could not be written`), { code: RECORD_NOT_WRITTEN, cause: error });
319
+ }
320
+ if (readLockHolder(path)?.token !== token) {
321
+ lockRecordLine('take-reclaimed-before-record', path, { fd, operation });
322
+ try { closeSync(fd); } catch { /* not held */ }
323
+ throw Object.assign(new Error(`${path}: reclaimed before its record`), { code: RECLAIMED_BEFORE_RECORD });
324
+ }
325
+ return { fd, bytes };
326
+ }
110
327
 
111
- function readLockHolder(lockPath: string): LockHolder | null {
328
+ /**
329
+ * A take's private file a process left at a crash between its link and its removal (`<lock>.take.<pid>.<uuid>`):
330
+ * removed where older than the grace, once a process per lock, when its take meets the lock held. It holds nothing.
331
+ */
332
+ const sweptTakes = new Set<string>();
333
+ function sweepTakeFiles(path: string): void {
334
+ if (sweptTakes.has(path)) return;
335
+ sweptTakes.add(path);
336
+ const prefix = `${basename(path)}.take.`;
112
337
  try {
113
- return JSON.parse(readFileSync(lockPath, 'utf8')) as LockHolder;
114
- } catch {
338
+ for (const name of readdirSync(dirname(path))) {
339
+ if (!name.startsWith(prefix)) continue;
340
+ const at = join(dirname(path), name);
341
+ try { if (Date.now() - statSync(at).mtimeMs > EMPTY_RECORD_GRACE_MS) unlinkSync(at); } catch { /* gone, or another's to remove */ }
342
+ }
343
+ } catch { /* an instrument of tidiness: nothing waits on it */ }
344
+ }
345
+
346
+ /** A file createRecorded made, let go: its descriptor closed, and the path removed only while it holds the token. */
347
+ function releaseRecorded(path: string, token: string, fd: number | null): void {
348
+ if (fd !== null) { try { closeSync(fd); } catch { /* released below */ } }
349
+ try {
350
+ const released = fd === null ? fileSteps()?.changeHolding(path, tokenNeedle(token), undefined) : undefined;
351
+ if (released?.changed) return;
352
+ if (released === undefined && readLockHolder(path)?.token === token) unlinkSync(path);
353
+ } catch (error) { lockRecordLine('release-failed', path, {}, error); }
354
+ }
355
+
356
+ /**
357
+ * A leftover reclaim record (`.recovery`) no reclaimer is behind (S2 R13 review): empty, or recorded by a process of
358
+ * this host that is not alive, and older than EMPTY_RECORD_GRACE_MS. It held every waiter of its lock until each
359
+ * timed out, as an empty lock file did.
360
+ */
361
+ function recoveryIsStale(recoveryPath: string): boolean {
362
+ let file: Stats;
363
+ try { file = statSync(recoveryPath); } catch { return false; }
364
+ if (Date.now() - file.mtimeMs <= EMPTY_RECORD_GRACE_MS) return false;
365
+ if (file.size === 0) return true;
366
+ const holder = readLockHolder(recoveryPath);
367
+ return holder !== null && holder.hostname === ownHostname() && Number.isInteger(holder.pid) && !pidAlive(holder.pid);
368
+ }
369
+
370
+ /**
371
+ * A WORLD'S CARRIED LOCKS, REMOVED AT ITS OPEN (S2 K4). A World booted over its kept state (`up --keep-state`, a
372
+ * tab's restored archive) carries whatever lock files its last life left in its services' state directories. One whose
373
+ * holder record cannot be read is no holder's once its taker has gone, and a restore gives it the restore's file time,
374
+ * so the empty-record grace and the 60 s age ran from the restore: every waiter waited them, or gave up at 10 s (Twenty
375
+ * e946aa4c run 2: anthropic's projection.lock, "holder record unreadable", 10,086 ms, then its credentials 500 and the
376
+ * server 10 s late). A carried `.recovery` that is not empty and does not parse was never ended by R13's rule at all.
377
+ *
378
+ * A file is carried only when all of these hold: it is a world-core lock or its reclaim record (`*.lock`,
379
+ * `*.lock.recovery`) in a service's state directory (`<root>/<service>/<stateDir>/world`, never a yarn.lock or any
380
+ * project file); its text is empty or a holder record's beginning (`{"pid"`) that does not parse; and its file time is
381
+ * before `before`, the start of this boot's claim. A restore sets the time before the claim; a taker that is live now
382
+ * (a CLI's changeset rebase, a branch World keeping this one as its parent's storage, a two-call taker between its
383
+ * create and its record) made its file after it. Each removal is still a reclaimer's: a lock's under its `.recovery`,
384
+ * taken with its record (createRecorded), a held `.recovery` skipped; the file read again, renamed aside, the aside copy
385
+ * read again and linked back where it now holds a record (EEXIST there is the path taken since, left to its holder).
386
+ * A record that parses is kept, under its rules (a dead holder's, our own pid's, the 60 s age, which a restore resets:
387
+ * not this rule's). Answers the paths removed.
388
+ */
389
+ export function removeCarriedLocks(roots: readonly string[], options: { stateDir: string; before: number }): string[] {
390
+ const removed: string[] = [];
391
+ const isHolder = (text: string): boolean => Number.isInteger(lockHolderOf('', text)?.pid);
392
+ const carriedText = (text: string): boolean => {
393
+ const trimmed = text.trim();
394
+ return trimmed === '' || (trimmed.startsWith('{"pid"') && !isHolder(trimmed));
395
+ };
396
+ /** The file's text where it is carried now (small, older than the claim, unreadable as a record); else undefined. */
397
+ const carriedNow = (path: string): string | undefined => {
398
+ try {
399
+ const file = statSync(path);
400
+ if (file.size > 1024 || !(file.mtimeMs < options.before)) return undefined;
401
+ const text = readFileSync(path, 'utf8');
402
+ return carriedText(text) ? text : undefined;
403
+ } catch { return undefined; }
404
+ };
405
+ /** Renamed aside, read again, put back where a taker recorded itself meanwhile; true when it is gone. */
406
+ const setAside = (path: string, text: string): boolean => {
407
+ const aside = `${path}.carried.${process.pid}.${randomUUID()}`;
408
+ try { renameSync(path, aside); }
409
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') lockRecordLine('carried-rename-failed', path, {}, error); return false; }
410
+ let after: string | undefined;
411
+ try { after = readFileSync(aside, 'utf8'); } catch { after = undefined; }
412
+ if (after !== undefined && isHolder(after)) {
413
+ try { linkSync(aside, path); lockRecordLine('carried-restored', path, {}); }
414
+ catch (error) { lockRecordLine((error as NodeJS.ErrnoException).code === 'EEXIST' ? 'carried-retaken' : 'carried-restore-failed', path, {}, error); }
415
+ try { unlinkSync(aside); } catch (error) { lockRecordLine('carried-unlink-failed', aside, {}, error); }
416
+ return false;
417
+ }
418
+ storageLockLine(`[world] carried storage lock renamed aside at open path=${path} bytes=${Buffer.byteLength(text)}`);
419
+ try { unlinkSync(aside); } catch (error) { lockRecordLine('carried-unlink-failed', aside, {}, error); }
420
+ return true;
421
+ };
422
+ const removeLock = (path: string): void => {
423
+ const recoveryPath = `${path}.recovery`, recoveryToken = randomUUID();
424
+ let recovery: { fd: number | null };
425
+ try { recovery = createRecorded(recoveryPath, `${JSON.stringify({ pid: process.pid, hostname: ownHostname(), at: new Date().toISOString(), token: recoveryToken })}\n`, recoveryToken, 'carried lock removal'); }
426
+ catch { return; } // a reclaimer is at work, or the recovery is carried too and goes first (visit)
427
+ try {
428
+ const text = carriedNow(path);
429
+ if (text !== undefined && setAside(path, text)) removed.push(path);
430
+ } finally { releaseRecorded(recoveryPath, recoveryToken, recovery.fd); }
431
+ };
432
+ const visit = (directory: string, depth: number): void => {
433
+ let entries: Dirent[];
434
+ try { entries = readdirSync(directory, { withFileTypes: true }); } catch { return; }
435
+ // A directory's carried reclaim records go first: a lock's removal is made under its `.recovery`.
436
+ const files = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
437
+ for (const name of files.filter((name) => name.endsWith('.lock.recovery'))) {
438
+ const path = join(directory, name), text = carriedNow(path);
439
+ if (text !== undefined && setAside(path, text)) removed.push(path);
440
+ }
441
+ for (const name of files.filter((name) => name.endsWith('.lock'))) {
442
+ const path = join(directory, name);
443
+ if (carriedNow(path) !== undefined) removeLock(path);
444
+ }
445
+ for (const entry of entries) {
446
+ if (entry.isDirectory() && depth < 8 && entry.name !== 'node_modules' && entry.name !== '.git') visit(join(directory, entry.name), depth + 1);
447
+ }
448
+ };
449
+ // Only the services' state directories (`<root>/<service>/<stateDir>/world`, and `<root>/<stateDir>/world`): never a
450
+ // service's own data beside them (a PostgreSQL cluster, a project tree).
451
+ for (const root of roots) {
452
+ const states = [join(root, options.stateDir, 'world')];
453
+ try { for (const entry of readdirSync(root, { withFileTypes: true })) if (entry.isDirectory()) states.push(join(root, entry.name, options.stateDir, 'world')); }
454
+ catch { continue; }
455
+ for (const state of states) if (existsSync(state)) visit(state, 0);
456
+ }
457
+ return removed;
458
+ }
459
+
460
+ /** This host's name. In a tab (where the kernel's file steps are offered) it is read once per process: every take
461
+ * records it, and each read was a request of the kernel (its UTS); a tab's process renamed while it runs keeps
462
+ * recording, and comparing, the name it started with. On a host it is read live, as before: there a rename
463
+ * (name.local -> name-2.local on macOS) must reach lockIsStale's comparison, or a dead holder recorded under the new
464
+ * name is never reclaimed, and the read costs no request. */
465
+ let hostnameRead: string | undefined;
466
+ const ownHostname = (): string => (fileSteps() ? (hostnameRead ??= hostname()) : hostname());
467
+
468
+ function recordLockReading(reading: LockReading): void {
469
+ try {
470
+ const text = `${JSON.stringify(reading.holder)}\n`;
471
+ // a take with no descriptor rewrites its record by path, and only while the file still holds its token: a file a
472
+ // stale sweep reclaimed and another made is never written over (the descriptor wrote the reclaimed file's own)
473
+ if (reading.fd === null) { fileSteps()?.changeHolding(reading.path, tokenNeedle(reading.holder.token!), text); return; }
474
+ // Padded with spaces to the record it replaces, which JSON allows after a value: a write that ends early leaves the
475
+ // new record's text over the old one's, never a shorter file with the old one's tail (malformed, and a malformed
476
+ // record keeps the 60 s rule, which a coordinator's live-owner lock never reaches).
477
+ const bytes = Buffer.byteLength(text), padded = reading.recordBytes !== undefined && bytes < reading.recordBytes
478
+ ? `${text.slice(0, -1)}${' '.repeat(reading.recordBytes - bytes)}\n` : text;
479
+ writeSync(reading.fd, padded, 0, 'utf8');
480
+ ftruncateSync(reading.fd, Buffer.byteLength(padded));
481
+ reading.recordBytes = Buffer.byteLength(padded);
482
+ } catch (error) {
483
+ lockRecordLine('rewrite-failed', reading.path, { fd: reading.fd, operation: reading.holder.operation }, error);
484
+ }
485
+ }
486
+
487
+ function storageLockLine(line: string): void {
488
+ try { console.warn(`${line.replace(/[\r\n]/g, ' ')} at=${Date.now()}`); } catch { /* an instrument */ }
489
+ }
490
+
491
+ /** Failure-only ownership evidence, on the same channel as the existing hold and reclaim warnings. */
492
+ function lockRecordLine(stage: string, path: string, fields: Record<string, unknown>, error?: unknown): void {
493
+ try {
494
+ const cause = error as { name?: unknown; code?: unknown } | null | undefined;
495
+ console.warn('[world-storage-lock]', JSON.stringify({
496
+ at: Date.now(), stage, path, pid: process.pid, hostname: ownHostname(), ...fields,
497
+ ...(typeof cause?.name === 'string' ? { error: cause.name } : {}),
498
+ ...(typeof cause?.code === 'string' ? { code: cause.code } : {}),
499
+ }));
500
+ } catch { /* an instrument must not change the lock's result */ }
501
+ }
502
+
503
+ /** Name a bookkeeping phase within an existing acquisition; never acquire another lock. */
504
+ export function withStorageLockOperation<T>(path: string, operation: string, fn: () => T): T {
505
+ let reading: LockReading | undefined;
506
+ for (let i = lockReadings.length - 1; i >= 0; i -= 1) {
507
+ if (lockReadings[i].path === path) { reading = lockReadings[i]; break; }
508
+ }
509
+ if (!reading) return fn();
510
+ const previous = reading.holder.operation;
511
+ reading.holder.operation = operation;
512
+ recordLockReading(reading);
513
+ try { return fn(); }
514
+ finally { reading.holder.operation = previous; recordLockReading(reading); }
515
+ }
516
+
517
+ /** A path that is not there: no such entry, or a part of it that is not a directory. */
518
+ const absent = (error: unknown): boolean => { const code = (error as NodeJS.ErrnoException | null)?.code; return code === 'ENOENT' || code === 'ENOTDIR'; };
519
+
520
+ function readLockHolder(lockPath: string, diagnostic?: 'timeout' | 'release'): LockHolder | null {
521
+ let text: string;
522
+ try {
523
+ text = readFileSync(lockPath, 'utf8');
524
+ } catch (error) {
525
+ if (diagnostic) lockRecordLine('read-failed', lockPath, { during: diagnostic }, error);
526
+ return null;
527
+ }
528
+ return lockHolderOf(lockPath, text, diagnostic);
529
+ }
530
+
531
+ /** A record's text as its holder (readLockHolder's parse); `null` text is no file, read as readLockHolder reads one. */
532
+ function lockHolderOf(lockPath: string, text: string | null, diagnostic?: 'timeout' | 'release'): LockHolder | null {
533
+ if (text === null) {
534
+ if (diagnostic) lockRecordLine('read-failed', lockPath, { during: diagnostic, code: 'ENOENT' });
535
+ return null;
536
+ }
537
+ try {
538
+ return JSON.parse(text) as LockHolder;
539
+ } catch (error) {
540
+ if (diagnostic) lockRecordLine('parse-failed', lockPath, { during: diagnostic, bytes: Buffer.byteLength(text) }, error);
115
541
  return null; // missing, empty (mid-write), or malformed
116
542
  }
117
543
  }
118
544
 
545
+ /** The same ownership account reaches a timeout's client as the storage trace. */
546
+ function lockHolderMessage(holder: LockHolder | null): string {
547
+ if (!holder) return ' (holder record unreadable)';
548
+ const since = typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
549
+ const wait = holder.waitingFor;
550
+ return ` (held by pid ${holder.pid} on ${holder.hostname}${Number.isFinite(since) ? ` for ${Date.now() - since}ms` : ''}; token=${holder.token ?? '?'} operation=${holder.operation ?? 'unknown'} caller=${holder.caller ?? 'unknown'}${wait ? ` waitingFor=${wait.path} waitingOperation=${wait.operation} waitingSince=${wait.since}` : ''})`;
551
+ }
552
+
119
553
  function pidAlive(pid: number): boolean {
120
554
  try {
121
555
  process.kill(pid, 0);
@@ -126,16 +560,28 @@ function pidAlive(pid: number): boolean {
126
560
  }
127
561
  }
128
562
 
129
- /** A lock is stale when its holder is provably gone (same host + dead pid) or it has
130
- * outlived LOCK_STALE_MS. The age falls back to the lockfile's own mtime when the
131
- * `at` record is unreadable — so a writer that crashed between creating the lock and
132
- * recording itself is still eventually reclaimed, while a freshly-created (recent
133
- * mtime) empty lock is left alone, avoiding a race with the live writer. */
563
+ /** A lock is stale when its holder is provably gone (same host + dead pid), when its file is empty past
564
+ * EMPTY_RECORD_GRACE_MS (a taker that ended before recording itself; a live holder never holds an empty record), or
565
+ * when it has outlived LOCK_STALE_MS. The age falls back to the lockfile's own mtime when the `at` record is
566
+ * unreadable, while a freshly-created (recent mtime) empty lock is left alone, avoiding a race with the live writer. */
134
567
  function lockIsStale(lockPath: string, retainLiveOwner = false): boolean {
135
568
  const holder = readLockHolder(lockPath);
136
- if (holder && holder.hostname === hostname() && Number.isInteger(holder.pid) && !pidAlive(holder.pid)) {
569
+ if (holder && holder.hostname === ownHostname() && Number.isInteger(holder.pid) && !pidAlive(holder.pid)) {
570
+ return true;
571
+ }
572
+ // A lock recorded under THIS process that this process does not hold now is no holder's: a release that could not
573
+ // remove its file, or an earlier process numbered as this one (a tab numbers its processes the same at each boot,
574
+ // and a World's state outlives them). Waiting on it was waiting on ourselves: Twenty 46's Redis twin, pid 10013,
575
+ // timed out on `actions.jsonl.lock` "held by pid 10013" and exited.
576
+ if (holder && holder.hostname === ownHostname() && holder.pid === process.pid && ownLocks.get(lockPath) !== holder.token) {
137
577
  return true;
138
578
  }
579
+ // An empty record past its grace names no holder, alive or not (EMPTY_RECORD_GRACE_MS): asked before the live
580
+ // owner's rule, which keeps a lock whose record says who holds it, and an empty one says no one.
581
+ if (!holder) {
582
+ try { const file = statSync(lockPath); if (file.size === 0 && Date.now() - file.mtimeMs > EMPTY_RECORD_GRACE_MS) return true; }
583
+ catch { return false; }
584
+ }
139
585
  if (retainLiveOwner) return false;
140
586
  const recordedAt = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
141
587
  let stamp = recordedAt;
@@ -160,14 +606,104 @@ function sleepSync(ms: number): void {
160
606
  * file lock — so the existing twins and the conformance suite are unaffected.
161
607
  */
162
608
  export class FsWorldStore implements WorldStore {
609
+ // ONE CALL OF THE FILE SYSTEM FOR ONE QUESTION. Each of these asked whether the path exists and then asked again
610
+ // for what it wanted; `stat` asked three times. A process on a host pays microseconds for that. A twin in a tab
611
+ // pays a kernel exchange for each, and a tree's stamp is a handful of these on every operation: Twenty's Redis twin
612
+ // spent 54 s of 136 in `existsSync` and `statSync` under `treeStamp`, and its server's requests waited on it.
163
613
  read(path: string): string | null {
164
- if (!existsSync(path)) return null;
165
- return readFileSync(path, 'utf8');
614
+ asked('read');
615
+ try { return readFileSync(path, 'utf8'); }
616
+ catch (error) { if (absent(error)) return null; throw error; }
617
+ }
618
+
619
+ /**
620
+ * A small file's text, kept while its own facts stand. THE GUARANTEE: the key is the path's own lstat (device,
621
+ * inode, size, modification and change times); this process's own writes through the store drop what is kept at
622
+ * once; another process's write is seen when any of those facts moves. That is the guarantee a tree's stamp
623
+ * already rests on for its logs (their size and modification time), with the inode and change time added: a
624
+ * replaced file (writeAtomic) is another inode, a file written in place has another time. What it cannot see is a
625
+ * rewrite to the same size on the same or a reused inode within one tick of the file system's clock, with no check
626
+ * between; the logs' stamp has the same limit. A link is read through, and not kept.
627
+ * A text read after a write that came between the lstat and the read is kept under the OLDER key, so the next
628
+ * check finds the facts moved and reads again: what is kept is never newer than its key, and never served past it.
629
+ */
630
+ private readonly kept = new Map<string, { key: string; text: string }>();
631
+ readKept(path: string): string | null {
632
+ asked('readKept');
633
+ // A missing file is asked for without an error being made of it: a World with no parent has no branch.json, and
634
+ // every stamp asks.
635
+ let own: Stats | undefined;
636
+ try { own = lstatSync(path, { throwIfNoEntry: false }); }
637
+ catch (error) { if (!absent(error)) throw error; }
638
+ if (!own) { this.kept.delete(path); return null; }
639
+ if (own.isSymbolicLink()) return this.read(path);
640
+ const key = `${own.dev}:${own.ino}:${own.size}:${own.mtimeMs}:${own.ctimeMs}`;
641
+ const held = this.kept.get(path);
642
+ if (held && held.key === key) return held.text;
643
+ const text = this.read(path);
644
+ if (text === null) { this.kept.delete(path); return null; }
645
+ this.kept.set(path, { key, text });
646
+ return text;
647
+ }
648
+
649
+ /**
650
+ * A TREE'S GENERATION: one file beside a tree's three files, a byte longer after every write of any of them, so a
651
+ * reader that has the tree's stamp asks ONE question to know it still stands (log.ts treeFacts), where it asked
652
+ * three, two of them of files a World with no parent does not have.
653
+ *
654
+ * WHAT MAKES IT SOUND. A writer appends a byte BEFORE its write and another AFTER it; a reader takes the
655
+ * generation's facts BEFORE it reads the files' own. The file only grows, so its size never repeats on one inode,
656
+ * and a replaced or removed file is another inode or none. So for any write W and any reader that kept a stamp S
657
+ * under generation G: if W's first byte is after G was read, the kept G is behind and S is not served; if it is
658
+ * before, then either W itself was before S's own reads (S holds it), or W's second byte is after G was read, and
659
+ * again G is behind. An append is one call the system orders among all writers: each byte is one append of its
660
+ * own, whole or not at all, on a POSIX filesystem and in a tab's one kernel (browser-substrate
661
+ * packages/node/src/kernel-virtual-filesystem.ts appendFileSync, an O_APPEND open and writeAll, which sends a write
662
+ * of at most DATA_BYTES as one SYS_write request the kernel serves whole; or, by a file step, its table's own
663
+ * O_APPEND open and write of the one byte, filesystem.ts appendFileBytes). The proof rests on that; log.ts
664
+ * appendToBranchLog's (`generationAfterOwnWrite`: one write is exactly two bytes) as well.
665
+ *
666
+ * WHERE THAT STOPS. (1) A writer that dies between its write and its second byte, with a reader's whole check
667
+ * between its first byte and its write: that reader keeps the older stamp until the next write of the tree. (2) A
668
+ * writer of these files that does not come through a store of this version (an older package, a copy made over a
669
+ * live directory by another program): the generation does not move. A directory made or removed whole has no
670
+ * generation or another's, which serves nothing kept. (3) A stamp that names files outside its own directory (a
671
+ * World with a parent: its view and its ancestors' logs) is never kept under a generation (log.ts).
672
+ */
673
+ generation(dirPath: string): string | null {
674
+ asked('generation');
675
+ let own: Stats | undefined;
676
+ try { own = lstatSync(join(dirPath, TREE_GENERATION), { throwIfNoEntry: false }); }
677
+ catch (error) { if (!absent(error)) throw error; }
678
+ if (!own || !own.isFile()) return null;
679
+ return `${own.dev}:${own.ino}:${own.size}:${own.mtimeMs}:${own.ctimeMs}`;
680
+ }
681
+
682
+ /** One write is `moved` twice, a byte each: the same file, those two bytes longer. */
683
+ generationAfterOwnWrite(dirPath: string, before: string): string | null {
684
+ const now = this.generation(dirPath);
685
+ if (now === null) return null;
686
+ const [dev, ino, size] = before.split(':'), [nowDev, nowIno, nowSize] = now.split(':');
687
+ return nowDev === dev && nowIno === ino && Number(nowSize) === Number(size) + 2 ? now : null;
688
+ }
689
+
690
+ /** One more byte on the generation beside `path`, where `path` is one of a tree's files. A directory that is not
691
+ * there has no reader to tell: the write that makes it is followed by the byte that makes its generation. */
692
+ private moved(path: string): void {
693
+ if (!TREE_FILE_NAMES.has(basename(path))) return;
694
+ try { appendFileSync(join(dirname(path), TREE_GENERATION), '.'); }
695
+ catch (error) { if (!absent(error)) throw error; }
696
+ }
697
+
698
+ /** A write of `path`, with the generation beside it moved before and after where it is one of a tree's files. */
699
+ private writing(path: string, write: () => void): void {
700
+ this.moved(path);
701
+ try { write(); } finally { this.moved(path); }
166
702
  }
167
703
 
168
704
  readLines(path: string): string[] {
169
- if (!existsSync(path)) return [];
170
- return readFileSync(path, 'utf8').split('\n');
705
+ try { return readFileSync(path, 'utf8').split('\n'); }
706
+ catch (error) { if (absent(error)) return []; throw error; }
171
707
  }
172
708
 
173
709
  readRange(path: string, start: number): string | null {
@@ -184,50 +720,66 @@ export class FsWorldStore implements WorldStore {
184
720
  }
185
721
 
186
722
  append(path: string, data: string): void {
723
+ asked('append');
187
724
  refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
725
+ this.kept.delete(path);
726
+ // A tree's file in a tab: the generation's byte, the append and the byte again as one request of the kernel
727
+ // (fileSteps), by its own opens, writes and closes in this order. Not where every append is to be fsynced.
728
+ if (TREE_FILE_NAMES.has(basename(path)) && process.env.VOLTER_DURABLE !== '1'
729
+ && fileSteps()?.appendBetween(join(dirname(path), TREE_GENERATION), '.', path, data)) return;
188
730
  // Historical appendDurable: a completed append syscall survives a process crash;
189
731
  // fsync additionally survives kernel-panic/power-loss when VOLTER_DURABLE=1.
190
- const fd = openSync(path, 'a');
191
- try {
192
- appendFileSync(fd, data);
193
- if (process.env.VOLTER_DURABLE === '1') fsyncSync(fd);
194
- } finally {
195
- closeSync(fd);
196
- }
732
+ this.writing(path, () => {
733
+ const fd = openSync(path, 'a');
734
+ try {
735
+ appendFileSync(fd, data);
736
+ if (process.env.VOLTER_DURABLE === '1') fsyncSync(fd);
737
+ } finally {
738
+ closeSync(fd);
739
+ }
740
+ });
197
741
  }
198
742
 
199
743
  write(path: string, data: string, options: { secret?: boolean } = {}): void {
200
744
  refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
201
- if (!options.secret) {
202
- mkdirSync(dirname(path), { recursive: true });
203
- writeFileSync(path, data);
204
- return;
205
- }
206
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
207
- writeFileSync(path, data, { mode: 0o600 });
208
- chmodSync(path, 0o600);
745
+ this.kept.delete(path);
746
+ this.writing(path, () => {
747
+ if (!options.secret) {
748
+ mkdirSync(dirname(path), { recursive: true });
749
+ writeFileSync(path, data);
750
+ return;
751
+ }
752
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
753
+ writeFileSync(path, data, { mode: 0o600 });
754
+ chmodSync(path, 0o600);
755
+ });
209
756
  }
210
757
 
211
758
  writeAtomic(path: string, data: string, options: { secret?: boolean } = {}): void {
212
759
  refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
760
+ this.kept.delete(path);
213
761
  mkdirSync(dirname(path), { recursive: true });
214
762
  const tmp = `${path}.${process.pid}.${Date.now()}.${fsAtomicSequence++}.tmp`;
215
- try {
216
- writeFileSync(tmp, data, options.secret ? { mode: 0o600 } : undefined);
217
- renameSync(tmp, path);
218
- if (options.secret) chmodSync(path, 0o600);
219
- } finally {
220
- rmSync(tmp, { force: true });
221
- }
763
+ this.writing(path, () => {
764
+ try {
765
+ writeFileSync(tmp, data, options.secret ? { mode: 0o600 } : undefined);
766
+ renameSync(tmp, path);
767
+ if (options.secret) chmodSync(path, 0o600);
768
+ } finally {
769
+ rmSync(tmp, { force: true });
770
+ }
771
+ });
222
772
  }
223
773
 
224
774
  exists(path: string): boolean {
775
+ asked('exists');
225
776
  return existsSync(path);
226
777
  }
227
778
 
228
779
  remove(path: string): void {
229
780
  refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
230
- rmSync(path, { recursive: true, force: true });
781
+ for (const held of this.kept.keys()) if (held === path || held.startsWith(`${path}/`)) this.kept.delete(held);
782
+ this.writing(path, () => rmSync(path, { recursive: true, force: true }));
231
783
  }
232
784
 
233
785
  mkdir(dirPath: string): void {
@@ -235,13 +787,24 @@ export class FsWorldStore implements WorldStore {
235
787
  }
236
788
 
237
789
  list(dirPath: string): string[] {
238
- if (!existsSync(dirPath)) return [];
239
- return readdirSync(dirPath);
790
+ asked('list');
791
+ try { return readdirSync(dirPath); }
792
+ catch (error) {
793
+ // No such directory is an empty one, as before; a path that IS there and is not a directory still fails.
794
+ const code = (error as NodeJS.ErrnoException).code;
795
+ if (code === 'ENOENT' || (code === 'ENOTDIR' && !existsSync(dirPath))) return [];
796
+ throw error;
797
+ }
240
798
  }
241
799
 
800
+ /** The real path of each path already resolved: a state directory's does not move under a running process, and
801
+ * every history lock asked for it again. A path that does not exist yet is not kept. */
802
+ private readonly canonical = new Map<string, string>();
242
803
  canonicalPath(path: string): string {
243
804
  const absolute = resolve(path);
244
- try { return realpathSync(absolute); }
805
+ const known = this.canonical.get(absolute);
806
+ if (known !== undefined) return known;
807
+ try { const real = realpathSync(absolute); this.canonical.set(absolute, real); return real; }
245
808
  catch (error) {
246
809
  if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || dirname(absolute) === absolute) throw error;
247
810
  return join(this.canonicalPath(dirname(absolute)), basename(absolute));
@@ -249,80 +812,194 @@ export class FsWorldStore implements WorldStore {
249
812
  }
250
813
 
251
814
  stat(path: string): WorldStat | null {
252
- if (!existsSync(path)) return null;
253
- const s = statSync(path);
254
- return { size: s.size, mtimeMs: s.mtimeMs, isDirectory: s.isDirectory(), ...(lstatSync(path).isSymbolicLink() ? { isSymbolicLink: true } : {}) };
815
+ asked('stat');
816
+ // The path itself first: where it is no link, that is the whole answer. A link is followed by a second call,
817
+ // and one that leads nowhere is absent, as it was.
818
+ // Asked without an error being made of a missing path (a tree's parent log is absent in a World with no parent).
819
+ let own: Stats | undefined;
820
+ try { own = lstatSync(path, { throwIfNoEntry: false }); }
821
+ catch (error) { if (!absent(error)) throw error; }
822
+ if (!own) return null;
823
+ if (!own.isSymbolicLink()) return { size: own.size, mtimeMs: own.mtimeMs, isDirectory: own.isDirectory() };
824
+ try { const s = statSync(path); return { size: s.size, mtimeMs: s.mtimeMs, isDirectory: s.isDirectory(), isSymbolicLink: true }; }
825
+ catch (error) { if (absent(error) || (error as NodeJS.ErrnoException).code === 'ELOOP') return null; throw error; }
255
826
  }
256
827
 
257
828
  /** Run `fn` holding an exclusive cross-process file lock. The lockfile records
258
- * `{pid, hostname, at}`; a contender that finds a stale lock (dead pid or age >
829
+ * identity, operation, caller and any nested wait; a contender that finds a stale lock (dead pid or age >
259
830
  * LOCK_STALE_MS) reclaims it by atomically renaming it aside — so a crashed holder
260
831
  * can't wedge the world forever. Reclaim is race-safe: only the process that wins
261
832
  * the rename clears the stale inode, and the exclusive `wx` create still decides the
262
833
  * winner. (Moved verbatim from storage.ts `withFileLock`.) */
263
- withLock<T>(lockPath: string, fn: () => T, options: { retainLiveOwner?: boolean } = {}): T {
264
- mkdirSync(dirname(lockPath), { recursive: true });
834
+ withLock<T>(lockPath: string, fn: () => T, options: WorldLockOptions = {}): T {
265
835
  const started = Date.now();
836
+ const scope = storageLockScope(options);
837
+ fn = countedHold(scope.operation, fn);
266
838
  let fd: number | null = null;
267
- while (fd === null) {
268
- try {
269
- fd = openSync(lockPath, 'wx');
270
- } catch (error) {
271
- const code = (error as NodeJS.ErrnoException).code;
272
- if (code !== 'EEXIST') throw error;
273
- if (lockIsStale(lockPath, options.retainLiveOwner)) {
274
- // Serialize reclaimers, then re-read: a contender's earlier stale observation
275
- // must never rename another contender's newly acquired, live lock.
276
- const recoveryPath = `${lockPath}.recovery`;
277
- let recovery: number;
278
- try { recovery = openSync(recoveryPath, 'wx'); }
279
- catch (error) {
280
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
281
- if (Date.now() - started > 10_000) throw new Error(`Lock recovery is active or interrupted: ${recoveryPath}. Inspect its owner before removing the recovery record.`);
282
- sleepSync(25); continue;
839
+ // A kernel's file step takes the lock with its record in one request and keeps no descriptor (fileSteps).
840
+ const steps = fileSteps();
841
+ // The token is the take's own: its release removes the file only while it still holds this token, so a file a
842
+ // stale-lock sweep reclaimed and another process re-created is never removed by the first holder.
843
+ const token = randomUUID();
844
+ const holderNow = (): LockHolder => ({ pid: process.pid, hostname: ownHostname(), at: new Date().toISOString(), token, operation: scope.operation, caller: scope.caller });
845
+ let record: LockHolder | undefined;
846
+ let taken = false;
847
+ let unrecordedTakes = 0;
848
+ let recordBytes: number | undefined;
849
+ let made = false;
850
+ let reported = false;
851
+ let attempts = 0;
852
+ let sleeps = 0;
853
+ let sleepMs = 0;
854
+ let waitedOn: LockHolder | null = null;
855
+ let nestedWait: { path: string; since: number; holders: LockReading[] } | undefined;
856
+ const finishNestedWait = (): void => {
857
+ if (!nestedWait) return;
858
+ const waitedMs = Date.now() - nestedWait.since;
859
+ for (const reading of nestedWait.holders) {
860
+ const key = `${nestedWait.path}\0${scope.operation}`;
861
+ const previous = reading.nestedWaits.get(key);
862
+ reading.nestedWaits.set(key, { path: nestedWait.path, operation: scope.operation, waitedMs: (previous?.waitedMs ?? 0) + waitedMs });
863
+ delete reading.holder.waitingFor;
864
+ recordLockReading(reading);
865
+ }
866
+ nestedWait = undefined;
867
+ };
868
+ const reportWait = (): void => {
869
+ if (reported || !nestedWait || Date.now() - started < 2_000) return;
870
+ reported = true;
871
+ const holder = readLockHolder(nestedWait.path);
872
+ const since = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
873
+ storageLockLine(`[world] storage lock still held path=${nestedWait.path} holder=${holder?.pid ?? '?'}@${holder?.hostname ?? '?'} token=${holder?.token ?? '?'} operation=${holder?.operation ?? holder?.caller ?? 'unknown:0'} holderMs=${Number.isFinite(since) ? Date.now() - since : '?'} waiter=${process.pid} waitingOperation=${scope.operation} caller=${scope.caller}`);
874
+ };
875
+ const slept = (path = lockPath): void => {
876
+ if (sleeps === 0) waitedOn = readLockHolder(path);
877
+ if (nestedWait?.path !== path) {
878
+ finishNestedWait();
879
+ nestedWait = { path, since: Date.now(), holders: [...lockReadings] };
880
+ for (const reading of nestedWait.holders) {
881
+ reading.holder.waitingFor = { path, operation: scope.operation, since: nestedWait.since };
882
+ recordLockReading(reading);
883
+ }
884
+ }
885
+ reportWait();
886
+ const sleeping = Date.now();
887
+ sleeps += 1;
888
+ sleepSync(25);
889
+ sleepMs += Date.now() - sleeping;
890
+ };
891
+ try {
892
+ while (!taken) {
893
+ // This runs in the synchronous loop: a blocked holder cannot service a timer.
894
+ reportWait();
895
+ try {
896
+ attempts += 1;
897
+ // The holder is recorded WITH the take (createRecorded): no other process sees this lock without its record.
898
+ record = holderNow();
899
+ const taking = createRecorded(lockPath, `${JSON.stringify(record)}\n`, token, scope.operation);
900
+ fd = taking.fd;
901
+ recordBytes = taking.bytes;
902
+ taken = true;
903
+ } catch (error) {
904
+ const code = (error as NodeJS.ErrnoException).code;
905
+ // The lock's directory is made when it is found missing, once, not before every take: it is there for
906
+ // every take after a state directory's first.
907
+ if (code === 'ENOENT' && !made) { made = true; mkdirSync(dirname(lockPath), { recursive: true }); continue; }
908
+ // A two-call take whose path did not hold its record after it (another reclaimed it between): taken again.
909
+ if (code === RECLAIMED_BEFORE_RECORD) continue;
910
+ // A take whose record could not be written: made again, a few times, before the failure is the caller's.
911
+ if (code === RECORD_NOT_WRITTEN) {
912
+ unrecordedTakes += 1;
913
+ if (unrecordedTakes >= RECORD_ATTEMPTS) throw Object.assign(new Error(`World storage lock ${lockPath}: its holder's record could not be written`), { code: 'EIO', cause: error });
914
+ sleepSync(25 * unrecordedTakes);
915
+ continue;
283
916
  }
284
- try {
285
- writeFileSync(recovery, JSON.stringify({ pid: process.pid, hostname: hostname() }));
286
- if (lockIsStale(lockPath, options.retainLiveOwner)) {
287
- const holder = readLockHolder(lockPath);
288
- const salvage = `${lockPath}.stale.${process.pid}.${Date.now()}`;
289
- try {
290
- renameSync(lockPath, salvage);
291
- console.warn(`[world] reclaiming stale storage lock ${lockPath} (held by pid ${holder?.pid ?? '?'} on ${holder?.hostname ?? '?'})`);
292
- unlinkSync(salvage);
293
- } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
917
+ if (code !== 'EEXIST') throw error;
918
+ if (lockIsStale(lockPath, options.retainLiveOwner)) {
919
+ // Serialize reclaimers, then re-read: a contender's earlier stale observation
920
+ // must never rename another contender's newly acquired, live lock. The reclaim record is made as a lock
921
+ // is, with its record and a token of its own, and released by that token.
922
+ const recoveryPath = `${lockPath}.recovery`;
923
+ const recoveryToken = randomUUID();
924
+ let recovery: { fd: number | null };
925
+ try { recovery = createRecorded(recoveryPath, `${JSON.stringify({ pid: process.pid, hostname: ownHostname(), at: new Date().toISOString(), token: recoveryToken })}\n`, recoveryToken, 'lock recovery'); }
926
+ catch (error) {
927
+ const recoveryCode = (error as NodeJS.ErrnoException).code;
928
+ if (recoveryCode === RECLAIMED_BEFORE_RECORD || recoveryCode === RECORD_NOT_WRITTEN) continue;
929
+ if (recoveryCode !== 'EEXIST') throw error;
930
+ // A reclaim record no reclaimer is behind is reclaimed as an empty lock file is (recoveryIsStale).
931
+ if (recoveryIsStale(recoveryPath)) {
932
+ const aside = `${recoveryPath}.stale.${process.pid}.${randomUUID()}`;
933
+ try { renameSync(recoveryPath, aside); unlinkSync(aside); lockRecordLine('recovery-reclaimed', recoveryPath, {}); }
934
+ catch (cause) { if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') throw cause; }
935
+ continue;
936
+ }
937
+ if (Date.now() - started > 10_000) throw new Error(`Lock recovery is active or interrupted: ${recoveryPath}${lockHolderMessage(readLockHolder(recoveryPath, 'timeout'))}. Inspect its owner before removing the recovery record.`);
938
+ slept(recoveryPath); continue;
294
939
  }
295
- } finally { closeSync(recovery); unlinkSync(recoveryPath); }
296
- continue;
297
- }
298
- if (Date.now() - started > 10_000) {
299
- const holder = readLockHolder(lockPath);
300
- const since = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
301
- throw new Error(`Timed out waiting for world storage lock: ${lockPath}${holder ? ` (held by pid ${holder.pid} on ${holder.hostname}${Number.isFinite(since) ? ` for ${Date.now() - since}ms` : ''})` : ''}`);
940
+ try {
941
+ if (lockIsStale(lockPath, options.retainLiveOwner)) {
942
+ const holder = readLockHolder(lockPath);
943
+ const salvage = `${lockPath}.stale.${process.pid}.${Date.now()}`;
944
+ try {
945
+ renameSync(lockPath, salvage);
946
+ console.warn(`[world] reclaiming stale storage lock ${lockPath} (held by pid ${holder?.pid ?? '?'} on ${holder?.hostname ?? '?'}) (reclaimer pid ${process.pid} on ${ownHostname()})`);
947
+ unlinkSync(salvage);
948
+ } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
949
+ }
950
+ } finally { releaseRecorded(recoveryPath, recoveryToken, recovery.fd); }
951
+ continue;
952
+ }
953
+ if (Date.now() - started > 10_000) {
954
+ const holder = readLockHolder(lockPath, 'timeout');
955
+ throw new Error(`Timed out waiting for world storage lock: ${lockPath}${lockHolderMessage(holder)}`);
956
+ }
957
+ slept();
302
958
  }
303
- sleepSync(25);
959
+ }
960
+ } finally {
961
+ finishNestedWait();
962
+ if (sleeps > 0) {
963
+ const waitedMs = Date.now() - started;
964
+ const first = waitedOn as LockHolder | null;
965
+ storageLockLine(`[world] storage lock wait path=${lockPath} outcome=${taken ? 'acquired' : 'failed'} waiter=${process.pid}@${ownHostname()} operation=${scope.operation} attempts=${attempts} sleeps=${sleeps} requestedSleepMs=${sleeps * 25} sleepMs=${sleepMs} outsideSleepMs=${waitedMs - sleepMs} waitedMs=${waitedMs} firstHolder=${first?.pid ?? '?'}@${first?.hostname ?? '?'} token=${first?.token ?? '?'}`);
304
966
  }
305
967
  }
306
968
 
307
- // Record the holder so a later contender can detect staleness. Best-effort: a write
308
- // failure here doesn't weaken exclusivity, only staleness diagnostics.
309
- try {
310
- writeFileSync(fd, `${JSON.stringify({ pid: process.pid, hostname: hostname(), at: new Date().toISOString() } satisfies LockHolder)}\n`);
311
- } catch { /* ignore */ }
969
+ // The holder's record was written with the take, in the loop above: a take whose record could not be written is
970
+ // never held (EMPTY_RECORD_GRACE_MS).
971
+ ownLocks.set(lockPath, token);
312
972
 
313
973
  const acquired = Date.now();
974
+ const reading: LockReading = { path: lockPath, fd, holder: record!, nestedWaits: new Map(), ...(recordBytes !== undefined ? { recordBytes } : {}) };
975
+ lockReadings.push(reading);
314
976
  try {
315
977
  return fn();
316
978
  } finally {
317
979
  // Every World on the machine waits on some of these locks; a long hold stalls them all.
318
980
  const heldMs = Date.now() - acquired;
981
+ if (heldMs > 2_000) storageLockLine(`[world] releasing storage lock path=${lockPath} operation=${scope.operation} heldMs=${heldMs} nestedWaits=${JSON.stringify([...reading.nestedWaits.values()])}`);
319
982
  if (heldMs > 2_000) console.warn(`[world] held storage lock ${lockPath} for ${heldMs}ms (pid ${process.pid})\n${new Error().stack?.split('\n').slice(2, 8).join('\n') ?? ''}`);
320
- const held = fstatSync(fd);
321
- closeSync(fd);
983
+ lockReadings.pop();
984
+ ownLocks.delete(lockPath);
985
+ if (fd !== null) {
986
+ try { closeSync(fd); }
987
+ catch (error) { lockRecordLine('close-failed', lockPath, { fd }, error); throw error; }
988
+ }
989
+ // By the token the take wrote, not by inode: a filesystem may answer a descriptor's stat and the path's with
990
+ // different identities (a tab's, where a committed path is the store's entry), and a lock released by inode
991
+ // was then never removed, left recorded as this live process's (above). A kernel's file step reads the record,
992
+ // compares its token and unlinks in one request; its answer is what the read below would have found.
322
993
  try {
323
- const current = lstatSync(lockPath);
324
- if (held.dev === current.dev && held.ino === current.ino) unlinkSync(lockPath);
325
- } catch { /* already reclaimed by a stale-lock sweep */ }
994
+ const released = fd === null ? steps?.changeHolding(lockPath, tokenNeedle(token), undefined) : undefined;
995
+ if (!released?.changed) {
996
+ const observed = released === undefined ? readLockHolder(lockPath, 'release') : lockHolderOf(lockPath, released.observed, 'release');
997
+ if (observed?.token === token) unlinkSync(lockPath);
998
+ else if (observed) lockRecordLine('release-token-mismatch', lockPath, {
999
+ token, recordedToken: observed.token, recordedPid: observed.pid, recordedHostname: observed.hostname,
1000
+ });
1001
+ }
1002
+ } catch (error) { lockRecordLine('release-failed', lockPath, { fd }, error); }
326
1003
  }
327
1004
  }
328
1005
  }