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