@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,168 @@
1
+ /** Metadata a caller needs about a stored path. `isDirectory` distinguishes a JSON
2
+ * sidecar from a per-service subdir (validate/scrub walk on it); `size`+`mtimeMs`
3
+ * drive the append-dedupe index's cache-validity check in storage.ts. */
4
+ export type WorldStat = {
5
+ size: number;
6
+ mtimeMs: number;
7
+ isDirectory: boolean;
8
+ isSymbolicLink?: boolean;
9
+ };
10
+ /**
11
+ * The synchronous persistence seam. Every path is an ABSOLUTE key the caller already
12
+ * computed (via `worldPaths`/`worldStateRoot`); the store treats it as an opaque key,
13
+ * so a single active store partitions cleanly by `root` (the root is embedded in the
14
+ * key) and no per-root wiring is needed.
15
+ */
16
+ export interface WorldStore {
17
+ /** Whole-file read; `null` when the path does not exist. */
18
+ read(path: string): string | null;
19
+ /** File content split on `\n` (blank lines and the trailing empty included, so a
20
+ * caller can report 1-based row numbers); `[]` when the path does not exist. */
21
+ readLines(path: string): string[];
22
+ /** Raw append of exactly `data` (the caller owns any trailing newline). On the fs
23
+ * backend this is the VOLTER_DURABLE-aware durable append. Does NOT create parents —
24
+ * call `mkdir` first, matching the historical `appendDurable` contract. */
25
+ append(path: string, data: string): void;
26
+ /** Non-atomic whole-file write, creating parent dirs. Matches the plain
27
+ * `writeFileSync` sidecar writers (cursors, refs, leases, plans, fork-meta). `secret` makes the
28
+ * file owner-only (0600 in a 0700 directory) where the backend has permissions. */
29
+ write(path: string, data: string, options?: {
30
+ secret?: boolean;
31
+ }): void;
32
+ /** Atomic whole-file write (write-temp-then-rename on fs), creating parent dirs.
33
+ * Used where a reader must never observe a half-written document (state.json). `secret` makes
34
+ * the file owner-only where the backend has permissions. */
35
+ writeAtomic(path: string, data: string, options?: {
36
+ secret?: boolean;
37
+ }): void;
38
+ /** Does the path exist? */
39
+ exists(path: string): boolean;
40
+ /** Recursively remove the path (force; a missing path is not an error). */
41
+ remove(path: string): void;
42
+ /** Ensure a directory (and parents) exists. A no-op on backends with implicit dirs. */
43
+ mkdir(dirPath: string): void;
44
+ /** Immediate child names of a directory; `[]` when it does not exist. */
45
+ list(dirPath: string): string[];
46
+ /** Metadata for a path, or `null` when it does not exist. */
47
+ stat(path: string): WorldStat | null;
48
+ /** The file from byte `start` to its end, decoded as UTF-8 (`start` must fall on a character boundary: a line's
49
+ * start in a log); `null` when the path does not exist. Optional: a reader of an append-only log uses it to read
50
+ * only what was appended, and reads the whole file on a store without it. */
51
+ readRange?(path: string, start: number): string | null;
52
+ /** Resolve filesystem aliases for durable ownership; absent on opaque-key stores. */
53
+ canonicalPath?(path: string): string;
54
+ /** Run `fn` holding an exclusive lock on `lockPath`, releasing it afterwards. The fs
55
+ * backend uses the historical cross-process reclaimable file lock; an in-memory,
56
+ * single-instance backend is a synchronous pass-through. */
57
+ withLock<T>(lockPath: string, fn: () => T, options?: {
58
+ retainLiveOwner?: boolean;
59
+ }): T;
60
+ }
61
+ /**
62
+ * The default store: a thin, faithful pass-through to `node:fs`. Every method preserves
63
+ * the exact behavior the kernel had before the seam existed — the durable append
64
+ * (VOLTER_DURABLE), the atomic write (temp + rename), and the reclaimable cross-process
65
+ * file lock — so the existing twins and the conformance suite are unaffected.
66
+ */
67
+ export declare class FsWorldStore implements WorldStore {
68
+ read(path: string): string | null;
69
+ readLines(path: string): string[];
70
+ readRange(path: string, start: number): string | null;
71
+ append(path: string, data: string): void;
72
+ write(path: string, data: string, options?: {
73
+ secret?: boolean;
74
+ }): void;
75
+ writeAtomic(path: string, data: string, options?: {
76
+ secret?: boolean;
77
+ }): void;
78
+ exists(path: string): boolean;
79
+ remove(path: string): void;
80
+ mkdir(dirPath: string): void;
81
+ list(dirPath: string): string[];
82
+ canonicalPath(path: string): string;
83
+ stat(path: string): WorldStat | null;
84
+ /** Run `fn` holding an exclusive cross-process file lock. The lockfile records
85
+ * `{pid, hostname, at}`; a contender that finds a stale lock (dead pid or age >
86
+ * LOCK_STALE_MS) reclaims it by atomically renaming it aside — so a crashed holder
87
+ * can't wedge the world forever. Reclaim is race-safe: only the process that wins
88
+ * the rename clears the stale inode, and the exclusive `wx` create still decides the
89
+ * winner. (Moved verbatim from storage.ts `withFileLock`.) */
90
+ withLock<T>(lockPath: string, fn: () => T, options?: {
91
+ retainLiveOwner?: boolean;
92
+ }): T;
93
+ }
94
+ /**
95
+ * An in-memory store. Files are entries in a `Map<path, string>`; directories are
96
+ * synthesized from the key set (a path is a directory when it is a strict prefix of a
97
+ * file key). `withLock` is a synchronous pass-through — a single in-process instance has
98
+ * no concurrent writers to serialize, so the read-then-append critical sections the
99
+ * kernel guards are already atomic under the single-threaded event loop.
100
+ *
101
+ * This is the portability proof: point the active store here and the entire kernel — and
102
+ * the clerk twin on top of it — runs with the filesystem untouched.
103
+ */
104
+ export declare class MemoryWorldStore implements WorldStore {
105
+ /** path → content, and path → version stamp driving `stat().mtimeMs`. */
106
+ private readonly files;
107
+ private readonly versions;
108
+ /** path → UTF-8 byte size, kept as content changes so `stat` never re-encodes a growing log. */
109
+ private readonly sizes;
110
+ /** The stamp of this store's most recent mutation (0 = never mutated). A serverless
111
+ * entry snapshots this before running the sync kernel and flushes ONLY when it moved —
112
+ * read-only requests (the common case) then skip the durable write entirely. */
113
+ private lastMutation;
114
+ mutationStamp(): number;
115
+ /** Paths whose content changed strictly after `stamp` — the DELTA-FLUSH primitive
116
+ * (runtime contract R11b): a serverless entry records `mutationStamp()` at hydration,
117
+ * runs the sync kernel, then persists ONLY these paths (plus removals, which the caller
118
+ * detects by diffing its hydrated path set against `entries()` — a removed path has no
119
+ * version to report). O(paths) scan, O(delta) write. */
120
+ changedSince(stamp: number): string[];
121
+ private bump;
122
+ read(path: string): string | null;
123
+ readLines(path: string): string[];
124
+ append(path: string, data: string): void;
125
+ write(path: string, data: string): void;
126
+ writeAtomic(path: string, data: string): void;
127
+ exists(path: string): boolean;
128
+ remove(path: string): void;
129
+ mkdir(_dirPath: string): void;
130
+ list(dirPath: string): string[];
131
+ stat(path: string): WorldStat | null;
132
+ withLock<T>(_lockPath: string, fn: () => T): T;
133
+ /** Test/inspection helper: a plain snapshot of every stored file. Not part of the
134
+ * `WorldStore` contract — used by `flushFrom` and by tests asserting no fs was touched. */
135
+ entries(): Record<string, string>;
136
+ }
137
+ /** The store the kernel currently persists through: the request's scoped store, else the
138
+ * module-global one (`FsWorldStore` by default). */
139
+ export declare function getActiveWorldStore(): WorldStore;
140
+ /** Swap the active store. A serverless/DO/test entry sets a `MemoryWorldStore` (or a
141
+ * backend-backed store) here before running the sync kernel, and restores the previous
142
+ * one afterwards. Returns the store that was active, so callers can restore it. */
143
+ export declare function setActiveWorldStore(store: WorldStore): WorldStore;
144
+ /** Run `fn` with `store` active, restoring the previous store afterwards (even on
145
+ * throw). ASYNC-AWARE: when `fn` returns a promise, the restore happens after it
146
+ * SETTLES — the first version restored at `fn`'s return, i.e. at an async callback's
147
+ * FIRST await, un-scoping the rest of the request (both serverless pack lanes hit this
148
+ * independently and hand-rolled the same try/finally, 2026-09-01).
149
+ *
150
+ * CONCURRENCY: where the runtime has AsyncLocalStorage the scope is per async context, so
151
+ * unserialized requests for different worlds interleave without bleeding stores. Only a runtime
152
+ * without it (a browser bundle) falls back to the module-global swap, which is safe only when
153
+ * store-scoped work is serialized. */
154
+ export declare function withWorldStore<T>(store: WorldStore, fn: () => Promise<T>): Promise<T>;
155
+ export declare function withWorldStore<T>(store: WorldStore, fn: () => T): T;
156
+ /** A durable, async source of a world snapshot (path → content). */
157
+ export interface HydrationSource {
158
+ load(): Promise<Record<string, string>>;
159
+ }
160
+ /** A durable, async sink for a world snapshot (path → content). */
161
+ export interface HydrationSink {
162
+ save(snapshot: Record<string, string>): Promise<void>;
163
+ }
164
+ /** Fill `store` from an async durable `source` before the sync kernel runs. */
165
+ export declare function hydrateInto(store: MemoryWorldStore, source: HydrationSource): Promise<void>;
166
+ /** Persist `store`'s current contents back to an async durable `sink` after the sync
167
+ * kernel has run. */
168
+ export declare function flushFrom(store: MemoryWorldStore, sink: HydrationSink): Promise<void>;
@@ -0,0 +1,475 @@
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 { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
27
+ import { hostname } from 'node:os';
28
+ import { basename, dirname, join, resolve } from 'node:path';
29
+ // ── FsWorldStore — the DEFAULT, byte-identical to the historical node:fs behavior ────
30
+ let fsAtomicSequence = 0;
31
+ /** How long a lock may live before an acquirer treats it as abandoned. Above the 10s
32
+ * wait timeout so a live-but-slow holder is never reclaimed out from under itself. */
33
+ const LOCK_STALE_MS = 60_000;
34
+ function readLockHolder(lockPath) {
35
+ try {
36
+ return JSON.parse(readFileSync(lockPath, 'utf8'));
37
+ }
38
+ catch {
39
+ return null; // missing, empty (mid-write), or malformed
40
+ }
41
+ }
42
+ function pidAlive(pid) {
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ }
47
+ catch (error) {
48
+ // ESRCH → no such process (dead); EPERM → exists but not ours to signal (alive)
49
+ return error.code === 'EPERM';
50
+ }
51
+ }
52
+ /** A lock is stale when its holder is provably gone (same host + dead pid) or it has
53
+ * outlived LOCK_STALE_MS. The age falls back to the lockfile's own mtime when the
54
+ * `at` record is unreadable — so a writer that crashed between creating the lock and
55
+ * recording itself is still eventually reclaimed, while a freshly-created (recent
56
+ * mtime) empty lock is left alone, avoiding a race with the live writer. */
57
+ function lockIsStale(lockPath, retainLiveOwner = false) {
58
+ const holder = readLockHolder(lockPath);
59
+ if (holder && holder.hostname === hostname() && Number.isInteger(holder.pid) && !pidAlive(holder.pid)) {
60
+ return true;
61
+ }
62
+ if (retainLiveOwner)
63
+ return false;
64
+ const recordedAt = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
65
+ let stamp = recordedAt;
66
+ if (!Number.isFinite(stamp)) {
67
+ try {
68
+ stamp = statSync(lockPath).mtimeMs;
69
+ }
70
+ catch {
71
+ return false; // lock vanished — let the next openSync settle it
72
+ }
73
+ }
74
+ return Date.now() - stamp > LOCK_STALE_MS;
75
+ }
76
+ function sleepSync(ms) {
77
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
78
+ }
79
+ /**
80
+ * The default store: a thin, faithful pass-through to `node:fs`. Every method preserves
81
+ * the exact behavior the kernel had before the seam existed — the durable append
82
+ * (VOLTER_DURABLE), the atomic write (temp + rename), and the reclaimable cross-process
83
+ * file lock — so the existing twins and the conformance suite are unaffected.
84
+ */
85
+ export class FsWorldStore {
86
+ read(path) {
87
+ if (!existsSync(path))
88
+ return null;
89
+ return readFileSync(path, 'utf8');
90
+ }
91
+ readLines(path) {
92
+ if (!existsSync(path))
93
+ return [];
94
+ return readFileSync(path, 'utf8').split('\n');
95
+ }
96
+ readRange(path, start) {
97
+ let fd;
98
+ try {
99
+ fd = openSync(path, 'r');
100
+ }
101
+ catch (error) {
102
+ if (error.code === 'ENOENT')
103
+ return null;
104
+ throw error;
105
+ }
106
+ try {
107
+ const size = fstatSync(fd).size;
108
+ if (start >= size)
109
+ return '';
110
+ const buffer = Buffer.allocUnsafe(size - start);
111
+ let read = 0;
112
+ while (read < buffer.length) {
113
+ const n = readSync(fd, buffer, read, buffer.length - read, start + read);
114
+ if (n === 0)
115
+ break;
116
+ read += n;
117
+ }
118
+ return buffer.toString('utf8', 0, read);
119
+ }
120
+ finally {
121
+ closeSync(fd);
122
+ }
123
+ }
124
+ append(path, data) {
125
+ // Historical appendDurable: a completed append syscall survives a process crash;
126
+ // fsync additionally survives kernel-panic/power-loss when VOLTER_DURABLE=1.
127
+ const fd = openSync(path, 'a');
128
+ try {
129
+ appendFileSync(fd, data);
130
+ if (process.env.VOLTER_DURABLE === '1')
131
+ fsyncSync(fd);
132
+ }
133
+ finally {
134
+ closeSync(fd);
135
+ }
136
+ }
137
+ write(path, data, options = {}) {
138
+ if (!options.secret) {
139
+ mkdirSync(dirname(path), { recursive: true });
140
+ writeFileSync(path, data);
141
+ return;
142
+ }
143
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
144
+ writeFileSync(path, data, { mode: 0o600 });
145
+ chmodSync(path, 0o600);
146
+ }
147
+ writeAtomic(path, data, options = {}) {
148
+ mkdirSync(dirname(path), { recursive: true });
149
+ const tmp = `${path}.${process.pid}.${Date.now()}.${fsAtomicSequence++}.tmp`;
150
+ try {
151
+ writeFileSync(tmp, data, options.secret ? { mode: 0o600 } : undefined);
152
+ renameSync(tmp, path);
153
+ if (options.secret)
154
+ chmodSync(path, 0o600);
155
+ }
156
+ finally {
157
+ rmSync(tmp, { force: true });
158
+ }
159
+ }
160
+ exists(path) {
161
+ return existsSync(path);
162
+ }
163
+ remove(path) {
164
+ rmSync(path, { recursive: true, force: true });
165
+ }
166
+ mkdir(dirPath) {
167
+ mkdirSync(dirPath, { recursive: true });
168
+ }
169
+ list(dirPath) {
170
+ if (!existsSync(dirPath))
171
+ return [];
172
+ return readdirSync(dirPath);
173
+ }
174
+ canonicalPath(path) {
175
+ const absolute = resolve(path);
176
+ try {
177
+ return realpathSync(absolute);
178
+ }
179
+ catch (error) {
180
+ if (error.code !== 'ENOENT' || dirname(absolute) === absolute)
181
+ throw error;
182
+ return join(this.canonicalPath(dirname(absolute)), basename(absolute));
183
+ }
184
+ }
185
+ stat(path) {
186
+ if (!existsSync(path))
187
+ return null;
188
+ const s = statSync(path);
189
+ return { size: s.size, mtimeMs: s.mtimeMs, isDirectory: s.isDirectory(), ...(lstatSync(path).isSymbolicLink() ? { isSymbolicLink: true } : {}) };
190
+ }
191
+ /** Run `fn` holding an exclusive cross-process file lock. The lockfile records
192
+ * `{pid, hostname, at}`; a contender that finds a stale lock (dead pid or age >
193
+ * LOCK_STALE_MS) reclaims it by atomically renaming it aside — so a crashed holder
194
+ * can't wedge the world forever. Reclaim is race-safe: only the process that wins
195
+ * the rename clears the stale inode, and the exclusive `wx` create still decides the
196
+ * winner. (Moved verbatim from storage.ts `withFileLock`.) */
197
+ withLock(lockPath, fn, options = {}) {
198
+ mkdirSync(dirname(lockPath), { recursive: true });
199
+ const started = Date.now();
200
+ let fd = null;
201
+ while (fd === null) {
202
+ try {
203
+ fd = openSync(lockPath, 'wx');
204
+ }
205
+ catch (error) {
206
+ const code = error.code;
207
+ if (code !== 'EEXIST')
208
+ throw error;
209
+ if (lockIsStale(lockPath, options.retainLiveOwner)) {
210
+ // Serialize reclaimers, then re-read: a contender's earlier stale observation
211
+ // must never rename another contender's newly acquired, live lock.
212
+ const recoveryPath = `${lockPath}.recovery`;
213
+ let recovery;
214
+ try {
215
+ recovery = openSync(recoveryPath, 'wx');
216
+ }
217
+ catch (error) {
218
+ if (error.code !== 'EEXIST')
219
+ throw error;
220
+ if (Date.now() - started > 10_000)
221
+ throw new Error(`Lock recovery is active or interrupted: ${recoveryPath}. Inspect its owner before removing the recovery record.`);
222
+ sleepSync(25);
223
+ continue;
224
+ }
225
+ try {
226
+ writeFileSync(recovery, JSON.stringify({ pid: process.pid, hostname: hostname() }));
227
+ if (lockIsStale(lockPath, options.retainLiveOwner)) {
228
+ const holder = readLockHolder(lockPath);
229
+ const salvage = `${lockPath}.stale.${process.pid}.${Date.now()}`;
230
+ try {
231
+ renameSync(lockPath, salvage);
232
+ console.warn(`[world] reclaiming stale storage lock ${lockPath} (held by pid ${holder?.pid ?? '?'} on ${holder?.hostname ?? '?'})`);
233
+ unlinkSync(salvage);
234
+ }
235
+ catch (error) {
236
+ if (error.code !== 'ENOENT')
237
+ throw error;
238
+ }
239
+ }
240
+ }
241
+ finally {
242
+ closeSync(recovery);
243
+ unlinkSync(recoveryPath);
244
+ }
245
+ continue;
246
+ }
247
+ if (Date.now() - started > 10_000) {
248
+ const holder = readLockHolder(lockPath);
249
+ const since = holder && typeof holder.at === 'string' ? Date.parse(holder.at) : NaN;
250
+ 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` : ''})` : ''}`);
251
+ }
252
+ sleepSync(25);
253
+ }
254
+ }
255
+ // Record the holder so a later contender can detect staleness. Best-effort: a write
256
+ // failure here doesn't weaken exclusivity, only staleness diagnostics.
257
+ try {
258
+ writeFileSync(fd, `${JSON.stringify({ pid: process.pid, hostname: hostname(), at: new Date().toISOString() })}\n`);
259
+ }
260
+ catch { /* ignore */ }
261
+ const acquired = Date.now();
262
+ try {
263
+ return fn();
264
+ }
265
+ finally {
266
+ // Every World on the machine waits on some of these locks; a long hold stalls them all.
267
+ const heldMs = Date.now() - acquired;
268
+ if (heldMs > 2_000)
269
+ console.warn(`[world] held storage lock ${lockPath} for ${heldMs}ms (pid ${process.pid})\n${new Error().stack?.split('\n').slice(2, 8).join('\n') ?? ''}`);
270
+ const held = fstatSync(fd);
271
+ closeSync(fd);
272
+ try {
273
+ const current = lstatSync(lockPath);
274
+ if (held.dev === current.dev && held.ino === current.ino)
275
+ unlinkSync(lockPath);
276
+ }
277
+ catch { /* already reclaimed by a stale-lock sweep */ }
278
+ }
279
+ }
280
+ }
281
+ // ── MemoryWorldStore — a Map-backed store; the proof a non-fs transport works ─────────
282
+ // A process-global, strictly-increasing stamp source shared by every MemoryWorldStore, so
283
+ // no two writes (across instances or requests) ever collide on `stat().mtimeMs`. See `bump`.
284
+ let memoryStoreClock = 0;
285
+ const byteLength = (text) => new TextEncoder().encode(text).length;
286
+ /**
287
+ * An in-memory store. Files are entries in a `Map<path, string>`; directories are
288
+ * synthesized from the key set (a path is a directory when it is a strict prefix of a
289
+ * file key). `withLock` is a synchronous pass-through — a single in-process instance has
290
+ * no concurrent writers to serialize, so the read-then-append critical sections the
291
+ * kernel guards are already atomic under the single-threaded event loop.
292
+ *
293
+ * This is the portability proof: point the active store here and the entire kernel — and
294
+ * the clerk twin on top of it — runs with the filesystem untouched.
295
+ */
296
+ export class MemoryWorldStore {
297
+ /** path → content, and path → version stamp driving `stat().mtimeMs`. */
298
+ files = new Map();
299
+ versions = new Map();
300
+ /** path → UTF-8 byte size, kept as content changes so `stat` never re-encodes a growing log. */
301
+ sizes = new Map();
302
+ /** The stamp of this store's most recent mutation (0 = never mutated). A serverless
303
+ * entry snapshots this before running the sync kernel and flushes ONLY when it moved —
304
+ * read-only requests (the common case) then skip the durable write entirely. */
305
+ lastMutation = 0;
306
+ mutationStamp() {
307
+ return this.lastMutation;
308
+ }
309
+ /** Paths whose content changed strictly after `stamp` — the DELTA-FLUSH primitive
310
+ * (runtime contract R11b): a serverless entry records `mutationStamp()` at hydration,
311
+ * runs the sync kernel, then persists ONLY these paths (plus removals, which the caller
312
+ * detects by diffing its hydrated path set against `entries()` — a removed path has no
313
+ * version to report). O(paths) scan, O(delta) write. */
314
+ changedSince(stamp) {
315
+ const changed = [];
316
+ for (const [path, version] of this.versions) {
317
+ if (version > stamp && this.files.has(path))
318
+ changed.push(path);
319
+ }
320
+ return changed.sort();
321
+ }
322
+ bump(path) {
323
+ // Draw the stamp from a PROCESS-GLOBAL monotonic counter, not a per-instance one.
324
+ // storage.ts's append-dedupe cache is keyed by path and validated by (size, mtimeMs),
325
+ // and it persists across requests in a Worker isolate while a fresh MemoryWorldStore is
326
+ // created per request (the hydrate→sync-kernel→flush shape). A per-instance counter
327
+ // resets to 0 each request, so two instances could present the SAME (size, mtimeMs) for
328
+ // one path with DIFFERENT content — serving a stale index (dedupe bypass) or a false
329
+ // conflict. A global counter makes every write across every instance a distinct,
330
+ // strictly-increasing stamp, so a cross-instance collision is impossible.
331
+ this.lastMutation = memoryStoreClock += 1;
332
+ this.versions.set(path, this.lastMutation);
333
+ }
334
+ read(path) {
335
+ return this.files.has(path) ? this.files.get(path) : null;
336
+ }
337
+ readLines(path) {
338
+ const content = this.files.get(path);
339
+ return content === undefined ? [] : content.split('\n');
340
+ }
341
+ append(path, data) {
342
+ this.files.set(path, (this.files.get(path) ?? '') + data);
343
+ this.sizes.set(path, (this.sizes.get(path) ?? 0) + byteLength(data));
344
+ this.bump(path);
345
+ }
346
+ write(path, data) {
347
+ this.files.set(path, data);
348
+ this.sizes.set(path, byteLength(data));
349
+ this.bump(path);
350
+ }
351
+ writeAtomic(path, data) {
352
+ // Atomic by nature in a single process: the assignment is indivisible, so no reader
353
+ // ever observes a partial document.
354
+ this.files.set(path, data);
355
+ this.sizes.set(path, byteLength(data));
356
+ this.bump(path);
357
+ }
358
+ exists(path) {
359
+ if (this.files.has(path))
360
+ return true;
361
+ // A directory "exists" when some file lives under it.
362
+ const prefix = path.endsWith('/') ? path : `${path}/`;
363
+ for (const key of this.files.keys())
364
+ if (key.startsWith(prefix))
365
+ return true;
366
+ return false;
367
+ }
368
+ remove(path) {
369
+ let removedAny = false;
370
+ if (this.files.delete(path)) {
371
+ this.versions.delete(path);
372
+ this.sizes.delete(path);
373
+ removedAny = true;
374
+ }
375
+ const prefix = path.endsWith('/') ? path : `${path}/`;
376
+ for (const key of [...this.files.keys()]) {
377
+ if (key.startsWith(prefix)) {
378
+ this.files.delete(key);
379
+ this.versions.delete(key);
380
+ this.sizes.delete(key);
381
+ removedAny = true;
382
+ }
383
+ }
384
+ // A pure removal is a mutation too: without this, a request that only deletes state
385
+ // would read as clean and the delta-flush would never persist the removal (R11b).
386
+ if (removedAny)
387
+ this.lastMutation = memoryStoreClock += 1;
388
+ }
389
+ mkdir(_dirPath) {
390
+ // Directories are implicit in the key set; nothing to do.
391
+ }
392
+ list(dirPath) {
393
+ const prefix = dirPath.endsWith('/') ? dirPath : `${dirPath}/`;
394
+ const children = new Set();
395
+ for (const key of this.files.keys()) {
396
+ if (!key.startsWith(prefix))
397
+ continue;
398
+ const rest = key.slice(prefix.length);
399
+ const slash = rest.indexOf('/');
400
+ children.add(slash === -1 ? rest : rest.slice(0, slash));
401
+ }
402
+ return [...children];
403
+ }
404
+ stat(path) {
405
+ const content = this.files.get(path);
406
+ if (content !== undefined) {
407
+ return { size: this.sizes.get(path) ?? byteLength(content), mtimeMs: this.versions.get(path) ?? 0, isDirectory: false };
408
+ }
409
+ if (this.exists(path)) {
410
+ return { size: 0, mtimeMs: 0, isDirectory: true };
411
+ }
412
+ return null;
413
+ }
414
+ withLock(_lockPath, fn) {
415
+ return fn();
416
+ }
417
+ /** Test/inspection helper: a plain snapshot of every stored file. Not part of the
418
+ * `WorldStore` contract — used by `flushFrom` and by tests asserting no fs was touched. */
419
+ entries() {
420
+ return Object.fromEntries(this.files);
421
+ }
422
+ }
423
+ // ── the active store (the injection point) ───────────────────────────────────────────
424
+ let activeStore = new FsWorldStore();
425
+ const scope = (() => {
426
+ try {
427
+ const hooks = globalThis.process?.getBuiltinModule?.('node:async_hooks');
428
+ return hooks?.AsyncLocalStorage ? new hooks.AsyncLocalStorage() : null;
429
+ }
430
+ catch {
431
+ return null;
432
+ }
433
+ })();
434
+ /** The store the kernel currently persists through: the request's scoped store, else the
435
+ * module-global one (`FsWorldStore` by default). */
436
+ export function getActiveWorldStore() {
437
+ return scope?.getStore() ?? activeStore;
438
+ }
439
+ /** Swap the active store. A serverless/DO/test entry sets a `MemoryWorldStore` (or a
440
+ * backend-backed store) here before running the sync kernel, and restores the previous
441
+ * one afterwards. Returns the store that was active, so callers can restore it. */
442
+ export function setActiveWorldStore(store) {
443
+ const previous = activeStore;
444
+ activeStore = store;
445
+ return previous;
446
+ }
447
+ export function withWorldStore(store, fn) {
448
+ if (scope)
449
+ return scope.run(store, fn);
450
+ const previous = setActiveWorldStore(store);
451
+ let result;
452
+ try {
453
+ result = fn();
454
+ }
455
+ catch (error) {
456
+ setActiveWorldStore(previous);
457
+ throw error;
458
+ }
459
+ if (result instanceof Promise) {
460
+ return result.finally(() => setActiveWorldStore(previous));
461
+ }
462
+ setActiveWorldStore(previous);
463
+ return result;
464
+ }
465
+ /** Fill `store` from an async durable `source` before the sync kernel runs. */
466
+ export async function hydrateInto(store, source) {
467
+ const snapshot = await source.load();
468
+ for (const [path, content] of Object.entries(snapshot))
469
+ store.write(path, content);
470
+ }
471
+ /** Persist `store`'s current contents back to an async durable `sink` after the sync
472
+ * kernel has run. */
473
+ export async function flushFrom(store, sink) {
474
+ await sink.save(store.entries());
475
+ }
@@ -0,0 +1,9 @@
1
+ export type WorldServiceConfig = {
2
+ provider?: string;
3
+ tool?: string;
4
+ pollLimit?: number;
5
+ } & Record<string, unknown>;
6
+ export type WorldConfig = {
7
+ services: Record<string, WorldServiceConfig>;
8
+ } & Record<string, unknown>;
9
+ export declare function loadWorldConfig(root?: string): WorldConfig;
@@ -0,0 +1,17 @@
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.js";
9
+ import { getActiveWorldStore } from "./world-store.js";
10
+ export function loadWorldConfig(root) {
11
+ const path = join(worldStateRoot(root), 'config.json');
12
+ const raw = getActiveWorldStore().read(path);
13
+ if (raw === null)
14
+ return { services: {} };
15
+ const parsed = JSON.parse(raw);
16
+ return { ...parsed, services: parsed.services ?? {} };
17
+ }