narraeon 0.6.0 → 0.6.2

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 (48) hide show
  1. package/README.md +11 -0
  2. package/README.zh-CN.md +8 -0
  3. package/dist/node/protocol/v1.js +12 -2
  4. package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
  5. package/dist/node/runtime/V1Runtime.js +5 -5
  6. package/dist/node/runtime/archive/WorldArchives.js +14 -46
  7. package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
  8. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
  9. package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
  10. package/dist/node/runtime/authoring/ModelToolConversation.js +25 -2
  11. package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
  12. package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
  13. package/dist/node/runtime/config/dataRootLock.js +108 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +0 -10
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
  16. package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
  17. package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
  18. package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
  19. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
  20. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
  21. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
  22. package/dist/node/runtime/play/PlayCallChain.js +338 -366
  23. package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
  24. package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
  25. package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
  26. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +39 -7
  27. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
  28. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +14 -6
  29. package/dist/node/runtime/setting/SettingImprovementSession.js +39 -25
  30. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
  31. package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
  32. package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
  33. package/dist/node/runtime/world/WorldActivity.js +14 -22
  34. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
  35. package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
  36. package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
  37. package/dist/node/runtime/world/WorldQueue.js +46 -0
  38. package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
  39. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -13
  40. package/dist/node/runtime/world-revision/WorldRevisionSession.js +29 -2
  41. package/dist/node/server/mobile.js +52 -0
  42. package/dist/node/server/startWebServer.js +15 -8
  43. package/dist/web/assets/index-CXnFaYK9.js +1323 -0
  44. package/dist/web/assets/{index-BJ-1Mb0e.css → index-LPMV0pl3.css} +1 -1
  45. package/dist/web/index.html +2 -2
  46. package/package.json +2 -1
  47. package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
  48. package/dist/web/assets/index-BgBL8G-A.js +0 -1323
