@volter/world-core 2.0.0

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 (180) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/app-route.cjs +154 -0
  4. package/app-route.d.cts +7 -0
  5. package/attach.cjs +80 -0
  6. package/dist/app-route.cjs +154 -0
  7. package/dist/app-route.d.cts +7 -0
  8. package/dist/attach.cjs +80 -0
  9. package/dist/generated/pack-facts.json +4306 -0
  10. package/dist/inject.cjs +1097 -0
  11. package/dist/network-policy.cjs +92 -0
  12. package/dist/network-policy.d.cts +10 -0
  13. package/dist/src/actions.d.ts +276 -0
  14. package/dist/src/actions.js +436 -0
  15. package/dist/src/ancestry.d.ts +22 -0
  16. package/dist/src/ancestry.js +238 -0
  17. package/dist/src/args.d.ts +3 -0
  18. package/dist/src/args.js +12 -0
  19. package/dist/src/blob-store.d.ts +55 -0
  20. package/dist/src/blob-store.js +186 -0
  21. package/dist/src/brand-tokens.d.ts +2 -0
  22. package/dist/src/brand-tokens.js +17 -0
  23. package/dist/src/changeset.d.ts +431 -0
  24. package/dist/src/changeset.js +0 -0
  25. package/dist/src/client-bundle.d.ts +1 -0
  26. package/dist/src/client-bundle.js +28 -0
  27. package/dist/src/credential.d.ts +38 -0
  28. package/dist/src/credential.js +114 -0
  29. package/dist/src/derived-core.d.ts +452 -0
  30. package/dist/src/derived-core.js +782 -0
  31. package/dist/src/derived.d.ts +84 -0
  32. package/dist/src/derived.js +122 -0
  33. package/dist/src/emit.d.ts +106 -0
  34. package/dist/src/emit.js +157 -0
  35. package/dist/src/executor.d.ts +120 -0
  36. package/dist/src/executor.js +387 -0
  37. package/dist/src/file-response.d.ts +3 -0
  38. package/dist/src/file-response.js +22 -0
  39. package/dist/src/fork.d.ts +26 -0
  40. package/dist/src/fork.js +68 -0
  41. package/dist/src/git/history.d.ts +36 -0
  42. package/dist/src/git/history.js +298 -0
  43. package/dist/src/git/index.d.ts +6 -0
  44. package/dist/src/git/index.js +6 -0
  45. package/dist/src/git/inflate.d.ts +11 -0
  46. package/dist/src/git/inflate.js +194 -0
  47. package/dist/src/git/objects.d.ts +64 -0
  48. package/dist/src/git/objects.js +161 -0
  49. package/dist/src/git/pack.d.ts +14 -0
  50. package/dist/src/git/pack.js +199 -0
  51. package/dist/src/git/refs.d.ts +19 -0
  52. package/dist/src/git/refs.js +35 -0
  53. package/dist/src/git/smart-http.d.ts +45 -0
  54. package/dist/src/git/smart-http.js +223 -0
  55. package/dist/src/hash.d.ts +38 -0
  56. package/dist/src/hash.js +48 -0
  57. package/dist/src/head.d.ts +140 -0
  58. package/dist/src/head.js +313 -0
  59. package/dist/src/history.d.ts +76 -0
  60. package/dist/src/history.js +322 -0
  61. package/dist/src/index.d.ts +73 -0
  62. package/dist/src/index.js +98 -0
  63. package/dist/src/lifecycle.d.ts +1 -0
  64. package/dist/src/lifecycle.js +8 -0
  65. package/dist/src/log.d.ts +254 -0
  66. package/dist/src/log.js +801 -0
  67. package/dist/src/mirror-shell.d.ts +2 -0
  68. package/dist/src/mirror-shell.js +13 -0
  69. package/dist/src/observe.d.ts +49 -0
  70. package/dist/src/observe.js +148 -0
  71. package/dist/src/pack-assets.d.ts +30 -0
  72. package/dist/src/pack-assets.js +88 -0
  73. package/dist/src/packRegistry.d.ts +374 -0
  74. package/dist/src/packRegistry.js +142 -0
  75. package/dist/src/placeholder-remote.d.ts +22 -0
  76. package/dist/src/placeholder-remote.js +86 -0
  77. package/dist/src/proxy.d.ts +25 -0
  78. package/dist/src/proxy.js +155 -0
  79. package/dist/src/rateBudget.d.ts +367 -0
  80. package/dist/src/rateBudget.js +925 -0
  81. package/dist/src/references.d.ts +18 -0
  82. package/dist/src/references.js +27 -0
  83. package/dist/src/remote-execute.d.ts +22 -0
  84. package/dist/src/remote-execute.js +1 -0
  85. package/dist/src/resource-blob.d.ts +10 -0
  86. package/dist/src/resource-blob.js +56 -0
  87. package/dist/src/scenario.d.ts +197 -0
  88. package/dist/src/scenario.js +425 -0
  89. package/dist/src/schemas.d.ts +78 -0
  90. package/dist/src/schemas.js +50 -0
  91. package/dist/src/serve-http.d.ts +48 -0
  92. package/dist/src/serve-http.js +340 -0
  93. package/dist/src/serve.d.ts +147 -0
  94. package/dist/src/serve.js +507 -0
  95. package/dist/src/shared-blob-index.d.ts +4 -0
  96. package/dist/src/shared-blob-index.js +126 -0
  97. package/dist/src/state-system.d.ts +70 -0
  98. package/dist/src/state-system.js +90 -0
  99. package/dist/src/storage.d.ts +101 -0
  100. package/dist/src/storage.js +337 -0
  101. package/dist/src/twin-fetch.d.ts +64 -0
  102. package/dist/src/twin-fetch.js +91 -0
  103. package/dist/src/types.d.ts +40 -0
  104. package/dist/src/types.js +1 -0
  105. package/dist/src/v1-removed.d.ts +159 -0
  106. package/dist/src/v1-removed.js +124 -0
  107. package/dist/src/volter-home.d.ts +5 -0
  108. package/dist/src/volter-home.js +10 -0
  109. package/dist/src/world-clock.d.ts +4 -0
  110. package/dist/src/world-clock.js +32 -0
  111. package/dist/src/world-env.d.ts +3 -0
  112. package/dist/src/world-env.js +22 -0
  113. package/dist/src/world-store-sql.d.ts +27 -0
  114. package/dist/src/world-store-sql.js +86 -0
  115. package/dist/src/world-store.d.ts +168 -0
  116. package/dist/src/world-store.js +475 -0
  117. package/dist/src/worldConfig.d.ts +9 -0
  118. package/dist/src/worldConfig.js +17 -0
  119. package/dist/stream-bridge.cjs +80 -0
  120. package/dist/vendor-hosts.cjs +200 -0
  121. package/generated/pack-facts.json +4306 -0
  122. package/inject.cjs +1097 -0
  123. package/network-policy.cjs +92 -0
  124. package/network-policy.d.cts +10 -0
  125. package/package.json +103 -0
  126. package/src/actions.ts +564 -0
  127. package/src/ancestry.ts +213 -0
  128. package/src/args.ts +14 -0
  129. package/src/blob-store.ts +185 -0
  130. package/src/brand-tokens.ts +17 -0
  131. package/src/changeset.ts +1032 -0
  132. package/src/client-bundle.ts +29 -0
  133. package/src/credential.ts +140 -0
  134. package/src/derived-core.ts +1004 -0
  135. package/src/derived.ts +176 -0
  136. package/src/emit.ts +242 -0
  137. package/src/executor.ts +431 -0
  138. package/src/file-response.ts +22 -0
  139. package/src/fork.ts +89 -0
  140. package/src/git/history.ts +177 -0
  141. package/src/git/index.ts +6 -0
  142. package/src/git/inflate.ts +125 -0
  143. package/src/git/objects.ts +110 -0
  144. package/src/git/pack.ts +105 -0
  145. package/src/git/refs.ts +25 -0
  146. package/src/git/smart-http.ts +149 -0
  147. package/src/hash.ts +66 -0
  148. package/src/head.ts +318 -0
  149. package/src/history.ts +246 -0
  150. package/src/index.ts +323 -0
  151. package/src/lifecycle.ts +8 -0
  152. package/src/log.ts +793 -0
  153. package/src/mirror-shell.ts +15 -0
  154. package/src/observe.ts +130 -0
  155. package/src/pack-assets.ts +81 -0
  156. package/src/packRegistry.ts +408 -0
  157. package/src/placeholder-remote.ts +81 -0
  158. package/src/proxy.ts +183 -0
  159. package/src/rateBudget.ts +1115 -0
  160. package/src/references.ts +46 -0
  161. package/src/remote-execute.ts +26 -0
  162. package/src/resource-blob.ts +57 -0
  163. package/src/scenario.ts +479 -0
  164. package/src/schemas.ts +56 -0
  165. package/src/serve-http.ts +299 -0
  166. package/src/serve.ts +618 -0
  167. package/src/shared-blob-index.ts +108 -0
  168. package/src/state-system.ts +115 -0
  169. package/src/storage.ts +407 -0
  170. package/src/twin-fetch.ts +147 -0
  171. package/src/types.ts +50 -0
  172. package/src/v1-removed.ts +172 -0
  173. package/src/volter-home.ts +11 -0
  174. package/src/world-clock.ts +33 -0
  175. package/src/world-env.ts +18 -0
  176. package/src/world-store-sql.ts +118 -0
  177. package/src/world-store.ts +572 -0
  178. package/src/worldConfig.ts +27 -0
  179. package/stream-bridge.cjs +80 -0
  180. package/vendor-hosts.cjs +200 -0
