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,5 +1,6 @@
1
1
  import { captureNarrativeOrigin, snapshotBackground, isHistoryBackground, } from "../history/HistoryBackground.js";
2
- import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
2
+ import { FileNativeArtifactStore, forkArtifactClosure, } from "../artifact/FileNativeArtifactStore.js";
3
+ import { migrateLegacyArtifactStore } from "../artifact/LegacyArtifactStoreMigration.js";
3
4
  import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
4
5
  import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
5
6
  import { assertSameGameplay, readGameplay, } from "../../protocol/gameplay.js";
@@ -7,7 +8,7 @@ import { HistoryQuery } from "../history/HistoryQuery.js";
7
8
  import { exportStory } from "../history/StoryExport.js";
8
9
  import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
9
10
  import { createHash, randomUUID } from "node:crypto";
10
- import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
11
+ import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
11
12
  import { dirname, join, resolve } from "node:path";
12
13
  import { isDeepStrictEqual } from "node:util";
13
14
  import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, } from "../content/FileNativeContentTree.js";
@@ -17,10 +18,11 @@ import { WorldDocumentStore } from "./WorldDocumentStore.js";
17
18
  import { readWorldActivity, updateWorldActivity, } from "./WorldActivity.js";
18
19
  import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
19
20
  import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
20
- import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
21
- import { FileNativeAuthorityV3, FileNativeAuthorityV3Error, } from "./FileNativeAuthorityV3.js";
21
+ import { FileNativeWorldStorageMigrator, UnsupportedLegacyStorageError, } from "./FileNativeWorldStorageMigrator.js";
22
+ import { WorldAuthorityError, WorldLedgerAuthority, } from "./WorldLedgerAuthority.js";
23
+ import { forgetLedger, rewindTo } from "./WorldTurnLedger.js";
22
24
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
23
- import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
25
+ import { projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
24
26
  const publicationFile = "publication.json";
25
27
  const localMetadataFile = "local.json";
26
28
  export class FileNativeWorldCreationError extends Error {
@@ -37,11 +39,21 @@ export class FileNativeWorldNotFoundError extends Error {
37
39
  this.name = "FileNativeWorldNotFoundError";
38
40
  }
39
41
  }
42
+ /**
43
+ * Completed operations kept per world for same-process duplicate guards. A
44
+ * pending player-revision intent resumes against its own world's entry, so
45
+ * retention is per world: another world's traffic cannot evict it.
46
+ */
47
+ const RETAINED_OPERATION_OUTCOMES_PER_WORLD = 256;
40
48
  export class FileNativeWorldStore {
41
49
  extensionRequests;
42
50
  #documentMaintenanceCache = new Map();
43
51
  #worldsRoot;
44
- #operationsRoot;
52
+ // Process-local duplicate guard keyed by operation ID (ADR-0044): entries are
53
+ // not persisted, so a restart forgets them and a repeated operation redoes.
54
+ // Completed entries are bounded per world; in-progress entries are not.
55
+ #operationOutcomes = new Map();
56
+ #retainedOperationIds = new Map();
45
57
  #promptCompiler;
46
58
  #activeControlUsers = new Map();
47
59
  #openingWorldRevisions = new Set();
@@ -56,23 +68,26 @@ export class FileNativeWorldStore {
56
68
  const root = resolve(dataRoot);
57
69
  this.#worldsRoot = join(root, "worlds-file-native");
58
70
  this.extensionRequests = new WorldExtensionRequests(this.#worldsRoot);
59
- this.#operationsRoot = join(root, "operations");
60
71
  this.#promptCompiler =
61
72
  options.promptCompiler ?? new FileNativePromptCompiler();
62
73
  this.operations = new FileNativeWorldOperationCoordinator(root);
63
74
  this.playTimeline = new FileNativePlayTimelineStore(root);
64
75
  this.playAdvances = new FileNativePlayAdvanceStore(root);
65
- this.#storageMigrator = new FileNativeWorldStorageMigrator(root, this.playTimeline);
76
+ this.#storageMigrator = new FileNativeWorldStorageMigrator(root);
66
77
  }
67
78
  /**
68
- * Upgrade a formally released cumulative layout before current Authority or
69
- * play-timeline behavior crosses its seam. Concurrent callers share one
79
+ * Upgrade an Authority V3 world to the current ledger layout before current
80
+ * Authority or play-timeline behavior crosses its seam. Concurrent callers share one
70
81
  * in-process promise; the durable world lock serializes processes.
71
82
  */
72
83
  async ensureCurrentStorage(worldId) {
73
84
  assertIdentity(worldId, "World ID");
74
85
  if (this.#currentStorage.has(worldId))
75
86
  return;
87
+ // A shared migration may be queued behind this caller; when the caller
88
+ // already holds the world, run the migration inline instead of awaiting it.
89
+ if (this.operations.isWorldHeld(worldId))
90
+ return this.#ensureCurrentStorageSerialized(worldId);
76
91
  const existing = this.#storageMigrations.get(worldId);
77
92
  if (existing !== undefined)
78
93
  return existing;
@@ -89,7 +104,7 @@ export class FileNativeWorldStore {
89
104
  async #ensureCurrentStorageSerialized(worldId) {
90
105
  try {
91
106
  const root = join(this.#worldsRoot, worldId);
92
- const authority = new FileNativeAuthorityV3(root);
107
+ const authority = new WorldLedgerAuthority(root);
93
108
  if (await authority.exists()) {
94
109
  await authority.readHead();
95
110
  this.#currentStorage.add(worldId);
@@ -97,6 +112,7 @@ export class FileNativeWorldStore {
97
112
  }
98
113
  await this.operations.withWorldAuthorityLock(worldId, async () => {
99
114
  await this.#storageMigrator.migrate(worldId);
115
+ await migrateLegacyArtifactStore(dirname(this.#worldsRoot), worldId);
100
116
  });
101
117
  this.#currentStorage.add(worldId);
102
118
  }
@@ -114,7 +130,9 @@ export class FileNativeWorldStore {
114
130
  }
115
131
  if (error instanceof WorldOperationBusyError)
116
132
  throw new FileNativeWorldCreationError("operation_conflict", "World storage is being upgraded by another durable operation", { cause: error });
117
- throw new FileNativeWorldCreationError("world_corrupt", "Released world storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
133
+ if (error instanceof UnsupportedLegacyStorageError)
134
+ throw new FileNativeWorldCreationError("world_corrupt", "This world was created by v0.2.0 or earlier; open it once with v0.3.0–v0.6.0 before upgrading", { cause: error });
135
+ throw new FileNativeWorldCreationError("world_corrupt", "World storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
118
136
  }
119
137
  }
120
138
  async gameplay(worldId) {
@@ -150,7 +168,7 @@ export class FileNativeWorldStore {
150
168
  const root = join(this.#worldsRoot, worldId);
151
169
  const publication = await readPublicationAt(root);
152
170
  const title = await readWorldTitleAt(root, publication);
153
- const endpoint = await new FileNativeAuthorityV3(root).recoverHeadResult();
171
+ const endpoint = await new WorldLedgerAuthority(root).recoverHeadResult();
154
172
  const history = new HistoryQuery(worldId, endpoint.history.map(({ messageId, role, exactText }) => ({
155
173
  id: messageId,
156
174
  role,
@@ -200,8 +218,14 @@ export class FileNativeWorldStore {
200
218
  throw error;
201
219
  }
202
220
  return this.operations.withExclusiveWorldStateMutation(worldId, () => this.operations.withWorldAuthorityLock(worldId, async () => {
221
+ forgetLedger(root);
203
222
  await rm(root, { recursive: true, force: true });
223
+ // 删除期间的并发读取可能重新填充缓存,删除后再失效一次。
224
+ forgetLedger(root);
204
225
  this.#currentStorage.delete(worldId);
226
+ for (const operationId of this.#retainedOperationIds.get(worldId) ?? [])
227
+ this.#operationOutcomes.delete(operationId);
228
+ this.#retainedOperationIds.delete(worldId);
205
229
  return { deleted: true };
206
230
  }));
207
231
  }
@@ -298,9 +322,7 @@ export class FileNativeWorldStore {
298
322
  const preview = this.#preview(boundInput, worldId);
299
323
  const finalRoot = join(this.#worldsRoot, worldId);
300
324
  const stagingRoot = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
301
- const operationPath = this.#operationPath(input.operationId);
302
325
  await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
303
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
304
326
  await rm(stagingRoot, { recursive: true, force: true });
305
327
  try {
306
328
  const openingMessage = {
@@ -334,19 +356,7 @@ export class FileNativeWorldStore {
334
356
  worldId,
335
357
  history: genesis.history,
336
358
  });
337
- await new FileNativeAuthorityV3(stagingRoot).initialize({
338
- operationId: input.operationId,
339
- state,
340
- history: [openingMessage],
341
- additionalMaterials: initialMaterials,
342
- });
343
- await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
344
- schemaVersion: 3,
345
- head: "genesis",
346
- sequence: 0,
347
- commitDigest: null,
348
- ...backgroundCount([openingMessage]),
349
- });
359
+ await new WorldLedgerAuthority(stagingRoot).initialize();
350
360
  await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
351
361
  const publication = {
352
362
  schemaVersion: 1,
@@ -358,12 +368,7 @@ export class FileNativeWorldStore {
358
368
  };
359
369
  await writeJson(join(stagingRoot, publicationFile), publication);
360
370
  await updateWorldActivity(stagingRoot, "genesis", [openingMessage], Date.now());
361
- await durableTree(stagingRoot);
362
- crashAtFileNativeWorldCreationEdge("after_staging");
363
371
  await rename(stagingRoot, finalRoot);
364
- await syncDirectory(this.#worldsRoot);
365
- crashAtFileNativeWorldCreationEdge("after_atomic_publish");
366
- await publishOperation(operationPath, publication);
367
372
  return {
368
373
  outcome: "created",
369
374
  world: await this.#currentSummary(publication),
@@ -418,12 +423,8 @@ export class FileNativeWorldStore {
418
423
  * must never pair a mutable state tree with a different response head.
419
424
  */
420
425
  async renderPlayerViewsAtHead(worldId, head) {
421
- const root = join(this.#worldsRoot, worldId);
422
- const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
423
426
  const [state, control] = await Promise.all([
424
- materialized?.head === head
425
- ? readTree(join(root, "state"))
426
- : this.recoverEndpoint(worldId, head).then(({ state }) => state),
427
+ this.recoverEndpoint(worldId, head).then(({ state }) => state),
427
428
  this.readSurface(worldId, "control"),
428
429
  ]);
429
430
  const source = control.find(({ path }) => path === "player-views.yaml");
@@ -437,7 +438,7 @@ export class FileNativeWorldStore {
437
438
  await this.ensureCurrentStorage(worldId);
438
439
  const root = join(this.#worldsRoot, worldId);
439
440
  await readPublicationAt(root);
440
- return (await new FileNativeAuthorityV3(root).readHead()).head;
441
+ return (await new WorldLedgerAuthority(root).readHead()).head;
441
442
  }
442
443
  async withArchiveExport(worldId, action) {
443
444
  assertIdentity(worldId, "World ID");
@@ -469,7 +470,7 @@ export class FileNativeWorldStore {
469
470
  await this.ensureCurrentStorage(worldId);
470
471
  const root = join(this.#worldsRoot, worldId);
471
472
  await readPublicationAt(root);
472
- return new FileNativeAuthorityV3(root).currentOperationId();
473
+ return new WorldLedgerAuthority(root).currentOperationId();
473
474
  }
474
475
  async saveControlDraft(worldId, files) {
475
476
  assertIdentity(worldId, "World ID");
@@ -547,7 +548,6 @@ export class FileNativeWorldStore {
547
548
  if (currentFingerprint === nextFingerprint) {
548
549
  await rm(stagingRoot, { recursive: true, force: true });
549
550
  await rm(previousPath, { recursive: true, force: true });
550
- await syncDirectory(runtimeRoot);
551
551
  return { fingerprint: nextFingerprint };
552
552
  }
553
553
  const previous = await readTree(previousPath);
@@ -556,16 +556,11 @@ export class FileNativeWorldStore {
556
556
  fingerprint(previous) === input.expectedFingerprint) {
557
557
  if (fingerprint(staged) === nextFingerprint) {
558
558
  await rename(stagedPath, currentPath);
559
- await syncDirectory(root);
560
- await syncDirectory(stagingRoot);
561
559
  await rm(previousPath, { recursive: true, force: true });
562
560
  await rm(stagingRoot, { recursive: true, force: true });
563
- await syncDirectory(runtimeRoot);
564
561
  return { fingerprint: nextFingerprint };
565
562
  }
566
563
  await rename(previousPath, currentPath);
567
- await syncDirectory(root);
568
- await syncDirectory(runtimeRoot);
569
564
  current = await readTree(currentPath);
570
565
  currentFingerprint = fingerprint(current);
571
566
  }
@@ -575,16 +570,10 @@ export class FileNativeWorldStore {
575
570
  await rm(stagingRoot, { recursive: true, force: true });
576
571
  await rm(previousPath, { recursive: true, force: true });
577
572
  await writeSurface(stagingRoot, "control", input.files);
578
- await durableTree(stagingRoot);
579
573
  await rename(currentPath, previousPath);
580
- await syncDirectory(root);
581
- await syncDirectory(runtimeRoot);
582
574
  await rename(stagedPath, currentPath);
583
- await syncDirectory(root);
584
- await syncDirectory(stagingRoot);
585
575
  await rm(previousPath, { recursive: true, force: true });
586
576
  await rm(stagingRoot, { recursive: true, force: true });
587
- await syncDirectory(runtimeRoot);
588
577
  return { fingerprint: nextFingerprint };
589
578
  });
590
579
  }
@@ -709,52 +698,22 @@ export class FileNativeWorldStore {
709
698
  }
710
699
  try {
711
700
  return await this.operations.withWorldAuthorityLock(worldId, async () => {
712
- const authorityStore = new FileNativeAuthorityV3(root);
713
- const authority = await authorityStore.readHead();
714
- const checkpoint = await readMaterializedCheckpoint(root);
715
- const cachedBackgrounds = await readTree(join(root, "runtime", "history-backgrounds"));
716
- if (!sameMaterializedEndpoint(checkpoint, authority) ||
717
- (checkpoint?.historyBackgroundCount ?? 0) !== cachedBackgrounds.length) {
718
- const recovered = await authorityStore.recover();
719
- let operation = null;
720
- if (authority.operationId !== null && authority.sequence > 0) {
721
- const candidate = await this.getOperationOutcome(authority.operationId);
722
- if (isCommittedOutcome(candidate)) {
723
- if (candidate.worldId !== worldId ||
724
- candidate.head !== authority.head ||
725
- candidate.commitDigest !== authority.commitDigest)
726
- throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head is bound to a different operation receipt");
727
- operation = candidate;
728
- }
729
- else if (candidate.outcome !== "not_started") {
730
- throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head has no recoverable acceptance receipt");
731
- }
732
- }
733
- await materializeRecoveredEndpoint(root, authority, recovered);
734
- if (operation !== null)
735
- await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
736
- }
737
- const [state, control, history, materials, backgrounds] = await Promise.all([
738
- readTree(join(root, "state")),
739
- readTree(join(root, "control")),
740
- readTree(join(root, "history")),
741
- readOptionalJson(join(root, "runtime", "additional-materials.json")),
742
- readTree(join(root, "runtime", "history-backgrounds")),
743
- ]);
744
- if ((materials?.head ?? "genesis") !== authority.head)
745
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
701
+ const recovered = await new WorldLedgerAuthority(root).recover();
702
+ const control = await readTree(join(root, "control"));
746
703
  return {
747
704
  worldId,
748
- parentHead: authority.head,
749
- files: [...state, ...control].reduce((files, { path, contents }, index) => {
750
- files[`${index < state.length ? "state" : "control"}/${path}`] =
751
- contents;
705
+ parentHead: recovered.head,
706
+ files: [...recovered.state, ...control].reduce((files, { path, contents }, index) => {
707
+ files[`${index < recovered.state.length ? "state" : "control"}/${path}`] = contents;
752
708
  return files;
753
709
  }, {}),
754
- additionalMaterials: materials?.items ?? [],
755
- history: materializedHistoryRecord(history),
756
- historyBackgrounds: materializedBackgroundRecord(backgrounds, materializedHistoryRecord(history)),
757
- narrativeCheckpoint: await authorityStore.readNarrativeCheckpoint(),
710
+ additionalMaterials: recovered.additionalMaterials,
711
+ history: Object.fromEntries(recovered.history.map(({ messageId, exactText }) => [
712
+ messageId,
713
+ exactText,
714
+ ])),
715
+ historyBackgrounds: historyBackgroundRecord(recovered.history),
716
+ narrativeCheckpoint: recovered.narrativeCheckpoint,
758
717
  };
759
718
  });
760
719
  }
@@ -767,7 +726,7 @@ export class FileNativeWorldStore {
767
726
  /**
768
727
  * Bind an immutable historical endpoint with the world's current control
769
728
  * surface. This is the same semantic input as a fresh context started while
770
- * that endpoint was current, without rematerializing it over today's head.
729
+ * that endpoint was current, without rewinding today's head.
771
730
  */
772
731
  async bindPlayCallChainAt(worldId, head) {
773
732
  assertIdentity(worldId, "World ID");
@@ -791,34 +750,29 @@ export class FileNativeWorldStore {
791
750
  ])),
792
751
  };
793
752
  }
794
- async reserveOperation(operationId) {
753
+ reserveOperation(operationId) {
795
754
  assertIdentity(operationId, "operation ID");
796
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
797
- const path = this.#operationOutcomePath(operationId);
798
- try {
799
- await writeFile(path, `${JSON.stringify({ outcome: "in_progress" }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
800
- await syncFile(path);
801
- await syncDirectory(this.#operationsRoot);
802
- return true;
803
- }
804
- catch (error) {
805
- if (isNodeError(error) && error.code === "EEXIST")
806
- return false;
807
- throw error;
755
+ if (this.#operationOutcomes.has(operationId))
756
+ return Promise.resolve(false);
757
+ this.#operationOutcomes.set(operationId, { phase: "in_progress" });
758
+ return Promise.resolve(true);
759
+ }
760
+ #rememberOperationOutcome(worldId, operationId, outcome) {
761
+ this.#operationOutcomes.set(operationId, { phase: "done", outcome });
762
+ const retained = this.#retainedOperationIds.get(worldId) ?? [];
763
+ retained.push(operationId);
764
+ while (retained.length > RETAINED_OPERATION_OUTCOMES_PER_WORLD) {
765
+ const evicted = retained.shift();
766
+ if (evicted !== undefined)
767
+ this.#operationOutcomes.delete(evicted);
808
768
  }
769
+ this.#retainedOperationIds.set(worldId, retained);
809
770
  }
810
- async releaseOperationReservation(operationId) {
771
+ releaseOperationReservation(operationId) {
811
772
  assertIdentity(operationId, "operation ID");
812
- const path = this.#operationOutcomePath(operationId);
813
- const current = await readOptionalJson(path);
814
- if (current === null)
815
- return;
816
- if (!isRecord(current) ||
817
- !hasExactKeys(current, ["outcome"]) ||
818
- current.outcome !== "in_progress")
819
- throw new FileNativeWorldCreationError("world_corrupt", "Operation reservation to release has an invalid shape");
820
- await rm(path, { force: true });
821
- await syncDirectory(this.#operationsRoot);
773
+ if (this.#operationOutcomes.get(operationId)?.phase === "in_progress")
774
+ this.#operationOutcomes.delete(operationId);
775
+ return Promise.resolve();
822
776
  }
823
777
  async recoverEndpoint(worldId, head) {
824
778
  assertIdentity(worldId, "World ID");
@@ -826,7 +780,7 @@ export class FileNativeWorldStore {
826
780
  const root = join(this.#worldsRoot, worldId);
827
781
  await readPublicationAt(root);
828
782
  try {
829
- const recovered = await new FileNativeAuthorityV3(root).recover(head);
783
+ const recovered = await new WorldLedgerAuthority(root).recover(head);
830
784
  return {
831
785
  worldId,
832
786
  head: recovered.head,
@@ -842,25 +796,6 @@ export class FileNativeWorldStore {
842
796
  throw new FileNativeWorldCreationError("world_corrupt", `Cannot recover immutable Authority endpoint${head === undefined ? "" : `: ${head}`}`, error instanceof Error ? { cause: error } : undefined);
843
797
  }
844
798
  }
845
- async repairMaterialization(worldId) {
846
- assertIdentity(worldId, "World ID");
847
- await this.ensureCurrentStorage(worldId);
848
- try {
849
- const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
850
- await this.bindPlayCallChain(worldId);
851
- const root = join(this.#worldsRoot, worldId);
852
- return (await new FileNativeAuthorityV3(root).readHead()).operationId;
853
- });
854
- return operationId === null
855
- ? { outcome: "not_started" }
856
- : this.getOperationOutcome(operationId);
857
- }
858
- catch (error) {
859
- if (error instanceof WorldOperationBusyError)
860
- throw new FileNativeWorldCreationError("operation_conflict", "This world has a durable state operation and cannot repair materialization yet");
861
- throw error;
862
- }
863
- }
864
799
  async deriveWorld(input) {
865
800
  assertIdentity(input.operationId, "Fork operation ID");
866
801
  assertIdentity(input.sourceWorldId, "Source world ID");
@@ -878,37 +813,17 @@ export class FileNativeWorldStore {
878
813
  const target = input.targetDataRoot === undefined
879
814
  ? this
880
815
  : new FileNativeWorldStore(input.targetDataRoot);
881
- const operationPath = input.identityPrefix === "import"
882
- ? join(target.#operationsRoot, `import-${operationDigest(input.operationId)}.json`)
883
- : target.#derivationOperationPath(input.operationId);
884
816
  const requestFingerprint = derivationRequestFingerprint(input);
885
817
  const worldId = `world-${operationDigest(`${input.identityPrefix ?? "derive"}:${input.operationId}`).slice(0, 24)}`;
886
818
  const finalRoot = input.publish === false
887
819
  ? resolve(input.targetWorldRoot)
888
820
  : join(target.#worldsRoot, worldId);
889
- const previous = input.publish === false
890
- ? null
891
- : await readOptionalJson(operationPath);
892
- if (previous !== null) {
893
- await target.ensureCurrentStorage(previous.worldId);
894
- if (!(await derivationPublicationMatches(join(target.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
895
- throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
896
- return {
897
- outcome: "derived",
898
- world: await target.#currentSummary(previous),
899
- worldRoot: finalRoot,
900
- };
901
- }
902
821
  const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
903
822
  if (alreadyPublished !== null) {
904
823
  if (input.publish !== false)
905
824
  await target.ensureCurrentStorage(alreadyPublished.worldId);
906
825
  if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
907
826
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
908
- if (input.publish !== false) {
909
- await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
910
- await publishJson(operationPath, alreadyPublished);
911
- }
912
827
  return {
913
828
  outcome: "derived",
914
829
  world: await readWorldSummaryAt(finalRoot, alreadyPublished),
@@ -920,43 +835,31 @@ export class FileNativeWorldStore {
920
835
  const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
921
836
  const sourcePublication = await readPublicationAt(sourceRoot);
922
837
  const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
923
- const sourceAuthority = new FileNativeAuthorityV3(sourceRoot);
838
+ const sourceAuthority = new WorldLedgerAuthority(sourceRoot);
924
839
  const sourceHead = input.sourceHead === "current"
925
840
  ? (await sourceAuthority.readHead()).head
926
841
  : input.sourceHead;
927
- if (input.requireMaterialized) {
928
- const authorityHead = await sourceAuthority.readHead();
929
- const materialized = await readMaterializedCheckpoint(sourceRoot);
930
- if (materialized?.head !== authorityHead.head ||
931
- materialized.commitDigest !== authorityHead.commitDigest)
932
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Please repair the world before exporting a backup");
933
- }
934
842
  const [sourceGenesis, control] = await Promise.all([
935
843
  readGenesisForFork(sourceRoot, input.sourceWorldId),
936
844
  readTree(join(sourceRoot, "control")),
937
845
  ]);
938
846
  const staging = join(dirname(finalRoot), `.staging-${worldId}-${randomUUID()}`);
939
847
  await mkdir(dirname(finalRoot), { recursive: true, mode: 0o700 });
940
- if (input.publish !== false)
941
- await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
942
848
  try {
943
- await mkdir(join(staging, "runtime"), { recursive: true });
944
- const clonedHead = await sourceAuthority.clonePrefixTo({
945
- targetWorldRoot: staging,
946
- selectedHead: sourceHead,
947
- operationId: input.operationId,
849
+ await copyForkClosure(sourceRoot, staging);
850
+ await rewindTo(staging, headPosition(sourceHead));
851
+ await forkArtifactClosure(staging, {
852
+ sourceWorldId: input.sourceWorldId,
853
+ targetWorldId: worldId,
854
+ restoredHead: sourceHead,
948
855
  });
949
- const selected = await new FileNativeAuthorityV3(staging).recoverHeadResult();
950
- await writeSurface(staging, "state", selected.state);
951
- await writeSurface(staging, "control", control);
856
+ // 分叉不信任来源的投影文件:当前文件由副本上的账本端点重建。
857
+ await new WorldLedgerAuthority(staging).materializeCurrentFiles();
858
+ const selected = await new WorldLedgerAuthority(staging).recover();
952
859
  await PackageScriptPermissions.write(staging, input.resetPackageScriptPermissions
953
860
  ? []
954
861
  : await PackageScriptPermissions.read(sourceRoot));
955
862
  await WorldExtensionControls.copy(sourceRoot, staging);
956
- await mkdir(join(staging, "history"), { recursive: true });
957
- for (const file of historySurfaceFiles(selected.history))
958
- await writeIdempotentText(join(staging, "history", file.path), file.contents);
959
- await replaceMaterializedTree(join(staging, "runtime", "history-backgrounds"), projectHistoryBackgrounds(selected.history));
960
863
  const genesis = {
961
864
  schemaVersion: 1,
962
865
  type: "file_native_genesis",
@@ -974,17 +877,6 @@ export class FileNativeWorldStore {
974
877
  worldId,
975
878
  history: genesis.history,
976
879
  });
977
- await writeJson(join(staging, "runtime", "additional-materials.json"), {
978
- head: sourceHead,
979
- items: selected.additionalMaterials,
980
- });
981
- await writeJson(join(staging, "runtime", "materialized-head.json"), {
982
- schemaVersion: 3,
983
- head: sourceHead,
984
- sequence: clonedHead.sequence,
985
- commitDigest: clonedHead.commitDigest,
986
- ...backgroundCount(selected.history),
987
- });
988
880
  await input.stageTarget?.({
989
881
  sourceHead,
990
882
  targetWorldRoot: staging,
@@ -1009,7 +901,6 @@ export class FileNativeWorldStore {
1009
901
  await writeJson(join(staging, "runtime", "archive-import.json"), {
1010
902
  schemaVersion: 1,
1011
903
  });
1012
- crashAtFileNativeAuthorityEdge("derivation_after_target_staging");
1013
904
  const publication = {
1014
905
  schemaVersion: 1,
1015
906
  worldId,
@@ -1020,13 +911,8 @@ export class FileNativeWorldStore {
1020
911
  };
1021
912
  await writeJson(join(staging, publicationFile), publication);
1022
913
  await updateWorldActivity(staging, sourceHead, selected.history, Date.now());
1023
- await durableTree(staging);
1024
- crashAtFileNativeAuthorityEdge("derivation_before_publish");
914
+ forgetLedger(finalRoot);
1025
915
  await rename(staging, finalRoot);
1026
- await syncDirectory(dirname(finalRoot));
1027
- crashAtFileNativeAuthorityEdge("derivation_after_publish");
1028
- if (input.publish !== false)
1029
- await publishJson(operationPath, publication);
1030
916
  return {
1031
917
  outcome: "derived",
1032
918
  world: await readWorldSummaryAt(finalRoot, publication),
@@ -1039,8 +925,6 @@ export class FileNativeWorldStore {
1039
925
  if (published !== null) {
1040
926
  if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
1041
927
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
1042
- if (input.publish !== false)
1043
- await publishJson(operationPath, published);
1044
928
  return {
1045
929
  outcome: "derived",
1046
930
  world: await readWorldSummaryAt(finalRoot, published),
@@ -1059,142 +943,53 @@ export class FileNativeWorldStore {
1059
943
  throw error;
1060
944
  }
1061
945
  }
1062
- async failOperation(operationId, outcome = "failed") {
1063
- const current = await this.getOperationOutcome(operationId);
1064
- if (isCommittedOutcome(current))
1065
- return;
1066
- await writeJson(this.#operationOutcomePath(operationId), { outcome });
1067
- }
1068
946
  async getOperationOutcome(operationId) {
1069
- return this.#readOperationOutcome(operationId);
1070
- }
1071
- async #readOperationOutcome(operationId) {
1072
947
  assertIdentity(operationId, "operation ID");
1073
- let directRecord;
1074
- try {
1075
- let value = await readOptionalJson(this.#operationOutcomePath(operationId));
1076
- if (isRecord(value) &&
1077
- (value.outcome === "acceptance_prepared" ||
1078
- value.outcome === "committed" ||
1079
- value.outcome === "committed_materialization_pending") &&
1080
- typeof value.worldId === "string") {
1081
- await this.ensureCurrentStorage(value.worldId);
1082
- value = await readOptionalJson(this.#operationOutcomePath(operationId));
1083
- }
1084
- if (value === null)
1085
- directRecord = null;
1086
- else {
1087
- assertOperationOutcomeIntegrity(value);
1088
- directRecord = value;
1089
- }
1090
- }
1091
- catch (error) {
1092
- if (error instanceof FileNativeWorldCreationError)
1093
- throw error;
1094
- throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
1095
- }
1096
- if (directRecord?.outcome === "acceptance_prepared")
1097
- return resolvePreparedOperationOutcome(this.#worldsRoot, operationId, directRecord);
1098
- const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
1099
- if (direct?.outcome === "committed" ||
1100
- direct?.outcome === "committed_materialization_pending") {
1101
- const worldRoot = join(this.#worldsRoot, direct.worldId);
1102
- const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, direct);
1103
- const materialized = await readMaterializedCheckpoint(worldRoot);
1104
- if (materialized !== null && materialized.sequence >= commit.sequence)
1105
- return { ...direct, outcome: "committed" };
1106
- await assertPendingMaterializationCompatible(worldRoot, direct);
1107
- return { ...direct, outcome: "committed_materialization_pending" };
1108
- }
1109
- return direct?.outcome === "in_progress"
1110
- ? { outcome: "in_progress" }
1111
- : (direct ?? { outcome: "not_started" });
948
+ const entry = this.#operationOutcomes.get(operationId);
949
+ if (entry === undefined)
950
+ return { outcome: "not_started" };
951
+ if (entry.phase === "in_progress")
952
+ return { outcome: "in_progress" };
953
+ const outcome = structuredClone(entry.outcome);
954
+ await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, outcome.worldId), outcome);
955
+ return outcome;
1112
956
  }
1113
957
  /**
1114
- * Replace one committed player message on the world's active timeline
1115
- * without changing world identity or destructively rewriting Authority.
1116
- * The new immutable commit restores the selected player's logical parent
1117
- * snapshot and appends the replacement player text in one acceptance step.
958
+ * Roll the world back to a player message's logical parent endpoint before an
959
+ * edited replacement is submitted (ADR-0045). Rewinding to the current head is
960
+ * a no-op, so the edit's steps can repeat safely.
1118
961
  */
1119
- async reviseTimeline(input) {
1120
- assertIdentity(input.operationId, "Timeline-revision operation ID");
1121
- assertIdentity(input.worldId, "World ID");
1122
- if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(input.expectedCurrentHead) ||
1123
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(input.restoresHead) ||
1124
- !/^commit:[1-9][0-9]*$/u.test(input.replacesHead))
1125
- throw new TypeError("Timeline-revision endpoint is invalid");
1126
- if (input.replacementText.trim() === "")
1127
- throw new TypeError("Edited player message cannot be empty");
1128
- if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
1129
- throw new TypeError("Timeline-revision request fingerprint is invalid");
1130
- await this.ensureCurrentStorage(input.worldId);
962
+ async rewindPlayerRevision(worldId, restoresHead) {
963
+ assertIdentity(worldId, "World ID");
964
+ if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(restoresHead))
965
+ throw new TypeError("Player-revision endpoint is invalid");
966
+ await this.ensureCurrentStorage(worldId);
1131
967
  try {
1132
- return await this.operations.withExclusiveWorldStateMutation(input.worldId, () => this.#reviseTimelineUnlocked(input));
968
+ await this.operations.withExclusiveWorldStateMutation(worldId, () => rewindTo(join(this.#worldsRoot, worldId), headPosition(restoresHead)));
1133
969
  }
1134
970
  catch (error) {
1135
971
  if (error instanceof WorldOperationBusyError)
1136
- throw new FileNativeWorldCreationError("operation_conflict", "Timeline editing is locked while a world revision is open");
972
+ throw new FileNativeWorldCreationError("operation_conflict", "Player edits are locked while a world revision is open");
1137
973
  throw error;
1138
974
  }
1139
975
  }
1140
- async #reviseTimelineUnlocked(input) {
1141
- const existing = await this.getOperationOutcome(input.operationId);
1142
- if (isCommittedOutcome(existing)) {
1143
- const fact = await new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId)).authorityFactAt(existing.head);
1144
- const commit = fact.commit;
1145
- if (existing.worldId !== input.worldId ||
1146
- fact.digest !== existing.commitDigest ||
1147
- commit?.mode !== "timeline_revision" ||
1148
- commit.timelineRevision?.restoresHead !== input.restoresHead ||
1149
- commit.timelineRevision.replacesHead !== input.replacesHead ||
1150
- commit.timelineRevision.requestFingerprint !==
1151
- input.requestFingerprint ||
1152
- !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({
1153
- role,
1154
- exactText,
1155
- })), [{ role: "player", exactText: input.replacementText }]))
1156
- throw new FileNativeWorldCreationError("operation_conflict", "The same timeline-revision operation ID is bound to a different request");
1157
- return existing;
1158
- }
1159
- const root = join(this.#worldsRoot, input.worldId);
1160
- const authorityStore = new FileNativeAuthorityV3(root);
1161
- const [replaced, replacementMaterials] = await Promise.all([
1162
- authorityStore.commitAt(input.replacesHead),
1163
- authorityStore.recoverMaterials(input.restoresHead),
1164
- ]);
1165
- const logicalParent = replaced?.timelineParent.head;
1166
- if (replaced === null ||
1167
- !replaced.historyAppend.some(({ role }) => role === "player") ||
1168
- logicalParent !== input.restoresHead)
1169
- throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
1170
- const binding = await this.bindPlayCallChainAt(input.worldId, input.restoresHead);
1171
- const snapshot = WorldDocumentStore.open({
1172
- layout: "world_state",
1173
- files: Object.entries(binding.files).map(([path, contents]) => ({
1174
- path,
1175
- contents,
1176
- })),
1177
- });
1178
- const background = snapshotBackground(captureNarrativeOrigin(snapshot, 1).value);
1179
- return this.#commitHistoryChange({
1180
- operationId: input.operationId,
1181
- worldId: input.worldId,
1182
- parentHead: input.expectedCurrentHead,
1183
- historyAppend: [
1184
- {
1185
- role: "player",
1186
- exactText: input.replacementText,
1187
- ...backgroundField(background),
1188
- },
1189
- ],
1190
- nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
1191
- stateChanges: [],
1192
- mode: "timeline_revision",
1193
- timelineRevision: {
1194
- restoresHead: input.restoresHead,
1195
- replacesHead: input.replacesHead,
1196
- requestFingerprint: input.requestFingerprint,
1197
- },
976
+ /** The in-progress player edit record; written before the first world change. */
977
+ async readPlayerRevisionIntent(worldId) {
978
+ assertIdentity(worldId, "World ID");
979
+ return readOptionalJsonFile(playerRevisionIntentPath(this.#worldsRoot, worldId));
980
+ }
981
+ async writePlayerRevisionIntent(worldId, intent) {
982
+ assertIdentity(worldId, "World ID");
983
+ const path = playerRevisionIntentPath(this.#worldsRoot, worldId);
984
+ await mkdir(dirname(path), { recursive: true });
985
+ const temporary = `${path}.${randomUUID()}.tmp`;
986
+ await writeFile(temporary, `${JSON.stringify(intent)}\n`, "utf8");
987
+ await rename(temporary, path);
988
+ }
989
+ async clearPlayerRevisionIntent(worldId) {
990
+ assertIdentity(worldId, "World ID");
991
+ await rm(playerRevisionIntentPath(this.#worldsRoot, worldId), {
992
+ force: true,
1198
993
  });
1199
994
  }
1200
995
  async commitCorrection(input) {
@@ -1262,83 +1057,51 @@ export class FileNativeWorldStore {
1262
1057
  }
1263
1058
  }
1264
1059
  async #commitHistoryChange(input) {
1265
- const existing = await this.getOperationOutcome(input.operationId);
1266
- if (isCommittedOutcome(existing)) {
1267
- assertSameOperationOutcome(existing, input);
1268
- const authority = new FileNativeAuthorityV3(join(this.#worldsRoot, existing.worldId));
1269
- const fact = await authority.authorityFactAt(existing.head);
1270
- if (fact.digest !== existing.commitDigest ||
1060
+ const existing = this.#operationOutcomes.get(input.operationId);
1061
+ if (existing?.phase === "done") {
1062
+ const outcome = existing.outcome;
1063
+ assertSameOperationOutcome(outcome, input);
1064
+ const fact = await new WorldLedgerAuthority(join(this.#worldsRoot, outcome.worldId)).authorityFactAt(outcome.head);
1065
+ if (fact.digest !== outcome.commitDigest ||
1271
1066
  !authorityCommitMatchesInput(fact.commit, input))
1272
1067
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
1273
- return existing;
1274
- }
1275
- const prepared = existing.outcome === "in_progress"
1276
- ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
1277
- : null;
1278
- if (prepared !== null) {
1279
- const comparable = {
1280
- ...structuredClone(prepared),
1281
- outcome: "committed_materialization_pending",
1282
- };
1283
- assertSameOperationOutcome(comparable, input);
1068
+ return outcome;
1284
1069
  }
1285
- if (existing.outcome !== "not_started" &&
1286
- !(existing.outcome === "in_progress" &&
1287
- (input.operationReserved === true || prepared !== null)))
1070
+ if (existing?.phase === "in_progress" && input.operationReserved !== true)
1288
1071
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
1289
1072
  const root = join(this.#worldsRoot, input.worldId);
1290
1073
  try {
1291
1074
  return await this.operations.withWorldAuthorityLock(input.worldId, async () => {
1292
- const concurrent = await this.#readOperationOutcome(input.operationId);
1293
- if (isCommittedOutcome(concurrent)) {
1294
- assertSameOperationOutcome(concurrent, input);
1295
- const known = await new FileNativeAuthorityV3(join(this.#worldsRoot, concurrent.worldId)).authorityFactAt(concurrent.head);
1296
- if (known.digest !== concurrent.commitDigest ||
1075
+ const concurrent = this.#operationOutcomes.get(input.operationId);
1076
+ if (concurrent?.phase === "done") {
1077
+ const outcome = concurrent.outcome;
1078
+ assertSameOperationOutcome(outcome, input);
1079
+ const known = await new WorldLedgerAuthority(join(this.#worldsRoot, outcome.worldId)).authorityFactAt(outcome.head);
1080
+ if (known.digest !== outcome.commitDigest ||
1297
1081
  !authorityCommitMatchesInput(known.commit, input))
1298
1082
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
1299
- return concurrent;
1083
+ return outcome;
1300
1084
  }
1301
- const concurrentPrepared = concurrent.outcome === "in_progress"
1302
- ? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
1303
- : null;
1304
- if (concurrentPrepared !== null)
1305
- assertSameOperationOutcome({
1306
- ...structuredClone(concurrentPrepared),
1307
- outcome: "committed_materialization_pending",
1308
- }, input);
1309
- else if (concurrent.outcome === "in_progress" &&
1085
+ if (concurrent?.phase === "in_progress" &&
1310
1086
  input.operationReserved !== true)
1311
1087
  throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
1312
- const authorityStore = new FileNativeAuthorityV3(root);
1088
+ const authorityStore = new WorldLedgerAuthority(root);
1313
1089
  const authority = await authorityStore.readHead();
1314
- if (authority.head !== input.parentHead) {
1090
+ if (authority.head !== input.parentHead)
1315
1091
  throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
1316
- }
1317
- const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1318
- if ((materialized?.head ?? "genesis") !== authority.head)
1319
- throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
1320
1092
  const [basisState, storedControl] = await Promise.all([
1321
- authorityStore.recoverState(input.timelineRevision?.restoresHead ?? authority.head),
1093
+ authorityStore.recoverState(authority.head),
1322
1094
  readTree(join(root, "control")),
1323
1095
  ]);
1324
1096
  const candidateState = new Map(basisState.map((file) => [file.path, file.contents]));
1325
- if (input.mode === "timeline_revision") {
1326
- if (input.timelineRevision === undefined ||
1327
- input.stateChanges.length !== 0)
1328
- throw new FileNativeWorldCreationError("operation_conflict", "Timeline-revision commit payload is invalid");
1329
- }
1330
- else {
1331
- if (input.timelineRevision !== undefined)
1332
- throw new FileNativeWorldCreationError("operation_conflict", "A regular commit cannot carry a timeline-revision payload");
1333
- for (const change of input.stateChanges) {
1334
- assertRelativePath(change.relativePath);
1335
- const previous = candidateState.get(change.relativePath);
1336
- const previousHash = previous === undefined ? null : sha256(previous);
1337
- if (previousHash !== change.expectedPreviousHash ||
1338
- sha256(change.canonicalNextBytes) !== change.nextHash)
1339
- throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
1340
- candidateState.set(change.relativePath, change.canonicalNextBytes);
1341
- }
1097
+ for (const change of input.stateChanges) {
1098
+ assertRelativePath(change.relativePath);
1099
+ const previous = candidateState.get(change.relativePath);
1100
+ const previousHash = previous === undefined ? null : sha256(previous);
1101
+ if (previousHash !== change.expectedPreviousHash ||
1102
+ sha256(change.canonicalNextBytes) !== change.nextHash)
1103
+ throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
1104
+ candidateState.set(change.relativePath, change.canonicalNextBytes);
1342
1105
  }
1343
1106
  const inspection = inspectContentPackageCurrentTree([
1344
1107
  ...[...candidateState].map(([path, contents]) => ({
@@ -1352,10 +1115,7 @@ export class FileNativeWorldStore {
1352
1115
  ], { requireOpening: false });
1353
1116
  if (inspection.status !== "usable")
1354
1117
  throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
1355
- if (concurrentPrepared === null)
1356
- await authorityStore.discardUnacceptedNextEpoch();
1357
- crashAtFileNativeAuthorityEdge("before_commit_acceptance");
1358
- const preparedAppend = await authorityStore.prepareAppend({
1118
+ const { commit, commitDigest } = await authorityStore.appendCommit({
1359
1119
  ...(input.playContext === undefined
1360
1120
  ? {}
1361
1121
  : { playContext: input.playContext }),
@@ -1364,12 +1124,9 @@ export class FileNativeWorldStore {
1364
1124
  : { worldClock: input.worldClock }),
1365
1125
  operationId: input.operationId,
1366
1126
  parentHead: input.parentHead,
1367
- ...(input.timelineRevision === undefined
1368
- ? {}
1369
- : { timelineParentHead: input.timelineRevision.restoresHead }),
1370
1127
  mode: input.mode,
1371
1128
  historyAppend: input.historyAppend,
1372
- stateChanges: structuredClone(input.stateChanges),
1129
+ stateChanges: input.stateChanges,
1373
1130
  nextMaterials: input.nextMaterials,
1374
1131
  ...(input.narrativeCheckpoint === undefined
1375
1132
  ? {}
@@ -1380,57 +1137,27 @@ export class FileNativeWorldStore {
1380
1137
  corrects: input.parentHead,
1381
1138
  }
1382
1139
  : {}),
1383
- ...(input.timelineRevision === undefined
1384
- ? {}
1385
- : { timelineRevision: input.timelineRevision }),
1386
1140
  });
1387
- crashAtFileNativeAuthorityEdge("after_authority_objects");
1388
- const acceptedPreparation = concurrentPrepared ?? prepared;
1389
- if (acceptedPreparation !== null &&
1390
- acceptedPreparation.commitDigest !== preparedAppend.commitDigest)
1391
- throw new FileNativeWorldCreationError("operation_conflict", "The prepared Authority fact does not match the reserved operation");
1392
- const { commit } = preparedAppend;
1393
- const pending = {
1394
- outcome: "committed_materialization_pending",
1141
+ const outcome = {
1142
+ outcome: "committed",
1395
1143
  worldId: input.worldId,
1396
1144
  parentHead: input.parentHead,
1397
1145
  head: commit.head,
1398
- commitDigest: preparedAppend.commitDigest,
1146
+ commitDigest,
1399
1147
  historyAppend: structuredClone(input.historyAppend),
1400
1148
  nextAdditionalMaterials: structuredClone(input.nextMaterials),
1401
1149
  mode: input.mode,
1402
1150
  };
1403
- await publishJson(this.#operationOutcomePath(input.operationId), {
1404
- ...pending,
1405
- outcome: "acceptance_prepared",
1406
- });
1407
- crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
1408
- await authorityStore.publishPrepared(preparedAppend);
1409
- const acceptedAt = Date.now();
1410
- crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1411
- await publishJson(this.#operationOutcomePath(input.operationId), pending);
1412
- try {
1413
- const recovered = await authorityStore.recover(commit.head);
1414
- await assertPendingMaterializationCompatible(root, pending);
1415
- await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered, acceptedAt);
1416
- crashAtFileNativeAuthorityEdge("after_materialization");
1417
- const outcome = { ...pending, outcome: "committed" };
1418
- await publishJson(this.#operationOutcomePath(input.operationId), outcome);
1419
- return outcome;
1420
- }
1421
- catch (error) {
1422
- if (error instanceof FileNativeWorldCreationError &&
1423
- (error.code === "world_corrupt" ||
1424
- error.code === "inconsistent_materialization"))
1425
- throw error;
1426
- return pending;
1427
- }
1151
+ this.#rememberOperationOutcome(input.worldId, input.operationId, outcome);
1152
+ const recovered = await authorityStore.recover(commit.head);
1153
+ await updateWorldActivity(root, commit.head, recovered.history, Date.now());
1154
+ return outcome;
1428
1155
  });
1429
1156
  }
1430
1157
  catch (error) {
1431
1158
  if (error instanceof WorldOperationBusyError)
1432
1159
  throw new FileNativeWorldCreationError("operation_conflict", "World Authority commit is being serialized by another process");
1433
- if (error instanceof FileNativeAuthorityV3Error)
1160
+ if (error instanceof WorldAuthorityError)
1434
1161
  throw new FileNativeWorldCreationError(error.code === "corrupt" ? "world_corrupt" : "operation_conflict", error.message, { cause: error });
1435
1162
  throw error;
1436
1163
  }
@@ -1440,7 +1167,7 @@ export class FileNativeWorldStore {
1440
1167
  await this.ensureCurrentStorage(worldId);
1441
1168
  const root = join(this.#worldsRoot, worldId);
1442
1169
  await readPublicationAt(root);
1443
- const authority = await new FileNativeAuthorityV3(root).readHistory();
1170
+ const authority = await new WorldLedgerAuthority(root).readHistory();
1444
1171
  return structuredClone(authority);
1445
1172
  }
1446
1173
  async inspectDocumentMaintenance(worldId, head) {
@@ -1463,7 +1190,7 @@ export class FileNativeWorldStore {
1463
1190
  await this.ensureCurrentStorage(worldId);
1464
1191
  const root = join(this.#worldsRoot, worldId);
1465
1192
  await readPublicationAt(root);
1466
- const authority = new FileNativeAuthorityV3(root);
1193
+ const authority = new WorldLedgerAuthority(root);
1467
1194
  const selectedHead = head ?? (await authority.readHead()).head;
1468
1195
  let cache = this.#documentMaintenanceCache.get(worldId);
1469
1196
  if (cache === undefined) {
@@ -1501,49 +1228,25 @@ export class FileNativeWorldStore {
1501
1228
  };
1502
1229
  }
1503
1230
  async #readPublication(operationId) {
1231
+ // The world root's own publication record is the only creation receipt.
1232
+ const worldId = `world-${operationDigest(operationId).slice(0, 24)}`;
1504
1233
  let publication;
1505
1234
  try {
1506
- publication = await readJson(this.#operationPath(operationId));
1235
+ publication = await readPublicationAt(join(this.#worldsRoot, worldId));
1507
1236
  }
1508
1237
  catch (error) {
1509
- if (isNodeError(error) && error.code === "ENOENT") {
1510
- const worldId = `world-${operationDigest(operationId).slice(0, 24)}`;
1511
- try {
1512
- publication = await readPublicationAt(join(this.#worldsRoot, worldId));
1513
- }
1514
- catch (worldError) {
1515
- if (isNodeError(worldError) && worldError.code === "ENOENT")
1516
- return null;
1517
- throw worldError;
1518
- }
1519
- if (publication.operationId !== operationId) {
1520
- throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
1521
- }
1522
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
1523
- await publishOperation(this.#operationPath(operationId), publication);
1524
- }
1525
- else {
1526
- throw error;
1527
- }
1238
+ if (isNodeError(error) && error.code === "ENOENT")
1239
+ return null;
1240
+ throw error;
1528
1241
  }
1529
- const worldPublication = await readPublicationAt(join(this.#worldsRoot, publication.worldId));
1530
- if (JSON.stringify(worldPublication) !== JSON.stringify(publication)) {
1531
- throw new FileNativeWorldCreationError("world_corrupt", "Creation operation does not match the world publication record");
1242
+ if (publication.operationId !== operationId) {
1243
+ throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
1532
1244
  }
1533
1245
  return publication;
1534
1246
  }
1535
1247
  #currentSummary(publication) {
1536
1248
  return readWorldSummaryAt(join(this.#worldsRoot, publication.worldId), publication);
1537
1249
  }
1538
- #operationPath(operationId) {
1539
- return join(this.#operationsRoot, `${operationDigest(operationId)}.json`);
1540
- }
1541
- #operationOutcomePath(operationId) {
1542
- return join(this.#operationsRoot, `outcome-${operationDigest(operationId)}.json`);
1543
- }
1544
- #derivationOperationPath(operationId) {
1545
- return join(this.#operationsRoot, `derive-${operationDigest(operationId)}.json`);
1546
- }
1547
1250
  #preview(input, worldId) {
1548
1251
  const openingMessage = genesisOpeningMessage(openingText(input.packageFiles));
1549
1252
  return this.#promptCompiler.preview({
@@ -1681,12 +1384,6 @@ function contentPackageCreationFingerprint(title, files) {
1681
1384
  function operationDigest(operationId) {
1682
1385
  return createHash("sha256").update(operationId).digest("hex");
1683
1386
  }
1684
- async function publishOperation(path, value) {
1685
- const temporary = `${path}.${randomUUID()}.tmp`;
1686
- await writeJson(temporary, value);
1687
- await rename(temporary, path);
1688
- await syncDirectory(dirname(path));
1689
- }
1690
1387
  function isRecord(value) {
1691
1388
  return typeof value === "object" && value !== null && !Array.isArray(value);
1692
1389
  }
@@ -1699,93 +1396,14 @@ function hasRequiredAndOptionalKeys(value, required, optional) {
1699
1396
  return (required.every((key) => Object.hasOwn(value, key)) &&
1700
1397
  Object.keys(value).every((key) => allowed.has(key)));
1701
1398
  }
1702
- function assertOperationOutcomeIntegrity(value) {
1703
- if (!isRecord(value) || typeof value.outcome !== "string")
1704
- throw corruptOperationOutcome();
1705
- if (value.outcome === "in_progress") {
1706
- if (!hasExactKeys(value, ["outcome"]))
1707
- throw corruptOperationOutcome();
1708
- return;
1709
- }
1710
- if (value.outcome === "failed" || value.outcome === "cancelled") {
1711
- if (!hasExactKeys(value, ["outcome"]))
1712
- throw corruptOperationOutcome();
1713
- return;
1714
- }
1715
- if (value.outcome === "acceptance_prepared" ||
1716
- value.outcome === "committed" ||
1717
- value.outcome === "committed_materialization_pending") {
1718
- if (!hasExactKeys(value, [
1719
- "outcome",
1720
- "worldId",
1721
- "parentHead",
1722
- "head",
1723
- "commitDigest",
1724
- "historyAppend",
1725
- "nextAdditionalMaterials",
1726
- "mode",
1727
- ]) ||
1728
- typeof value.worldId !== "string" ||
1729
- value.worldId.length === 0 ||
1730
- typeof value.parentHead !== "string" ||
1731
- !/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
1732
- typeof value.head !== "string" ||
1733
- !/^commit:[1-9][0-9]*$/u.test(value.head) ||
1734
- typeof value.commitDigest !== "string" ||
1735
- !/^[a-f0-9]{64}$/u.test(value.commitDigest) ||
1736
- !Array.isArray(value.historyAppend) ||
1737
- !value.historyAppend.every(isPlayHistoryAppendInput) ||
1738
- !Array.isArray(value.nextAdditionalMaterials) ||
1739
- !value.nextAdditionalMaterials.every(isAuthorityMaterialSelection) ||
1740
- !(value.mode === "play" ||
1741
- value.mode === "correction" ||
1742
- value.mode === "timeline_revision"))
1743
- throw corruptOperationOutcome();
1744
- return;
1745
- }
1746
- throw corruptOperationOutcome();
1747
- }
1748
- function projectOperationOutcome(value) {
1749
- return structuredClone(value);
1750
- }
1751
- async function readAcceptancePreparedOutcome(path) {
1752
- const value = await readOptionalJson(path);
1753
- if (value === null)
1754
- return null;
1755
- assertOperationOutcomeIntegrity(value);
1756
- if (value.outcome !== "acceptance_prepared")
1757
- return null;
1758
- return structuredClone(value);
1759
- }
1760
- async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
1761
- const prepared = value;
1762
- const worldRoot = join(worldsRoot, prepared.worldId);
1763
- const authorityStore = new FileNativeAuthorityV3(worldRoot);
1764
- const authority = await authorityStore.readHead();
1765
- if (authority.head === prepared.parentHead)
1766
- return { outcome: "in_progress" };
1767
- if (authority.operationId !== operationId)
1768
- throw corruptOperationOutcome();
1769
- const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, prepared);
1770
- if (authority.sequence < commit.sequence)
1771
- throw corruptOperationOutcome();
1772
- const materialized = await readMaterializedCheckpoint(worldRoot);
1773
- const outcome = materialized !== null && materialized.sequence >= commit.sequence
1774
- ? "committed"
1775
- : "committed_materialization_pending";
1776
- const resolved = { ...structuredClone(prepared), outcome };
1777
- if (outcome === "committed_materialization_pending")
1778
- await assertPendingMaterializationCompatible(worldRoot, resolved);
1779
- return resolved;
1780
- }
1781
1399
  function corruptOperationOutcome() {
1782
1400
  return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
1783
1401
  }
1784
1402
  async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1785
- const fact = await new FileNativeAuthorityV3(worldRoot).authorityFactAt(outcome.head);
1403
+ const fact = await new WorldLedgerAuthority(worldRoot).authorityFactAt(outcome.head);
1786
1404
  const { commit } = fact;
1787
1405
  if (fact.digest !== outcome.commitDigest ||
1788
- commit.auditParent.head !== outcome.parentHead ||
1406
+ commit.parentHead !== outcome.parentHead ||
1789
1407
  commit.head !== outcome.head ||
1790
1408
  commit.mode !== outcome.mode ||
1791
1409
  !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
@@ -1797,143 +1415,6 @@ async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1797
1415
  throw corruptOperationOutcome();
1798
1416
  return commit;
1799
1417
  }
1800
- async function assertPendingMaterializationCompatible(worldRoot, outcome) {
1801
- const authority = new FileNativeAuthorityV3(worldRoot);
1802
- await assertCommittedAuthorityOutcomeMatches(worldRoot, outcome);
1803
- const [parent, target, state, history] = await Promise.all([
1804
- authority.recover(outcome.parentHead),
1805
- authority.recover(outcome.head),
1806
- readTree(join(worldRoot, "state")),
1807
- readTree(join(worldRoot, "history")),
1808
- ]);
1809
- assertProjectionBetweenEndpoints("state", state, parent.state, target.state);
1810
- assertHistoryProjectionBetweenEndpoints(history, historySurfaceFiles(parent.history), historySurfaceFiles(target.history));
1811
- const existingMaterials = await readOptionalJson(join(worldRoot, "runtime", "additional-materials.json"));
1812
- if (existingMaterials !== null &&
1813
- !((existingMaterials.head === parent.head &&
1814
- isDeepStrictEqual(existingMaterials.items, parent.additionalMaterials)) ||
1815
- (existingMaterials.head === target.head &&
1816
- isDeepStrictEqual(existingMaterials.items, target.additionalMaterials))))
1817
- throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
1818
- }
1819
- function assertProjectionBetweenEndpoints(surface, current, parent, target) {
1820
- const parentFiles = new Map(parent.map((file) => [file.path, file.contents]));
1821
- const targetFiles = new Map(target.map((file) => [file.path, file.contents]));
1822
- for (const file of current) {
1823
- if (file.contents !== parentFiles.get(file.path) &&
1824
- file.contents !== targetFiles.get(file.path))
1825
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World ${surface} projection conflicts with both accepted endpoints: ${file.path}`);
1826
- }
1827
- }
1828
- function assertHistoryProjectionBetweenEndpoints(current, parent, target) {
1829
- const byMessageId = (files) => new Map(files.map((file) => [
1830
- fileNativeHistoryMessageIdFromProjectionPath(file.path),
1831
- file.contents,
1832
- ]));
1833
- const parentFiles = byMessageId(parent);
1834
- const targetFiles = byMessageId(target);
1835
- const seen = new Set();
1836
- for (const file of current) {
1837
- const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1838
- if (messageId === null ||
1839
- seen.has(messageId) ||
1840
- (file.contents !== parentFiles.get(messageId) &&
1841
- file.contents !== targetFiles.get(messageId)))
1842
- throw new FileNativeWorldCreationError("inconsistent_materialization", `World history projection conflicts with both accepted endpoints: ${file.path}`);
1843
- seen.add(messageId);
1844
- }
1845
- }
1846
- function isAuthorityMaterialSelection(value) {
1847
- if (!isRecord(value))
1848
- return false;
1849
- if (value.kind === "document")
1850
- return (hasExactKeys(value, ["kind", "document"]) &&
1851
- typeof value.document === "string");
1852
- if (value.kind === "node")
1853
- return (hasExactKeys(value, ["kind", "document", "locator"]) &&
1854
- typeof value.document === "string" &&
1855
- isAuthorityMaterialLocator(value.locator));
1856
- if (value.kind === "history_message")
1857
- return (hasExactKeys(value, ["kind", "message"]) &&
1858
- typeof value.message === "string");
1859
- return (value.kind === "history_commit" &&
1860
- hasExactKeys(value, ["kind", "commit"]) &&
1861
- typeof value.commit === "string");
1862
- }
1863
- function isAuthorityMaterialLocator(value) {
1864
- return (isRecord(value) &&
1865
- hasRequiredAndOptionalKeys(value, [], ["yaml", "markdown"]) &&
1866
- (value.yaml === undefined ||
1867
- (Array.isArray(value.yaml) &&
1868
- value.yaml.every((segment) => typeof segment === "string" || Number.isSafeInteger(segment)))) &&
1869
- (value.markdown === undefined ||
1870
- (Array.isArray(value.markdown) &&
1871
- value.markdown.every((segment) => typeof segment === "string"))));
1872
- }
1873
- async function publishJson(path, value) {
1874
- const temporary = `${path}.${randomUUID()}.tmp`;
1875
- await writeJson(temporary, value);
1876
- await rename(temporary, path);
1877
- await syncDirectory(dirname(path));
1878
- }
1879
- async function readMaterializedCheckpoint(root) {
1880
- const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
1881
- if (value === null)
1882
- return null;
1883
- if (!isRecord(value) ||
1884
- !hasRequiredAndOptionalKeys(value, ["schemaVersion", "head", "sequence", "commitDigest"], ["historyBackgroundCount"]) ||
1885
- (value.historyBackgroundCount !== undefined &&
1886
- (!Number.isSafeInteger(value.historyBackgroundCount) ||
1887
- Number(value.historyBackgroundCount) < 1)) ||
1888
- value.schemaVersion !== 3 ||
1889
- typeof value.head !== "string" ||
1890
- !Number.isSafeInteger(value.sequence) ||
1891
- Number(value.sequence) < 0 ||
1892
- (value.commitDigest !== null &&
1893
- (typeof value.commitDigest !== "string" ||
1894
- !/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
1895
- (Number(value.sequence) === 0
1896
- ? value.head !== "genesis" || value.commitDigest !== null
1897
- : value.head !== `commit:${String(value.sequence)}` ||
1898
- value.commitDigest === null))
1899
- throw new FileNativeWorldCreationError("world_corrupt", "Materialized world checkpoint has an invalid shape");
1900
- return value;
1901
- }
1902
- function sameMaterializedEndpoint(checkpoint, authority) {
1903
- return (checkpoint !== null &&
1904
- checkpoint.head === authority.head &&
1905
- checkpoint.sequence === authority.sequence &&
1906
- checkpoint.commitDigest === authority.commitDigest);
1907
- }
1908
- function isPlayHistoryAppendInput(value) {
1909
- return (isRecord(value) &&
1910
- hasRequiredAndOptionalKeys(value, ["role", "exactText"], ["background"]) &&
1911
- (value.background === undefined ||
1912
- (isHistoryBackground(value.background) &&
1913
- (value.role !== "player" || value.background.kind === "snapshot"))) &&
1914
- (value.role === "player" || value.role === "narrator") &&
1915
- typeof value.exactText === "string");
1916
- }
1917
- async function materializeRecoveredEndpoint(root, authority, endpoint, lastActivityAt = null) {
1918
- if (authority.head !== endpoint.head)
1919
- throw new FileNativeWorldCreationError("world_corrupt", "Recovered endpoint does not match the Authority head to materialize");
1920
- await replaceMaterializedTree(join(root, "state"), endpoint.state);
1921
- crashAtFileNativeAuthorityEdge("after_state_materialization");
1922
- await replaceMaterializedTree(join(root, "history"), historySurfaceFiles(endpoint.history));
1923
- await replaceMaterializedTree(join(root, "runtime", "history-backgrounds"), projectHistoryBackgrounds(endpoint.history));
1924
- await publishJson(join(root, "runtime", "additional-materials.json"), {
1925
- head: authority.head,
1926
- items: endpoint.additionalMaterials,
1927
- });
1928
- await publishJson(join(root, "runtime", "materialized-head.json"), {
1929
- schemaVersion: 3,
1930
- head: authority.head,
1931
- sequence: authority.sequence,
1932
- commitDigest: authority.commitDigest,
1933
- ...backgroundCount(endpoint.history),
1934
- });
1935
- await updateWorldActivity(root, authority.head, endpoint.history, lastActivityAt);
1936
- }
1937
1418
  async function replaceMaterializedTree(root, files) {
1938
1419
  const next = new Map();
1939
1420
  for (const file of files) {
@@ -1953,34 +1434,19 @@ async function replaceMaterializedTree(root, files) {
1953
1434
  continue;
1954
1435
  const path = join(root, file.path);
1955
1436
  await rm(path, { force: true });
1956
- await syncDirectory(dirname(path));
1957
1437
  }
1958
1438
  }
1959
1439
  async function publishText(path, contents) {
1960
1440
  await mkdir(dirname(path), { recursive: true });
1961
1441
  const temporary = `${path}.${randomUUID()}.tmp`;
1962
1442
  await writeFile(temporary, contents, "utf8");
1963
- await syncFile(temporary);
1964
1443
  await rename(temporary, path);
1965
- await syncDirectory(dirname(path));
1966
1444
  }
1967
1445
  function historySurfaceFiles(messages) {
1968
1446
  return projectFileNativeHistorySurface(messages, (messageId) => {
1969
1447
  throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${messageId}`);
1970
1448
  });
1971
1449
  }
1972
- function materializedHistoryRecord(files) {
1973
- const result = {};
1974
- for (const file of files) {
1975
- const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
1976
- if (messageId === null)
1977
- throw new FileNativeWorldCreationError("world_corrupt", `Materialized history path is invalid: ${file.path}`);
1978
- if (messageId in result)
1979
- throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
1980
- result[messageId] = file.contents;
1981
- }
1982
- return result;
1983
- }
1984
1450
  async function writeIdempotentText(path, contents) {
1985
1451
  try {
1986
1452
  const existing = await readFile(path, "utf8");
@@ -1992,15 +1458,9 @@ async function writeIdempotentText(path, contents) {
1992
1458
  throw error;
1993
1459
  const temporary = `${path}.${randomUUID()}.tmp`;
1994
1460
  await writeFile(temporary, contents, "utf8");
1995
- await syncFile(temporary);
1996
1461
  await rename(temporary, path);
1997
- await syncDirectory(dirname(path));
1998
1462
  }
1999
1463
  }
2000
- function isCommittedOutcome(outcome) {
2001
- return (outcome.outcome === "committed" ||
2002
- outcome.outcome === "committed_materialization_pending");
2003
- }
2004
1464
  function assertSameOperationOutcome(outcome, input) {
2005
1465
  if (outcome.worldId !== input.worldId ||
2006
1466
  outcome.parentHead !== input.parentHead ||
@@ -2037,8 +1497,7 @@ function authorityCommitMatchesInput(commit, input) {
2037
1497
  const candidate = input.stateChanges[index];
2038
1498
  if (candidate === undefined)
2039
1499
  return false;
2040
- const { nextBlob, ...metadata } = stored;
2041
- const expected = {
1500
+ return isDeepStrictEqual(stored, {
2042
1501
  kind: candidate.kind,
2043
1502
  documentId: candidate.documentId,
2044
1503
  stableShortRef: candidate.stableShortRef,
@@ -2046,10 +1505,7 @@ function authorityCommitMatchesInput(commit, input) {
2046
1505
  codec: candidate.codec,
2047
1506
  expectedPreviousHash: candidate.expectedPreviousHash,
2048
1507
  nextHash: candidate.nextHash,
2049
- };
2050
- return (nextBlob.epoch === commit.sequence &&
2051
- `sha256:${nextBlob.digest}` === candidate.nextHash &&
2052
- isDeepStrictEqual(metadata, expected));
1508
+ });
2053
1509
  });
2054
1510
  const expectedCorrection = input.mode === "correction"
2055
1511
  ? input.stateChanges.map(({ documentId }) => documentId)
@@ -2057,9 +1513,7 @@ function authorityCommitMatchesInput(commit, input) {
2057
1513
  return (commit.mode === input.mode &&
2058
1514
  commit.playContext === input.playContext &&
2059
1515
  commit.worldClock === input.worldClock &&
2060
- commit.auditParent.head === input.parentHead &&
2061
- commit.timelineParent.head ===
2062
- (input.timelineRevision?.restoresHead ?? input.parentHead) &&
1516
+ commit.parentHead === input.parentHead &&
2063
1517
  isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
2064
1518
  role,
2065
1519
  exactText,
@@ -2067,12 +1521,48 @@ function authorityCommitMatchesInput(commit, input) {
2067
1521
  })), input.historyAppend) &&
2068
1522
  stateChangesMatch &&
2069
1523
  isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
2070
- isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
2071
1524
  isDeepStrictEqual(commit.narrativeCheckpoint, input.narrativeCheckpoint) &&
2072
1525
  isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
2073
1526
  commit.corrects ===
2074
1527
  (input.mode === "correction" ? input.parentHead : undefined));
2075
1528
  }
1529
+ function headPosition(head) {
1530
+ if (head === "genesis")
1531
+ return 0;
1532
+ const match = /^commit:([1-9][0-9]*)$/u.exec(head);
1533
+ if (match === null)
1534
+ throw new TypeError(`Endpoint is invalid: ${head}`);
1535
+ return Number.parseInt(match[1], 10);
1536
+ }
1537
+ // 分叉只复制一个世界的闭包;回退与身份重分配由调用方在副本上完成。
1538
+ const forkClosureEntries = [
1539
+ "state",
1540
+ "control",
1541
+ "history",
1542
+ join("runtime", "head.json"),
1543
+ join("runtime", "turns"),
1544
+ join("runtime", "history-backgrounds"),
1545
+ join("runtime", "additional-materials.json"),
1546
+ join("runtime", "genesis.json"),
1547
+ join("runtime", "play-genesis-timeline.json"),
1548
+ "artifacts",
1549
+ ];
1550
+ async function copyForkClosure(sourceRoot, target) {
1551
+ await mkdir(join(target, "runtime"), { recursive: true, mode: 0o700 });
1552
+ for (const entry of forkClosureEntries) {
1553
+ const source = join(sourceRoot, entry);
1554
+ try {
1555
+ await stat(source);
1556
+ }
1557
+ catch (error) {
1558
+ if (isNodeError(error) && error.code === "ENOENT")
1559
+ continue;
1560
+ throw error;
1561
+ }
1562
+ await mkdir(dirname(join(target, entry)), { recursive: true, mode: 0o700 });
1563
+ await cp(source, join(target, entry), { recursive: true });
1564
+ }
1565
+ }
2076
1566
  function openWorldDocumentSnapshot(files) {
2077
1567
  return WorldDocumentStore.open({
2078
1568
  layout: "world_state",
@@ -2089,36 +1579,11 @@ async function writeJson(path, value) {
2089
1579
  encoding: "utf8",
2090
1580
  mode: 0o600,
2091
1581
  });
2092
- await syncFile(path);
2093
1582
  }
2094
- async function syncFile(path) {
2095
- const handle = await open(path, "r");
2096
- try {
2097
- await handle.sync();
2098
- }
2099
- finally {
2100
- await handle.close();
2101
- }
2102
- }
2103
- async function syncDirectory(path) {
2104
- const handle = await open(path, "r");
2105
- try {
2106
- await handle.sync();
2107
- }
2108
- finally {
2109
- await handle.close();
2110
- }
2111
- }
2112
- async function durableTree(root) {
2113
- const entries = await readdir(root, { withFileTypes: true });
2114
- for (const entry of entries) {
2115
- const path = join(root, entry.name);
2116
- if (entry.isDirectory())
2117
- await durableTree(path);
2118
- else if (entry.isFile())
2119
- await syncFile(path);
2120
- }
2121
- await syncDirectory(root);
1583
+ async function publishJson(path, value) {
1584
+ const temporary = `${path}.${randomUUID()}.tmp`;
1585
+ await writeJson(temporary, value);
1586
+ await rename(temporary, path);
2122
1587
  }
2123
1588
  async function readGenesisForFork(worldRoot, worldId) {
2124
1589
  const value = await readJson(join(worldRoot, "runtime", "genesis.json"));
@@ -2191,15 +1656,15 @@ async function readPublicationAt(root) {
2191
1656
  async function readWorldSummaryAt(root, publication, serializeRebuild) {
2192
1657
  const published = publication ?? (await readPublicationAt(root));
2193
1658
  const gameplay = await readWorldGameplayAt(root);
2194
- const { lastActivityAt, excerpt, playerTurns } = await readWorldActivity(root, serializeRebuild);
2195
- const summary = {
1659
+ const activity = await readWorldActivity(root, serializeRebuild);
1660
+ return {
2196
1661
  ...toSummary(published, gameplay),
2197
1662
  title: await readWorldTitleAt(root, published),
2198
- lastActivityAt,
2199
- excerpt,
2200
- playerTurns,
1663
+ lastActivityAt: activity.lastActivityAt,
1664
+ excerpt: activity.excerpt,
1665
+ playerTurns: activity.playerTurns,
1666
+ ...(activity.pendingUpgrade === true ? { pendingUpgrade: true } : {}),
2201
1667
  };
2202
- return summary;
2203
1668
  }
2204
1669
  async function readWorldTitleAt(root, published) {
2205
1670
  const metadata = await readOptionalJson(join(root, localMetadataFile));
@@ -2318,36 +1783,20 @@ function assertRelativePath(path) {
2318
1783
  function isNodeError(error) {
2319
1784
  return error instanceof Error && "code" in error;
2320
1785
  }
2321
- function crashAtFileNativeWorldCreationEdge(edge) {
2322
- if (process.env.NODE_ENV === "test" &&
2323
- process.env
2324
- .NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_WORLD_CREATION_EDGE === edge) {
2325
- process.kill(process.pid, "SIGKILL");
2326
- }
2327
- }
2328
- function crashAtFileNativeAuthorityEdge(edge) {
2329
- if (process.env.NODE_ENV === "test" &&
2330
- process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_AUTHORITY_EDGE ===
2331
- edge) {
2332
- process.kill(process.pid, "SIGKILL");
2333
- }
2334
- }
2335
1786
  import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
2336
1787
  function backgroundField(background) {
2337
1788
  return background === undefined ? {} : { background };
2338
1789
  }
2339
- function materializedBackgroundRecord(files, history) {
2340
- return Object.fromEntries(files.map(({ path, contents }) => {
2341
- const id = path.replace(/\.json$/u, "");
2342
- const value = JSON.parse(contents);
2343
- if (path !== `${id}.json` ||
2344
- !Object.hasOwn(history, id) ||
2345
- !isHistoryBackground(value))
2346
- throw new FileNativeWorldCreationError("world_corrupt", "Materialized history background is invalid or unreachable");
2347
- return [id, value];
2348
- }));
1790
+ function playerRevisionIntentPath(worldsRoot, worldId) {
1791
+ return join(worldsRoot, worldId, "runtime", "player-revision-intent.json");
2349
1792
  }
2350
- function backgroundCount(messages) {
2351
- const count = messages.filter(({ background }) => background !== undefined).length;
2352
- return count === 0 ? {} : { historyBackgroundCount: count };
1793
+ async function readOptionalJsonFile(path) {
1794
+ try {
1795
+ return JSON.parse(await readFile(path, "utf8"));
1796
+ }
1797
+ catch (error) {
1798
+ if (error.code === "ENOENT")
1799
+ return null;
1800
+ throw error;
1801
+ }
2353
1802
  }