@@ -1,11 +1,15 @@
1
1
  import { isHistoryBackground, } from "../history/HistoryBackground.js";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { link, mkdir, open, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
5
  import { isDeepStrictEqual } from "node:util";
6
- import { cloneFilePhysically } from "../FileNativePhysicalClone.js";
7
6
  export const continuityHeadFile = "continuity-head.json";
8
7
  export const authorityV3Directory = "authority-v3";
8
+ /**
9
+ * Stale files that v0.3.0–v0.6.0 worlds still carry from the layout they
10
+ * replaced. The conversion to the turn ledger removes them; nothing reads them.
11
+ */
12
+ export const staleV3WorldFiles = [join("runtime", "materialized-head.json")];
9
13
  export class FileNativeAuthorityV3Error extends Error {
10
14
  code;
11
15
  constructor(code, message) {
@@ -15,6 +19,10 @@ export class FileNativeAuthorityV3Error extends Error {
15
19
  }
16
20
  }
17
21
  /**
22
+ * Read-only access to the retired Authority V3 fact chain. It exists only so
23
+ * the storage migrator can convert a V3 world into the current layout; nothing
24
+ * on the live path writes V3 facts.
25
+ *
18
26
  * World-neutral immutable Authority facts with one target-local mutable head.
19
27
  *
20
28
  * Every sequence owns an immutable epoch directory. A fork can therefore
@@ -22,7 +30,7 @@ export class FileNativeAuthorityV3Error extends Error {
22
30
  * ancestor facts. Object references include their creation epoch so unchanged
23
31
  * state/history/material objects remain directly addressable after a fork.
24
32
  */
25
- export class FileNativeAuthorityV3 {
33
+ export class LegacyAuthorityV3Reader {
26
34
  #runtimeRoot;
27
35
  #authorityRoot;
28
36
  constructor(worldRoot) {
@@ -40,57 +48,6 @@ export class FileNativeAuthorityV3 {
40
48
  throw error;
41
49
  }
42
50
  }
43
- async initialize(input) {
44
- const epoch = 0;
45
- const state = await this.#writeStateManifest(epoch, input.state);
46
- const historySegment = {
47
- schemaVersion: 3,
48
- type: "file_native_history_segment",
49
- previous: null,
50
- messages: structuredClone(input.history),
51
- };
52
- assertHistorySegment(historySegment);
53
- assertGenesisHistory(historySegment.messages);
54
- const history = input.history.length === 0
55
- ? null
56
- : await this.#writeObject(epoch, "history.json", historySegment);
57
- const materialSet = {
58
- schemaVersion: 3,
59
- type: "file_native_material_set",
60
- items: structuredClone(input.additionalMaterials),
61
- };
62
- assertMaterialSet(materialSet);
63
- const materials = await this.#writeObject(epoch, "materials.json", materialSet);
64
- const endpointValue = {
65
- schemaVersion: 3,
66
- type: "file_native_authority_endpoint",
67
- state,
68
- history,
69
- historyLength: input.history.length,
70
- materials,
71
- };
72
- assertEndpoint(endpointValue);
73
- const endpoint = await this.#writeObject(epoch, "endpoint.json", endpointValue);
74
- const genesisValue = {
75
- schemaVersion: 3,
76
- type: "file_native_authority_genesis",
77
- result: endpoint,
78
- };
79
- assertGenesis(genesisValue);
80
- const genesis = await this.#writeObject(epoch, "authority.json", genesisValue);
81
- const head = {
82
- schemaVersion: 3,
83
- type: "file_native_continuity_head",
84
- head: "genesis",
85
- sequence: 0,
86
- genesisDigest: genesis.digest,
87
- commitDigest: null,
88
- result: endpoint,
89
- operationId: input.operationId,
90
- };
91
- await publishJson(join(this.#runtimeRoot, continuityHeadFile), head);
92
- return head;
93
- }
94
51
  async readHead() {
95
52
  const value = await readJson(join(this.#runtimeRoot, continuityHeadFile));
96
53
  assertHead(value);
@@ -99,143 +56,6 @@ export class FileNativeAuthorityV3 {
99
56
  async currentOperationId() {
100
57
  return (await this.readHead()).operationId;
101
58
  }
102
- async discardUnacceptedNextEpoch() {
103
- const current = await this.readHead();
104
- const epochsRoot = join(this.#authorityRoot, "epochs");
105
- await rm(this.#epochRoot(current.sequence + 1), {
106
- recursive: true,
107
- force: true,
108
- });
109
- await syncDirectory(epochsRoot);
110
- }
111
- async prepareAppend(input) {
112
- const current = await this.readHead();
113
- if (current.head !== input.parentHead)
114
- throw new FileNativeAuthorityV3Error("stale", "The parent endpoint of the Authority operation has changed");
115
- const sequence = current.sequence + 1;
116
- const head = `commit:${sequence}`;
117
- const basisHead = input.timelineParentHead ?? input.parentHead;
118
- const basisResult = await this.#resultRefAt(basisHead);
119
- const basis = await this.#readEndpoint(basisResult);
120
- const state = await this.#nextState(sequence, basis.state, input.stateChanges);
121
- const historyAppend = input.historyAppend.map((message, index) => ({
122
- messageId: `message.${sequence}.${index + 1}.${message.role}`,
123
- ...structuredClone(message),
124
- }));
125
- const history = historyAppend.length === 0
126
- ? basis.history
127
- : await this.#writeHistorySegment(sequence, {
128
- schemaVersion: 3,
129
- type: "file_native_history_segment",
130
- previous: basis.history,
131
- messages: historyAppend,
132
- });
133
- const basisMaterials = await this.#readMaterials(basis.materials);
134
- const nextMaterialSet = {
135
- schemaVersion: 3,
136
- type: "file_native_material_set",
137
- items: structuredClone(input.nextMaterials),
138
- };
139
- assertMaterialSet(nextMaterialSet);
140
- const materials = isDeepStrictEqual(basisMaterials.items, input.nextMaterials)
141
- ? basis.materials
142
- : await this.#writeObject(sequence, "materials.json", nextMaterialSet);
143
- const endpointValue = {
144
- schemaVersion: 3,
145
- type: "file_native_authority_endpoint",
146
- state,
147
- history,
148
- historyLength: basis.historyLength + historyAppend.length,
149
- materials,
150
- ...(input.narrativeCheckpoint === undefined
151
- ? basis.narrativeCheckpoint === undefined
152
- ? {}
153
- : { narrativeCheckpoint: basis.narrativeCheckpoint }
154
- : {
155
- narrativeCheckpoint: {
156
- ...input.narrativeCheckpoint,
157
- head,
158
- historyMessageId: historyAppend.at(-1)?.messageId ?? "",
159
- },
160
- }),
161
- };
162
- assertEndpoint(endpointValue);
163
- const endpoint = await this.#writeObject(sequence, "endpoint.json", endpointValue);
164
- const storedChanges = [];
165
- for (const change of input.stateChanges) {
166
- const blob = await this.#writeBlob(sequence, change.canonicalNextBytes);
167
- storedChanges.push({
168
- kind: change.kind,
169
- documentId: change.documentId,
170
- stableShortRef: change.stableShortRef,
171
- relativePath: change.relativePath,
172
- codec: change.codec,
173
- expectedPreviousHash: change.expectedPreviousHash,
174
- nextHash: change.nextHash,
175
- nextBlob: blob,
176
- });
177
- }
178
- const auditParent = {
179
- head: current.head,
180
- digest: current.commitDigest ?? current.genesisDigest,
181
- };
182
- const timelineParent = await this.#authorityRefAt(basisHead);
183
- const commit = {
184
- ...(input.playContext === undefined
185
- ? {}
186
- : { playContext: input.playContext }),
187
- ...(input.worldClock === undefined
188
- ? {}
189
- : { worldClock: input.worldClock }),
190
- schemaVersion: 3,
191
- type: "file_native_authority_commit",
192
- sequence,
193
- head,
194
- auditParent,
195
- timelineParent,
196
- mode: input.mode,
197
- historyAppend,
198
- stateChanges: storedChanges,
199
- nextAdditionalMaterials: structuredClone(input.nextMaterials),
200
- result: endpoint,
201
- ...(input.narrativeCheckpoint === undefined
202
- ? {}
203
- : { narrativeCheckpoint: structuredClone(input.narrativeCheckpoint) }),
204
- ...(input.correctionTargets === undefined
205
- ? {}
206
- : { correctionTargets: structuredClone(input.correctionTargets) }),
207
- ...(input.corrects === undefined ? {} : { corrects: input.corrects }),
208
- ...(input.timelineRevision === undefined
209
- ? {}
210
- : { timelineRevision: structuredClone(input.timelineRevision) }),
211
- };
212
- assertCommit(commit);
213
- const authority = await this.#writeObject(sequence, "authority.json", commit);
214
- return {
215
- commit,
216
- commitDigest: authority.digest,
217
- nextHead: {
218
- schemaVersion: 3,
219
- type: "file_native_continuity_head",
220
- head,
221
- sequence,
222
- genesisDigest: current.genesisDigest,
223
- commitDigest: authority.digest,
224
- result: endpoint,
225
- operationId: input.operationId,
226
- },
227
- stateChanges: structuredClone(input.stateChanges),
228
- };
229
- }
230
- async publishPrepared(prepared) {
231
- const current = await this.readHead();
232
- if (current.sequence + 1 !== prepared.nextHead.sequence ||
233
- current.head !== prepared.commit.auditParent.head ||
234
- (current.commitDigest ?? current.genesisDigest) !==
235
- prepared.commit.auditParent.digest)
236
- throw new FileNativeAuthorityV3Error("stale", "The prepared Authority fact no longer extends the current endpoint");
237
- await publishJson(join(this.#runtimeRoot, continuityHeadFile), prepared.nextHead);
238
- }
239
59
  async recover(requestedHead) {
240
60
  const current = await this.readHead();
241
61
  const head = requestedHead ?? current.head;
@@ -350,54 +170,6 @@ export class FileNativeAuthorityV3 {
350
170
  await this.#assertAcceptedAuthorityRef(current, sequence, head, digest);
351
171
  return { commit: value, digest };
352
172
  }
353
- async clonePrefixTo(input) {
354
- const sourceHead = await this.readHead();
355
- const selectedSequence = input.selectedHead === "genesis"
356
- ? 0
357
- : parseHeadSequence(input.selectedHead);
358
- if (selectedSequence > sourceHead.sequence)
359
- throw new FileNativeAuthorityV3Error("conflict", `Cannot fork a nonexistent Authority endpoint: ${input.selectedHead}`);
360
- const selected = input.selectedHead === sourceHead.head
361
- ? { ...structuredClone(sourceHead), operationId: input.operationId }
362
- : selectedSequence === 0
363
- ? await this.#headAtGenesis(sourceHead, input.operationId)
364
- : await this.#headAtCommit(sourceHead, selectedSequence, input.operationId);
365
- const targetRuntime = join(input.targetWorldRoot, "runtime");
366
- const targetAuthority = join(targetRuntime, authorityV3Directory);
367
- for (let epoch = 0; epoch <= selectedSequence; epoch += 1)
368
- await cloneImmutableTree(this.#epochRoot(epoch), join(targetAuthority, "epochs", epochName(epoch)));
369
- await publishJson(join(targetRuntime, continuityHeadFile), selected);
370
- return selected;
371
- }
372
- async #headAtGenesis(current, operationId) {
373
- const genesis = await this.#readObject({ epoch: 0, digest: current.genesisDigest }, "authority.json", "file_native_authority_genesis");
374
- assertGenesis(genesis);
375
- await this.#assertAcceptedGenesis(current);
376
- return {
377
- schemaVersion: 3,
378
- type: "file_native_continuity_head",
379
- head: "genesis",
380
- sequence: 0,
381
- genesisDigest: current.genesisDigest,
382
- commitDigest: null,
383
- result: genesis.result,
384
- operationId,
385
- };
386
- }
387
- async #headAtCommit(current, sequence, operationId) {
388
- const { value, digest } = await this.#readAuthorityCommit(sequence);
389
- await this.#assertAcceptedAuthorityRef(current, sequence, value.head, digest);
390
- return {
391
- schemaVersion: 3,
392
- type: "file_native_continuity_head",
393
- head: value.head,
394
- sequence,
395
- genesisDigest: current.genesisDigest,
396
- commitDigest: digest,
397
- result: value.result,
398
- operationId,
399
- };
400
- }
401
173
  async #resultRefAt(head) {
402
174
  const current = await this.readHead();
403
175
  if (head === current.head) {
@@ -431,19 +203,6 @@ export class FileNativeAuthorityV3 {
431
203
  await this.#assertAcceptedAuthorityRef(current, sequence, head, digest);
432
204
  return value.result;
433
205
  }
434
- async #authorityRefAt(head) {
435
- const current = await this.readHead();
436
- if (head === "genesis") {
437
- await this.#assertAcceptedGenesis(current);
438
- return { head, digest: current.genesisDigest };
439
- }
440
- const sequence = parseHeadSequence(head);
441
- const { value, digest } = await this.#readAuthorityCommit(sequence);
442
- if (value.head !== head || sequence > current.sequence)
443
- throw new FileNativeAuthorityV3Error("conflict", `Authority endpoint does not exist: ${head}`);
444
- await this.#assertAcceptedAuthorityRef(current, sequence, head, digest);
445
- return { head, digest };
446
- }
447
206
  async #assertAcceptedAuthorityRef(current, sequence, head, digest) {
448
207
  if (sequence > current.sequence)
449
208
  throw new FileNativeAuthorityV3Error("conflict", `Authority endpoint does not exist: ${head}`);
@@ -464,60 +223,6 @@ export class FileNativeAuthorityV3 {
464
223
  first.auditParent.digest !== current.genesisDigest)
465
224
  throw new FileNativeAuthorityV3Error("corrupt", "Authority audit chain does not extend its genesis anchor");
466
225
  }
467
- async #nextState(epoch, basisRef, changes) {
468
- if (changes.length === 0)
469
- return basisRef;
470
- const basis = await this.#readStateManifest(basisRef);
471
- const files = new Map(basis.files.map((file) => [file.path, file]));
472
- for (const change of changes) {
473
- const previous = files.get(change.relativePath);
474
- if ((previous?.sha256 ?? null) !== change.expectedPreviousHash)
475
- throw new FileNativeAuthorityV3Error("conflict", `State-change previous hash conflicts: ${change.relativePath}`);
476
- if (hashWithPrefix(change.canonicalNextBytes) !== change.nextHash)
477
- throw new FileNativeAuthorityV3Error("conflict", `State-change next hash conflicts: ${change.relativePath}`);
478
- const blob = await this.#writeBlob(epoch, change.canonicalNextBytes);
479
- files.set(change.relativePath, {
480
- path: change.relativePath,
481
- sha256: change.nextHash,
482
- blob,
483
- });
484
- }
485
- const manifest = {
486
- schemaVersion: 3,
487
- type: "file_native_state_manifest",
488
- files: [...files.values()].sort((left, right) => left.path.localeCompare(right.path)),
489
- };
490
- assertStateManifest(manifest);
491
- return this.#writeObject(epoch, "state.json", manifest);
492
- }
493
- async #writeStateManifest(epoch, files) {
494
- const entries = [];
495
- for (const file of files) {
496
- const blob = await this.#writeBlob(epoch, file.contents);
497
- entries.push({
498
- path: file.path,
499
- sha256: hashWithPrefix(file.contents),
500
- blob,
501
- });
502
- }
503
- const manifest = {
504
- schemaVersion: 3,
505
- type: "file_native_state_manifest",
506
- files: entries.sort((left, right) => left.path.localeCompare(right.path)),
507
- };
508
- assertStateManifest(manifest);
509
- return this.#writeObject(epoch, "state.json", manifest);
510
- }
511
- async #writeHistorySegment(epoch, segment) {
512
- assertHistorySegment(segment);
513
- return this.#writeObject(epoch, "history.json", segment);
514
- }
515
- async #writeBlob(epoch, contents) {
516
- const digest = hashHex(contents);
517
- const path = join(this.#epochRoot(epoch), "blobs", `${digest}.txt`);
518
- await publishImmutableText(path, contents);
519
- return { epoch, digest, sha256: `sha256:${digest}` };
520
- }
521
226
  async #recoverState(ref) {
522
227
  const manifest = await this.#readStateManifest(ref);
523
228
  const state = [];
@@ -568,7 +273,6 @@ export class FileNativeAuthorityV3 {
568
273
  return value;
569
274
  }
570
275
  async #readAuthorityCommit(sequence) {
571
- forbidAuthorityFactDecodeForTest();
572
276
  const path = join(this.#epochRoot(sequence), "authority.json");
573
277
  const value = await readJson(path);
574
278
  if (!isRecord(value) ||
@@ -579,14 +283,7 @@ export class FileNativeAuthorityV3 {
579
283
  const commit = value;
580
284
  return { value: commit, digest: objectDigest(commit) };
581
285
  }
582
- async #writeObject(epoch, name, value) {
583
- const digest = objectDigest(value);
584
- await publishImmutableJson(join(this.#epochRoot(epoch), name), value);
585
- return { epoch, digest };
586
- }
587
286
  async #readObject(ref, name, type) {
588
- if (name === "authority.json")
589
- forbidAuthorityFactDecodeForTest();
590
287
  const value = await readJson(join(this.#epochRoot(ref.epoch), name));
591
288
  if (!isRecord(value) ||
592
289
  value.schemaVersion !== 3 ||
@@ -667,15 +364,6 @@ function assertHistorySegment(value) {
667
364
  !value.messages.every(isHistoryMessage))
668
365
  throw corruptShape("Authority history segment");
669
366
  }
670
- function assertGenesisHistory(messages) {
671
- for (const [index, message] of messages.entries()) {
672
- const expected = index === 0 && message.role === "narrator"
673
- ? "message.genesis.narrator"
674
- : `message.genesis.${index + 1}.${message.role}`;
675
- if (message.messageId !== expected)
676
- throw corruptShape("Authority genesis history-message identity");
677
- }
678
- }
679
367
  function assertMaterialSet(value) {
680
368
  if (!isRecord(value) ||
681
369
  !hasExactKeys(value, ["schemaVersion", "type", "items"]) ||
@@ -971,81 +659,6 @@ function hashWithPrefix(value) {
971
659
  function isDigest(value) {
972
660
  return /^[a-f0-9]{64}$/u.test(value);
973
661
  }
974
- async function cloneImmutableTree(source, target) {
975
- await mkdir(target, { recursive: true, mode: 0o700 });
976
- const entries = await readdir(source, { withFileTypes: true });
977
- for (const entry of entries) {
978
- const sourcePath = join(source, entry.name);
979
- const targetPath = join(target, entry.name);
980
- if (entry.isDirectory()) {
981
- await cloneImmutableTree(sourcePath, targetPath);
982
- continue;
983
- }
984
- if (!entry.isFile())
985
- throw new FileNativeAuthorityV3Error("corrupt", `Immutable Authority closure contains a non-file entry: ${entry.name}`);
986
- await cloneImmutableFile(sourcePath, targetPath);
987
- }
988
- }
989
- async function cloneImmutableFile(source, target) {
990
- await cloneFilePhysically({ source, target, immutable: true });
991
- }
992
- async function publishImmutableJson(path, value) {
993
- await publishImmutableText(path, `${JSON.stringify(value, null, 2)}\n`);
994
- }
995
- async function publishImmutableText(path, contents) {
996
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
997
- const temporary = `${path}.${randomUUID()}.tmp`;
998
- try {
999
- await writeFile(temporary, contents, {
1000
- encoding: "utf8",
1001
- flag: "wx",
1002
- mode: 0o600,
1003
- });
1004
- await syncFile(temporary);
1005
- try {
1006
- await link(temporary, path);
1007
- await syncDirectory(dirname(path));
1008
- }
1009
- catch (error) {
1010
- if (!isNodeError(error) || error.code !== "EEXIST")
1011
- throw error;
1012
- if ((await readFile(path, "utf8")) !== contents)
1013
- throw new FileNativeAuthorityV3Error("corrupt", `Immutable Authority object conflicts: ${path}`);
1014
- }
1015
- }
1016
- finally {
1017
- await rm(temporary, { force: true });
1018
- }
1019
- }
1020
- async function publishJson(path, value) {
1021
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
1022
- const temporary = `${path}.${randomUUID()}.tmp`;
1023
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
1024
- encoding: "utf8",
1025
- mode: 0o600,
1026
- });
1027
- await syncFile(temporary);
1028
- await rename(temporary, path);
1029
- await syncDirectory(dirname(path));
1030
- }
1031
- async function syncFile(path) {
1032
- const handle = await open(path, "r");
1033
- try {
1034
- await handle.sync();
1035
- }
1036
- finally {
1037
- await handle.close();
1038
- }
1039
- }
1040
- async function syncDirectory(path) {
1041
- const handle = await open(path, "r");
1042
- try {
1043
- await handle.sync();
1044
- }
1045
- finally {
1046
- await handle.close();
1047
- }
1048
- }
1049
662
  async function readJson(path) {
1050
663
  return JSON.parse(await readFile(path, "utf8"));
1051
664
  }
@@ -1055,7 +668,3 @@ function isRecord(value) {
1055
668
  function isNodeError(error) {
1056
669
  return error instanceof Error && "code" in error;
1057
670
  }
1058
- function forbidAuthorityFactDecodeForTest() {
1059
- if (process.env.NARRAEON_INTERNAL_TEST_FORBID_AUTHORITY_FACT_DECODE === "1")
1060
- throw new Error("Authority fact decoding is forbidden by this test");
1061
- }
@@ -1,16 +1,26 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { FileNativeAuthorityV3 } from "./FileNativeAuthorityV3.js";
4
+ import { WorldLedgerAuthority } from "./WorldLedgerAuthority.js";
5
5
  import { fileNativeHistoryMessageIdFromProjectionPath } from "./FileNativeHistoryProjection.js";
6
+ /** Listing never upgrades storage: a world without a ledger shows as pending. */
7
+ export const pendingUpgradeActivity = {
8
+ lastActivityAt: null,
9
+ excerpt: null,
10
+ playerTurns: 0,
11
+ pendingUpgrade: true,
12
+ };
6
13
  /** Read only the small head/cache on a hit; repair misses from the local projection. */
7
14
  export async function readWorldActivity(root, serializeRebuild = (rebuild) => rebuild()) {
8
- const head = await currentHead(root);
15
+ const ledger = new WorldLedgerAuthority(root);
16
+ if (!(await ledger.exists()))
17
+ return pendingUpgradeActivity;
18
+ const head = (await ledger.readHead()).head;
9
19
  const cached = await readCache(root);
10
20
  if (cached?.head === head)
11
21
  return cached;
12
22
  return serializeRebuild(async () => {
13
- const latestHead = await currentHead(root);
23
+ const latestHead = (await ledger.readHead()).head;
14
24
  const latestCache = await readCache(root);
15
25
  if (latestCache?.head === latestHead)
16
26
  return latestCache;
@@ -21,11 +31,7 @@ export async function readWorldActivity(root, serializeRebuild = (rebuild) => re
21
31
  excerpt: null,
22
32
  playerTurns: 0,
23
33
  };
24
- // An accepted endpoint may still be awaiting recovery. Never label its old
25
- // materialized history as a valid cache for the new endpoint.
26
- const materialized = await readJson(join(root, "runtime/materialized-head.json"));
27
- if (!isRecord(materialized) || materialized.head !== latestHead)
28
- return empty;
34
+ // history/ is the current endpoint's own projection, so it is always valid to rebuild from.
29
35
  const entries = await readdir(join(root, "history"), {
30
36
  withFileTypes: true,
31
37
  });
@@ -73,20 +79,6 @@ function excerpt(text) {
73
79
  ? null
74
80
  : Array.from(singleLine).slice(0, 80).join("");
75
81
  }
76
- async function currentHead(root) {
77
- try {
78
- return (await new FileNativeAuthorityV3(root).readHead()).head;
79
- }
80
- catch (error) {
81
- if (!isMissing(error))
82
- throw error;
83
- // Listing a released layout must not trigger an Authority migration/replay.
84
- const checkpoint = await readJson(join(root, "runtime/materialized-head.json"));
85
- return isRecord(checkpoint) && typeof checkpoint.head === "string"
86
- ? checkpoint.head
87
- : "genesis";
88
- }
89
- }
90
82
  async function readCache(root) {
91
83
  const value = await readJson(join(root, "runtime/activity.json")).catch(() => null);
92
84
  return isRecord(value) &&
@@ -14,7 +14,7 @@ export async function readDocumentMaintenanceFacts(authority, head, cache) {
14
14
  throw new Error("Document maintenance history is not reachable");
15
15
  seen.add(cursor);
16
16
  timeline.push(commit);
17
- cursor = commit.timelineParent.head;
17
+ cursor = commit.parentHead;
18
18
  }
19
19
  timeline.reverse();
20
20
  const prefix = cache.get(cursor);
@@ -86,7 +86,7 @@ export async function readDocumentMaintenanceFacts(authority, head, cache) {
86
86
  continue;
87
87
  const changed = await Promise.all(commit.stateChanges.map(async (change) => ({
88
88
  change,
89
- contents: await authority.readStateChangeContents(change),
89
+ contents: await authority.readStateChangeContents(commit, change),
90
90
  })));
91
91
  for (const { change, contents } of changed)
92
92
  files.set(change.relativePath, contents);