@@ -0,0 +1,572 @@
1
+ // WORLD STORE — the transport seam for the control-plane's persistence.
2
+ //
3
+ // The kernel's on-disk surface is a CLOSED SET of synchronous primitives: JSONL
4
+ // append-logs (read-all-rows / append-row), whole-file JSON sidecars
5
+ // (read/write/exists/remove), directory listing, and atomic-write + cross-process
6
+ // file-lock. `WorldStore` names exactly that set, and nothing more. Route the whole
7
+ // kernel through it and the twin persists through ANY backend — the local
8
+ // filesystem today (`FsWorldStore`, the default), an in-memory Map for tests and
9
+ // serverless proofs (`MemoryWorldStore`), a Durable Object / KV / redis tomorrow —
10
+ // without touching a line of twin business logic.
11
+ //
12
+ // The interface is SYNCHRONOUS on purpose. The kernel's internals (append-then-
13
+ // project, read-then-append critical sections) are synchronous, and `clerk-twin.ts`'s
14
+ // request handler is a pure sync function keyed by `root`. Making the store async
15
+ // would ripple `await` through every one of those call sites and change the handler's
16
+ // shape — precisely the blast radius this seam exists to avoid. An async backend is
17
+ // handled at a SEPARATE persistence boundary: `hydrate()` the durable source into a
18
+ // sync store, run the sync kernel, `flush()` back out. See `hydrateInto`/`flushFrom`.
19
+ //
20
+ // NOT part of this store, deliberately: `rateBudget.ts`'s ledger. That is a
21
+ // client-side guard on REAL vendor calls, keyed by CREDENTIAL and living in the user's
22
+ // home dir (NOT under the project state root) — it is not twin persistence and never
23
+ // runs on the serve path. Its bespoke inode-identity lock and reservation semantics
24
+ // cannot be expressed through this generic seam without weakening them, so it stays on
25
+ // `node:fs`. See its module header.
26
+ import {
27
+ appendFileSync,
28
+ chmodSync,
29
+ closeSync,
30
+ existsSync,
31
+ fsyncSync,
32
+ fstatSync,
33
+ lstatSync,
34
+ mkdirSync,
35
+ openSync,
36
+ readdirSync,
37
+ readSync,
38
+ realpathSync,
39
+ readFileSync,
40
+ renameSync,
41
+ rmSync,
42
+ statSync,
43
+ unlinkSync,
44
+ writeFileSync,
45
+ } from 'node:fs';
46
+ import { hostname } from 'node:os';
47
+ import { basename, dirname, join, resolve } from 'node:path';
48
+
49
+ /** Metadata a caller needs about a stored path. `isDirectory` distinguishes a JSON
50
+ * sidecar from a per-service subdir (validate/scrub walk on it); `size`+`mtimeMs`
51
+ * drive the append-dedupe index's cache-validity check in storage.ts. */
52
+ export type WorldStat = { size: number; mtimeMs: number; isDirectory: boolean; isSymbolicLink?: boolean };
53
+
54
+ /**
55
+ * The synchronous persistence seam. Every path is an ABSOLUTE key the caller already
56
+ * computed (via `worldPaths`/`worldStateRoot`); the store treats it as an opaque key,
57
+ * so a single active store partitions cleanly by `root` (the root is embedded in the
58
+ * key) and no per-root wiring is needed.
59
+ */
60
+ export interface WorldStore {
61
+ /** Whole-file read; `null` when the path does not exist. */
62
+ read(path: string): string | null;
63
+ /** File content split on `\n` (blank lines and the trailing empty included, so a
64
+ * caller can report 1-based row numbers); `[]` when the path does not exist. */
65
+ readLines(path: string): string[];
66
+ /** Raw append of exactly `data` (the caller owns any trailing newline). On the fs
67
+ * backend this is the VOLTER_DURABLE-aware durable append. Does NOT create parents —
68
+ * call `mkdir` first, matching the historical `appendDurable` contract. */
69
+ append(path: string, data: string): void;
70
+ /** Non-atomic whole-file write, creating parent dirs. Matches the plain
71
+ * `writeFileSync` sidecar writers (cursors, refs, leases, plans, fork-meta). `secret` makes the
72
+ * file owner-only (0600 in a 0700 directory) where the backend has permissions. */
73
+ write(path: string, data: string, options?: { secret?: boolean }): void;
74
+ /** Atomic whole-file write (write-temp-then-rename on fs), creating parent dirs.
75
+ * Used where a reader must never observe a half-written document (state.json). `secret` makes
76
+ * the file owner-only where the backend has permissions. */
77
+ writeAtomic(path: string, data: string, options?: { secret?: boolean }): void;
78
+ /** Does the path exist? */
79
+ exists(path: string): boolean;
80
+ /** Recursively remove the path (force; a missing path is not an error). */
81
+ remove(path: string): void;
82
+ /** Ensure a directory (and parents) exists. A no-op on backends with implicit dirs. */
83
+ mkdir(dirPath: string): void;
84
+ /** Immediate child names of a directory; `[]` when it does not exist. */
85
+ list(dirPath: string): string[];
86
+ /** Metadata for a path, or `null` when it does not exist. */
87
+ stat(path: string): WorldStat | null;
88
+ /** The file from byte `start` to its end, decoded as UTF-8 (`start` must fall on a character boundary: a line's
89
+ * start in a log); `null` when the path does not exist. Optional: a reader of an append-only log uses it to read
90
+ * only what was appended, and reads the whole file on a store without it. */
91
+ readRange?(path: string, start: number): string | null;
92
+ /** Resolve filesystem aliases for durable ownership; absent on opaque-key stores. */
93
+ canonicalPath?(path: string): string;
94
+ /** Run `fn` holding an exclusive lock on `lockPath`, releasing it afterwards. The fs
95
+ * backend uses the historical cross-process reclaimable file lock; an in-memory,
96
+ * single-instance backend is a synchronous pass-through. */
97
+ withLock<T>(lockPath: string, fn: () => T, options?: { retainLiveOwner?: boolean }): T;
98
+ }
99
+
100
+ // ── FsWorldStore — the DEFAULT, byte-identical to the historical node:fs behavior ────
101
+
102
+ let fsAtomicSequence = 0;
103
+
104
+ /** How long a lock may live before an acquirer treats it as abandoned. Above the 10s
105
+ * wait timeout so a live-but-slow holder is never reclaimed out from under itself. */
106
+ const LOCK_STALE_MS = 60_000;
107
+
108
+ type LockHolder = { pid: number; hostname: string; at: string };
109
+
110
+ function readLockHolder(lockPath: string): LockHolder | null {
111
+ try {
112
+ return JSON.parse(readFileSync(lockPath, 'utf8')) as LockHolder;
113
+ } catch {
114
+ return null; // missing, empty (mid-write), or malformed
115
+ }
116
+ }
117
+
118
+ function pidAlive(pid: number): boolean {
119
+ try {
120
+ process.kill(pid, 0);
121
+ return true;
122
+ } catch (error) {
123
+ // ESRCH → no such process (dead); EPERM → exists but not ours to signal (alive)
124
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
125
+ }
126
+ }
127
+
128
+ /** A lock is stale when its holder is provably gone (same host + dead pid) or it has
129
+ * outlived LOCK_STALE_MS. The age falls back to the lockfile's own mtime when the
130
+ * `at` record is unreadable — so a writer that crashed between creating the lock and
131
+ * recording itself is still eventually reclaimed, while a freshly-created (recent
132
+ * mtime) empty lock is left alone, avoiding a race with the live writer. */
133
+ function lockIsStale(lockPath: string, retainLiveOwner = false): boolean {
134
+ const holder = readLockHolder(lockPath);
135
+ if (holder && holder.hostname === hostname() && Number.isInteger(holder.pid) && !pidAlive(holder.pid)) {
136
+ return true;
137
+ }
138
+ if (retainLiveOwner) return false;
139
+ const recordedAt = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
140
+ let stamp = recordedAt;
141
+ if (!Number.isFinite(stamp)) {
142
+ try {
143
+ stamp = statSync(lockPath).mtimeMs;
144
+ } catch {
145
+ return false; // lock vanished — let the next openSync settle it
146
+ }
147
+ }
148
+ return Date.now() - stamp > LOCK_STALE_MS;
149
+ }
150
+
151
+ function sleepSync(ms: number): void {
152
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
153
+ }
154
+
155
+ /**
156
+ * The default store: a thin, faithful pass-through to `node:fs`. Every method preserves
157
+ * the exact behavior the kernel had before the seam existed — the durable append
158
+ * (VOLTER_DURABLE), the atomic write (temp + rename), and the reclaimable cross-process
159
+ * file lock — so the existing twins and the conformance suite are unaffected.
160
+ */
161
+ export class FsWorldStore implements WorldStore {
162
+ read(path: string): string | null {
163
+ if (!existsSync(path)) return null;
164
+ return readFileSync(path, 'utf8');
165
+ }
166
+
167
+ readLines(path: string): string[] {
168
+ if (!existsSync(path)) return [];
169
+ return readFileSync(path, 'utf8').split('\n');
170
+ }
171
+
172
+ readRange(path: string, start: number): string | null {
173
+ let fd: number;
174
+ try { fd = openSync(path, 'r'); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error; }
175
+ try {
176
+ const size = fstatSync(fd).size;
177
+ if (start >= size) return '';
178
+ const buffer = Buffer.allocUnsafe(size - start);
179
+ let read = 0;
180
+ while (read < buffer.length) { const n = readSync(fd, buffer, read, buffer.length - read, start + read); if (n === 0) break; read += n; }
181
+ return buffer.toString('utf8', 0, read);
182
+ } finally { closeSync(fd); }
183
+ }
184
+
185
+ append(path: string, data: string): void {
186
+ // Historical appendDurable: a completed append syscall survives a process crash;
187
+ // fsync additionally survives kernel-panic/power-loss when VOLTER_DURABLE=1.
188
+ const fd = openSync(path, 'a');
189
+ try {
190
+ appendFileSync(fd, data);
191
+ if (process.env.VOLTER_DURABLE === '1') fsyncSync(fd);
192
+ } finally {
193
+ closeSync(fd);
194
+ }
195
+ }
196
+
197
+ write(path: string, data: string, options: { secret?: boolean } = {}): void {
198
+ if (!options.secret) {
199
+ mkdirSync(dirname(path), { recursive: true });
200
+ writeFileSync(path, data);
201
+ return;
202
+ }
203
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
204
+ writeFileSync(path, data, { mode: 0o600 });
205
+ chmodSync(path, 0o600);
206
+ }
207
+
208
+ writeAtomic(path: string, data: string, options: { secret?: boolean } = {}): void {
209
+ mkdirSync(dirname(path), { recursive: true });
210
+ const tmp = `${path}.${process.pid}.${Date.now()}.${fsAtomicSequence++}.tmp`;
211
+ try {
212
+ writeFileSync(tmp, data, options.secret ? { mode: 0o600 } : undefined);
213
+ renameSync(tmp, path);
214
+ if (options.secret) chmodSync(path, 0o600);
215
+ } finally {
216
+ rmSync(tmp, { force: true });
217
+ }
218
+ }
219
+
220
+ exists(path: string): boolean {
221
+ return existsSync(path);
222
+ }
223
+
224
+ remove(path: string): void {
225
+ rmSync(path, { recursive: true, force: true });
226
+ }
227
+
228
+ mkdir(dirPath: string): void {
229
+ mkdirSync(dirPath, { recursive: true });
230
+ }
231
+
232
+ list(dirPath: string): string[] {
233
+ if (!existsSync(dirPath)) return [];
234
+ return readdirSync(dirPath);
235
+ }
236
+
237
+ canonicalPath(path: string): string {
238
+ const absolute = resolve(path);
239
+ try { return realpathSync(absolute); }
240
+ catch (error) {
241
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || dirname(absolute) === absolute) throw error;
242
+ return join(this.canonicalPath(dirname(absolute)), basename(absolute));
243
+ }
244
+ }
245
+
246
+ stat(path: string): WorldStat | null {
247
+ if (!existsSync(path)) return null;
248
+ const s = statSync(path);
249
+ return { size: s.size, mtimeMs: s.mtimeMs, isDirectory: s.isDirectory(), ...(lstatSync(path).isSymbolicLink() ? { isSymbolicLink: true } : {}) };
250
+ }
251
+
252
+ /** Run `fn` holding an exclusive cross-process file lock. The lockfile records
253
+ * `{pid, hostname, at}`; a contender that finds a stale lock (dead pid or age >
254
+ * LOCK_STALE_MS) reclaims it by atomically renaming it aside — so a crashed holder
255
+ * can't wedge the world forever. Reclaim is race-safe: only the process that wins
256
+ * the rename clears the stale inode, and the exclusive `wx` create still decides the
257
+ * winner. (Moved verbatim from storage.ts `withFileLock`.) */
258
+ withLock<T>(lockPath: string, fn: () => T, options: { retainLiveOwner?: boolean } = {}): T {
259
+ mkdirSync(dirname(lockPath), { recursive: true });
260
+ const started = Date.now();
261
+ let fd: number | null = null;
262
+ while (fd === null) {
263
+ try {
264
+ fd = openSync(lockPath, 'wx');
265
+ } catch (error) {
266
+ const code = (error as NodeJS.ErrnoException).code;
267
+ if (code !== 'EEXIST') throw error;
268
+ if (lockIsStale(lockPath, options.retainLiveOwner)) {
269
+ // Serialize reclaimers, then re-read: a contender's earlier stale observation
270
+ // must never rename another contender's newly acquired, live lock.
271
+ const recoveryPath = `${lockPath}.recovery`;
272
+ let recovery: number;
273
+ try { recovery = openSync(recoveryPath, 'wx'); }
274
+ catch (error) {
275
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
276
+ if (Date.now() - started > 10_000) throw new Error(`Lock recovery is active or interrupted: ${recoveryPath}. Inspect its owner before removing the recovery record.`);
277
+ sleepSync(25); continue;
278
+ }
279
+ try {
280
+ writeFileSync(recovery, JSON.stringify({ pid: process.pid, hostname: hostname() }));
281
+ if (lockIsStale(lockPath, options.retainLiveOwner)) {
282
+ const holder = readLockHolder(lockPath);
283
+ const salvage = `${lockPath}.stale.${process.pid}.${Date.now()}`;
284
+ try {
285
+ renameSync(lockPath, salvage);
286
+ console.warn(`[world] reclaiming stale storage lock ${lockPath} (held by pid ${holder?.pid ?? '?'} on ${holder?.hostname ?? '?'})`);
287
+ unlinkSync(salvage);
288
+ } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
289
+ }
290
+ } finally { closeSync(recovery); unlinkSync(recoveryPath); }
291
+ continue;
292
+ }
293
+ if (Date.now() - started > 10_000) {
294
+ const holder = readLockHolder(lockPath);
295
+ const since = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
296
+ 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` : ''})` : ''}`);
297
+ }
298
+ sleepSync(25);
299
+ }
300
+ }
301
+
302
+ // Record the holder so a later contender can detect staleness. Best-effort: a write
303
+ // failure here doesn't weaken exclusivity, only staleness diagnostics.
304
+ try {
305
+ writeFileSync(fd, `${JSON.stringify({ pid: process.pid, hostname: hostname(), at: new Date().toISOString() } satisfies LockHolder)}\n`);
306
+ } catch { /* ignore */ }
307
+
308
+ const acquired = Date.now();
309
+ try {
310
+ return fn();
311
+ } finally {
312
+ // Every World on the machine waits on some of these locks; a long hold stalls them all.
313
+ const heldMs = Date.now() - acquired;
314
+ 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') ?? ''}`);
315
+ const held = fstatSync(fd);
316
+ closeSync(fd);
317
+ try {
318
+ const current = lstatSync(lockPath);
319
+ if (held.dev === current.dev && held.ino === current.ino) unlinkSync(lockPath);
320
+ } catch { /* already reclaimed by a stale-lock sweep */ }
321
+ }
322
+ }
323
+ }
324
+
325
+ // ── MemoryWorldStore — a Map-backed store; the proof a non-fs transport works ─────────
326
+
327
+ // A process-global, strictly-increasing stamp source shared by every MemoryWorldStore, so
328
+ // no two writes (across instances or requests) ever collide on `stat().mtimeMs`. See `bump`.
329
+ let memoryStoreClock = 0;
330
+
331
+ const byteLength = (text: string): number => new TextEncoder().encode(text).length;
332
+
333
+ /**
334
+ * An in-memory store. Files are entries in a `Map<path, string>`; directories are
335
+ * synthesized from the key set (a path is a directory when it is a strict prefix of a
336
+ * file key). `withLock` is a synchronous pass-through — a single in-process instance has
337
+ * no concurrent writers to serialize, so the read-then-append critical sections the
338
+ * kernel guards are already atomic under the single-threaded event loop.
339
+ *
340
+ * This is the portability proof: point the active store here and the entire kernel — and
341
+ * the clerk twin on top of it — runs with the filesystem untouched.
342
+ */
343
+ export class MemoryWorldStore implements WorldStore {
344
+ /** path → content, and path → version stamp driving `stat().mtimeMs`. */
345
+ private readonly files = new Map<string, string>();
346
+ private readonly versions = new Map<string, number>();
347
+ /** path → UTF-8 byte size, kept as content changes so `stat` never re-encodes a growing log. */
348
+ private readonly sizes = new Map<string, number>();
349
+ /** The stamp of this store's most recent mutation (0 = never mutated). A serverless
350
+ * entry snapshots this before running the sync kernel and flushes ONLY when it moved —
351
+ * read-only requests (the common case) then skip the durable write entirely. */
352
+ private lastMutation = 0;
353
+
354
+ mutationStamp(): number {
355
+ return this.lastMutation;
356
+ }
357
+
358
+ /** Paths whose content changed strictly after `stamp` — the DELTA-FLUSH primitive
359
+ * (runtime contract R11b): a serverless entry records `mutationStamp()` at hydration,
360
+ * runs the sync kernel, then persists ONLY these paths (plus removals, which the caller
361
+ * detects by diffing its hydrated path set against `entries()` — a removed path has no
362
+ * version to report). O(paths) scan, O(delta) write. */
363
+ changedSince(stamp: number): string[] {
364
+ const changed: string[] = [];
365
+ for (const [path, version] of this.versions) {
366
+ if (version > stamp && this.files.has(path)) changed.push(path);
367
+ }
368
+ return changed.sort();
369
+ }
370
+
371
+ private bump(path: string): void {
372
+ // Draw the stamp from a PROCESS-GLOBAL monotonic counter, not a per-instance one.
373
+ // storage.ts's append-dedupe cache is keyed by path and validated by (size, mtimeMs),
374
+ // and it persists across requests in a Worker isolate while a fresh MemoryWorldStore is
375
+ // created per request (the hydrate→sync-kernel→flush shape). A per-instance counter
376
+ // resets to 0 each request, so two instances could present the SAME (size, mtimeMs) for
377
+ // one path with DIFFERENT content — serving a stale index (dedupe bypass) or a false
378
+ // conflict. A global counter makes every write across every instance a distinct,
379
+ // strictly-increasing stamp, so a cross-instance collision is impossible.
380
+ this.lastMutation = memoryStoreClock += 1;
381
+ this.versions.set(path, this.lastMutation);
382
+ }
383
+
384
+ read(path: string): string | null {
385
+ return this.files.has(path) ? this.files.get(path)! : null;
386
+ }
387
+
388
+ readLines(path: string): string[] {
389
+ const content = this.files.get(path);
390
+ return content === undefined ? [] : content.split('\n');
391
+ }
392
+
393
+ append(path: string, data: string): void {
394
+ this.files.set(path, (this.files.get(path) ?? '') + data);
395
+ this.sizes.set(path, (this.sizes.get(path) ?? 0) + byteLength(data));
396
+ this.bump(path);
397
+ }
398
+
399
+ write(path: string, data: string): void {
400
+ this.files.set(path, data);
401
+ this.sizes.set(path, byteLength(data));
402
+ this.bump(path);
403
+ }
404
+
405
+ writeAtomic(path: string, data: string): void {
406
+ // Atomic by nature in a single process: the assignment is indivisible, so no reader
407
+ // ever observes a partial document.
408
+ this.files.set(path, data);
409
+ this.sizes.set(path, byteLength(data));
410
+ this.bump(path);
411
+ }
412
+
413
+ exists(path: string): boolean {
414
+ if (this.files.has(path)) return true;
415
+ // A directory "exists" when some file lives under it.
416
+ const prefix = path.endsWith('/') ? path : `${path}/`;
417
+ for (const key of this.files.keys()) if (key.startsWith(prefix)) return true;
418
+ return false;
419
+ }
420
+
421
+ remove(path: string): void {
422
+ let removedAny = false;
423
+ if (this.files.delete(path)) {
424
+ this.versions.delete(path);
425
+ this.sizes.delete(path);
426
+ removedAny = true;
427
+ }
428
+ const prefix = path.endsWith('/') ? path : `${path}/`;
429
+ for (const key of [...this.files.keys()]) {
430
+ if (key.startsWith(prefix)) {
431
+ this.files.delete(key);
432
+ this.versions.delete(key);
433
+ this.sizes.delete(key);
434
+ removedAny = true;
435
+ }
436
+ }
437
+ // A pure removal is a mutation too: without this, a request that only deletes state
438
+ // would read as clean and the delta-flush would never persist the removal (R11b).
439
+ if (removedAny) this.lastMutation = memoryStoreClock += 1;
440
+ }
441
+
442
+ mkdir(_dirPath: string): void {
443
+ // Directories are implicit in the key set; nothing to do.
444
+ }
445
+
446
+ list(dirPath: string): string[] {
447
+ const prefix = dirPath.endsWith('/') ? dirPath : `${dirPath}/`;
448
+ const children = new Set<string>();
449
+ for (const key of this.files.keys()) {
450
+ if (!key.startsWith(prefix)) continue;
451
+ const rest = key.slice(prefix.length);
452
+ const slash = rest.indexOf('/');
453
+ children.add(slash === -1 ? rest : rest.slice(0, slash));
454
+ }
455
+ return [...children];
456
+ }
457
+
458
+ stat(path: string): WorldStat | null {
459
+ const content = this.files.get(path);
460
+ if (content !== undefined) {
461
+ return { size: this.sizes.get(path) ?? byteLength(content), mtimeMs: this.versions.get(path) ?? 0, isDirectory: false };
462
+ }
463
+ if (this.exists(path)) {
464
+ return { size: 0, mtimeMs: 0, isDirectory: true };
465
+ }
466
+ return null;
467
+ }
468
+
469
+ withLock<T>(_lockPath: string, fn: () => T): T {
470
+ return fn();
471
+ }
472
+
473
+ /** Test/inspection helper: a plain snapshot of every stored file. Not part of the
474
+ * `WorldStore` contract — used by `flushFrom` and by tests asserting no fs was touched. */
475
+ entries(): Record<string, string> {
476
+ return Object.fromEntries(this.files);
477
+ }
478
+ }
479
+
480
+ // ── the active store (the injection point) ───────────────────────────────────────────
481
+
482
+ let activeStore: WorldStore = new FsWorldStore();
483
+
484
+ /** The request-scoped store: `withWorldStore` runs its callback inside an AsyncLocalStorage
485
+ * context where the runtime has one (Node, Bun, workerd with nodejs_compat), so concurrent
486
+ * requests for different worlds in one isolate each see their own store across awaits. Reached
487
+ * through `process.getBuiltinModule`, never a static import, so a browser bundle stays clean and
488
+ * falls back to the module-global swap. */
489
+ type StoreScope = { getStore(): WorldStore | undefined; run<R>(store: WorldStore, fn: () => R): R };
490
+ const scope: StoreScope | null = (() => {
491
+ try {
492
+ const hooks = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process?.getBuiltinModule?.('node:async_hooks') as { AsyncLocalStorage?: new () => StoreScope } | undefined;
493
+ return hooks?.AsyncLocalStorage ? new hooks.AsyncLocalStorage() : null;
494
+ } catch {
495
+ return null;
496
+ }
497
+ })();
498
+
499
+ /** The store the kernel currently persists through: the request's scoped store, else the
500
+ * module-global one (`FsWorldStore` by default). */
501
+ export function getActiveWorldStore(): WorldStore {
502
+ return scope?.getStore() ?? activeStore;
503
+ }
504
+
505
+ /** Swap the active store. A serverless/DO/test entry sets a `MemoryWorldStore` (or a
506
+ * backend-backed store) here before running the sync kernel, and restores the previous
507
+ * one afterwards. Returns the store that was active, so callers can restore it. */
508
+ export function setActiveWorldStore(store: WorldStore): WorldStore {
509
+ const previous = activeStore;
510
+ activeStore = store;
511
+ return previous;
512
+ }
513
+
514
+ /** Run `fn` with `store` active, restoring the previous store afterwards (even on
515
+ * throw). ASYNC-AWARE: when `fn` returns a promise, the restore happens after it
516
+ * SETTLES — the first version restored at `fn`'s return, i.e. at an async callback's
517
+ * FIRST await, un-scoping the rest of the request (both serverless pack lanes hit this
518
+ * independently and hand-rolled the same try/finally, 2026-09-01).
519
+ *
520
+ * CONCURRENCY: where the runtime has AsyncLocalStorage the scope is per async context, so
521
+ * unserialized requests for different worlds interleave without bleeding stores. Only a runtime
522
+ * without it (a browser bundle) falls back to the module-global swap, which is safe only when
523
+ * store-scoped work is serialized. */
524
+ export function withWorldStore<T>(store: WorldStore, fn: () => Promise<T>): Promise<T>;
525
+ export function withWorldStore<T>(store: WorldStore, fn: () => T): T;
526
+ export function withWorldStore<T>(store: WorldStore, fn: () => T | Promise<T>): T | Promise<T> {
527
+ if (scope) return scope.run(store, fn);
528
+ const previous = setActiveWorldStore(store);
529
+ let result: T | Promise<T>;
530
+ try {
531
+ result = fn();
532
+ } catch (error) {
533
+ setActiveWorldStore(previous);
534
+ throw error;
535
+ }
536
+ if (result instanceof Promise) {
537
+ return result.finally(() => setActiveWorldStore(previous));
538
+ }
539
+ setActiveWorldStore(previous);
540
+ return result;
541
+ }
542
+
543
+ // ── async persistence boundary ───────────────────────────────────────────────────────
544
+ //
545
+ // The kernel is synchronous; a durable backend (Durable Object storage, KV, redis, S3)
546
+ // is asynchronous. These two helpers are the ONLY async in the persistence story: a
547
+ // serverless entry `await hydrate()`s the durable snapshot into a sync store, runs the
548
+ // sync kernel (which never awaits), then `await flush()`es the mutated snapshot back.
549
+ // The shapes are proven against `MemoryWorldStore` (see world-store.test.ts); a real DO
550
+ // adapter implements `HydrationSource`/`HydrationSink` over its own async storage.
551
+
552
+ /** A durable, async source of a world snapshot (path → content). */
553
+ export interface HydrationSource {
554
+ load(): Promise<Record<string, string>>;
555
+ }
556
+
557
+ /** A durable, async sink for a world snapshot (path → content). */
558
+ export interface HydrationSink {
559
+ save(snapshot: Record<string, string>): Promise<void>;
560
+ }
561
+
562
+ /** Fill `store` from an async durable `source` before the sync kernel runs. */
563
+ export async function hydrateInto(store: MemoryWorldStore, source: HydrationSource): Promise<void> {
564
+ const snapshot = await source.load();
565
+ for (const [path, content] of Object.entries(snapshot)) store.write(path, content);
566
+ }
567
+
568
+ /** Persist `store`'s current contents back to an async durable `sink` after the sync
569
+ * kernel has run. */
570
+ export async function flushFrom(store: MemoryWorldStore, sink: HydrationSink): Promise<void> {
571
+ await sink.save(store.entries());
572
+ }
@@ -0,0 +1,27 @@
1
+ // World service config — generic per-service settings for the twin runtime
2
+ // (`.volter/world/config.json`). Kept deliberately small + open: the index
3
+ // signature lets a service carry arbitrary extra config that downstream tools
4
+ // define. (The tracker's annotation adapter defines + reads `annotationPolicy` and
5
+ // `browseUrlTemplate` on top of this — those are verification concerns, not the
6
+ // twin's, so they are NOT typed here.)
7
+ import { join } from 'node:path';
8
+ import { worldStateRoot } from './storage.ts';
9
+ import { getActiveWorldStore } from './world-store.ts';
10
+
11
+ export type WorldServiceConfig = {
12
+ provider?: string;
13
+ tool?: string;
14
+ pollLimit?: number;
15
+ } & Record<string, unknown>;
16
+
17
+ export type WorldConfig = {
18
+ services: Record<string, WorldServiceConfig>;
19
+ } & Record<string, unknown>;
20
+
21
+ export function loadWorldConfig(root?: string): WorldConfig {
22
+ const path = join(worldStateRoot(root), 'config.json');
23
+ const raw = getActiveWorldStore().read(path);
24
+ if (raw === null) return { services: {} };
25
+ const parsed = JSON.parse(raw) as Partial<WorldConfig>;
26
+ return { ...parsed, services: parsed.services ?? {} };
27
+ }
@@ -0,0 +1,80 @@
1
+ // THE STREAM BRIDGE: a World's byte-stream doors (a TCP protocol a twin serves — smtp's own, planetscale's
2
+ // mysql — carried over a WebSocket, because a hosted World takes no inbound TCP) as loopback listeners,
3
+ // so an UNMODIFIED client (nodemailer, mysql2, Python's smtplib) connects to 127.0.0.1:<port> as it would
4
+ // to the twin's own listener. One WebSocket per TCP connection; its first message is the World token,
5
+ // then bytes both ways. Used by the attach entry (in the app's process) and by `volter-world attach`
6
+ // (beside a command in any language).
7
+ 'use strict';
8
+
9
+ const net = require('node:net');
10
+
11
+ /** The bridged connections whose WebSocket has not closed yet, across every bridge. */
12
+ const live = new Set();
13
+
14
+ /** Serve one stream door on 127.0.0.1:`port` (0: any). Resolves to the listening server. */
15
+ function bridgeStream(url, token, port = 0) {
16
+ const server = net.createServer((socket) => {
17
+ const ws = new WebSocket(url);
18
+ const done = new Promise((resolve) => ws.addEventListener('close', resolve));
19
+ live.add(done);
20
+ done.then(() => live.delete(done));
21
+ ws.binaryType = 'arraybuffer';
22
+ const early = [];
23
+ let open = false;
24
+ // the client may write and hang up before the WebSocket opens: its bytes still go, then the close
25
+ let hangUpWhenOpen = false;
26
+ ws.addEventListener('open', () => {
27
+ ws.send(token);
28
+ open = true;
29
+ for (const chunk of early) ws.send(chunk);
30
+ early.length = 0;
31
+ if (hangUpWhenOpen) ws.close(1000);
32
+ });
33
+ ws.addEventListener('message', (event) => { if (typeof event.data !== 'string') socket.write(Buffer.from(event.data)); });
34
+ ws.addEventListener('close', (event) => {
35
+ if (event.code >= 4000) process.stderr.write(`[twin-stream] ${url}: ${event.reason || event.code}\n`);
36
+ socket.end();
37
+ });
38
+ ws.addEventListener('error', () => socket.destroy());
39
+ socket.on('data', (chunk) => { if (open) ws.send(chunk); else early.push(chunk); });
40
+ const hangUp = () => {
41
+ if (!open) { hangUpWhenOpen = true; return; }
42
+ try { ws.close(1000); } catch { /* already closed */ }
43
+ };
44
+ socket.on('close', hangUp);
45
+ socket.on('error', hangUp);
46
+ });
47
+ return new Promise((resolve, reject) => {
48
+ server.once('error', reject);
49
+ server.listen(port, '127.0.0.1', () => { server.off('error', reject); resolve(server); });
50
+ });
51
+ }
52
+
53
+ /** The env a client reads for a stream, with the bridge's listener in place of `${host}`/`${port}`. */
54
+ function streamEnv(templates, port) {
55
+ const out = {};
56
+ for (const [name, template] of Object.entries(templates || {})) {
57
+ out[name] = String(template).replace(/\$\{host\}/g, '127.0.0.1').replace(/\$\{port\}/g, String(port));
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /** Resolves once every bridged connection has finished carrying its bytes (or after `ms`): what a
63
+ * launcher waits for after its command exits, since a client may write and exit at once. */
64
+ function drainStreams(ms = 10_000) {
65
+ return Promise.race([Promise.all([...live]), new Promise((resolve) => setTimeout(resolve, ms).unref())]);
66
+ }
67
+
68
+ /** Bridge every stream of a manifest; resolves to the env naming the listeners and the servers. */
69
+ async function bridgeStreams(streams, token) {
70
+ const env = {};
71
+ const servers = [];
72
+ for (const stream of Object.values(streams || {})) {
73
+ const server = await bridgeStream(stream.url, token);
74
+ servers.push(server);
75
+ Object.assign(env, streamEnv(stream.env, server.address().port));
76
+ }
77
+ return { env, servers };
78
+ }
79
+
80
+ module.exports = { bridgeStream, bridgeStreams, drainStreams, streamEnv };