narraeon 0.5.5 → 0.5.7

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 (49) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/node/protocol/appPreferences.js +4 -2
  4. package/dist/node/protocol/playFailure.js +13 -0
  5. package/dist/node/protocol/protagonistTemplates.js +13 -0
  6. package/dist/node/protocol/v1.js +25 -19
  7. package/dist/node/protocol/worldArchives.js +7 -0
  8. package/dist/node/runtime/V1Runtime.js +48 -6
  9. package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
  10. package/dist/node/runtime/archive/WorldArchives.js +389 -0
  11. package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
  12. package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
  13. package/dist/node/runtime/content/ContentWorkspace.js +5 -0
  14. package/dist/node/runtime/history/HistoryBackground.js +84 -0
  15. package/dist/node/runtime/history/HistoryQuery.js +7 -2
  16. package/dist/node/runtime/history/HistoryRendering.js +31 -4
  17. package/dist/node/runtime/history/StoryExport.js +33 -0
  18. package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
  19. package/dist/node/runtime/model/ModelHost.js +12 -1
  20. package/dist/node/runtime/model/ModelHostStream.js +11 -3
  21. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +2 -0
  22. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +5 -0
  23. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +91 -4
  24. package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
  25. package/dist/node/runtime/play/PlayCallChain.js +212 -61
  26. package/dist/node/runtime/play/PlayCallChainError.js +3 -1
  27. package/dist/node/runtime/play/PlayContinuity.js +3 -20
  28. package/dist/node/runtime/play/PlayFailure.js +20 -0
  29. package/dist/node/runtime/play/PlayTraceRetention.js +10 -11
  30. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +12 -59
  31. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +2 -2
  32. package/dist/node/runtime/world/FileNativeAuthorityV3.js +7 -4
  33. package/dist/node/runtime/world/FileNativeHistoryProjection.js +11 -0
  34. package/dist/node/runtime/world/FileNativeWorldStore.js +250 -55
  35. package/dist/node/runtime/world/WorldActivity.js +143 -0
  36. package/dist/node/runtime/world/WorldYamlRendering.js +28 -0
  37. package/dist/node/server/createServer.js +48 -0
  38. package/dist/node/shared/default-preset-host.js +18 -8
  39. package/dist/node/shared/default-setting-improvement-prompt.js +4 -0
  40. package/dist/node/shared/history-use-guidance.js +2 -2
  41. package/dist/node/shared/ordered-play-prompts.js +2 -2
  42. package/dist/node/shared/sample-packages/index.js +10 -0
  43. package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
  44. package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
  45. package/dist/web/assets/index-DxNtxzLR.js +1255 -0
  46. package/dist/web/assets/{index-CVN48xrw.css → index-DzOnu9uv.css} +1 -1
  47. package/dist/web/index.html +2 -2
  48. package/package.json +6 -2
  49. package/dist/web/assets/index-DTFZQan4.js +0 -1223
@@ -1,6 +1,9 @@
1
+ import { captureNarrativeOrigin, snapshotBackground, isHistoryBackground, } from "../history/HistoryBackground.js";
1
2
  import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
2
3
  import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
3
4
  import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
5
+ import { HistoryQuery } from "../history/HistoryQuery.js";
6
+ import { exportStory } from "../history/StoryExport.js";
4
7
  import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
5
8
  import { createHash, randomUUID } from "node:crypto";
6
9
  import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
@@ -10,12 +13,13 @@ import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, }
10
13
  import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
11
14
  import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
12
15
  import { WorldDocumentStore } from "./WorldDocumentStore.js";
16
+ import { readWorldActivity, updateWorldActivity, } from "./WorldActivity.js";
13
17
  import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
14
18
  import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
15
19
  import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
16
20
  import { FileNativeAuthorityV3, FileNativeAuthorityV3Error, } from "./FileNativeAuthorityV3.js";
17
21
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
18
- import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, } from "./FileNativeHistoryProjection.js";
22
+ import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
19
23
  const publicationFile = "publication.json";
20
24
  const localMetadataFile = "local.json";
21
25
  export class FileNativeWorldCreationError extends Error {
@@ -40,6 +44,7 @@ export class FileNativeWorldStore {
40
44
  #promptCompiler;
41
45
  #activeControlUsers = new Map();
42
46
  #openingWorldRevisions = new Set();
47
+ #archiveExports = new Set();
43
48
  operations;
44
49
  playTimeline;
45
50
  playAdvances;
@@ -111,16 +116,40 @@ export class FileNativeWorldStore {
111
116
  throw new FileNativeWorldCreationError("world_corrupt", "Released world storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
112
117
  }
113
118
  }
119
+ async readWorldSummary(worldId) {
120
+ assertIdentity(worldId, "World ID");
121
+ return readWorldSummaryAt(join(this.#worldsRoot, worldId));
122
+ }
114
123
  async listWorlds() {
115
124
  await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
116
- const entries = await readdir(this.#worldsRoot, { withFileTypes: true });
125
+ const entries = (await readdir(this.#worldsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
117
126
  const worlds = [];
118
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
119
- if (!entry.isDirectory() || entry.name.startsWith("."))
120
- continue;
121
- worlds.push(await readWorldSummaryAt(join(this.#worldsRoot, entry.name)));
127
+ // Bound concurrent shell/head/cache reads even in a large workspace.
128
+ for (let offset = 0; offset < entries.length; offset += 16) {
129
+ worlds.push(...(await Promise.all(entries
130
+ .slice(offset, offset + 16)
131
+ .map((entry) => readWorldSummaryAt(join(this.#worldsRoot, entry.name), undefined, (rebuild) => this.operations.withWorldAuthorityLock(entry.name, rebuild))))));
122
132
  }
123
- return worlds;
133
+ return worlds.sort((left, right) => (right.lastActivityAt ?? -1) - (left.lastActivityAt ?? -1) ||
134
+ left.title.localeCompare(right.title) ||
135
+ left.worldId.localeCompare(right.worldId));
136
+ }
137
+ /** Explicit full-history read; independent of page and model projections. */
138
+ async exportStory(worldId, format, locale) {
139
+ assertIdentity(worldId, "World ID");
140
+ await this.ensureCurrentStorage(worldId);
141
+ return this.operations.withWorldAuthorityLock(worldId, async () => {
142
+ const root = join(this.#worldsRoot, worldId);
143
+ const publication = await readPublicationAt(root);
144
+ const title = await readWorldTitleAt(root, publication);
145
+ const endpoint = await new FileNativeAuthorityV3(root).recoverHeadResult();
146
+ const history = new HistoryQuery(worldId, endpoint.history.map(({ messageId, role, exactText }) => ({
147
+ id: messageId,
148
+ role,
149
+ text: exactText,
150
+ })));
151
+ return exportStory(title, history.messages, format, locale);
152
+ });
124
153
  }
125
154
  async renameWorld(worldId, name) {
126
155
  assertIdentity(worldId, "World ID");
@@ -143,7 +172,7 @@ export class FileNativeWorldStore {
143
172
  worldId,
144
173
  name: trimmed,
145
174
  });
146
- return { ...toSummary(publication), title: trimmed };
175
+ return readWorldSummaryAt(root, publication);
147
176
  });
148
177
  }
149
178
  /**
@@ -266,7 +295,10 @@ export class FileNativeWorldStore {
266
295
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
267
296
  await rm(stagingRoot, { recursive: true, force: true });
268
297
  try {
269
- const openingMessage = genesisOpeningMessage(openingText(packageFiles));
298
+ const openingMessage = {
299
+ ...genesisOpeningMessage(openingText(packageFiles)),
300
+ ...backgroundField(snapshotBackground(captureNarrativeOrigin(inspection.worldDocumentSnapshot, 1).value)),
301
+ };
270
302
  const initialMaterials = [
271
303
  { kind: "history_message", message: openingMessage.messageId },
272
304
  ];
@@ -276,6 +308,7 @@ export class FileNativeWorldStore {
276
308
  await mkdir(join(stagingRoot, "history"), { recursive: true });
277
309
  await mkdir(join(stagingRoot, "runtime"), { recursive: true });
278
310
  await writeIdempotentText(join(stagingRoot, "history", historySurfaceFiles([openingMessage])[0].path), openingMessage.exactText);
311
+ await replaceMaterializedTree(join(stagingRoot, "runtime", "history-backgrounds"), projectHistoryBackgrounds([openingMessage]));
279
312
  const genesis = {
280
313
  schemaVersion: 1,
281
314
  type: "file_native_genesis",
@@ -304,6 +337,7 @@ export class FileNativeWorldStore {
304
337
  head: "genesis",
305
338
  sequence: 0,
306
339
  commitDigest: null,
340
+ ...backgroundCount([openingMessage]),
307
341
  });
308
342
  await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
309
343
  const publication = {
@@ -315,13 +349,18 @@ export class FileNativeWorldStore {
315
349
  sourceFingerprint,
316
350
  };
317
351
  await writeJson(join(stagingRoot, publicationFile), publication);
352
+ await updateWorldActivity(stagingRoot, "genesis", [openingMessage], Date.now());
318
353
  await durableTree(stagingRoot);
319
354
  crashAtFileNativeWorldCreationEdge("after_staging");
320
355
  await rename(stagingRoot, finalRoot);
321
356
  await syncDirectory(this.#worldsRoot);
322
357
  crashAtFileNativeWorldCreationEdge("after_atomic_publish");
323
358
  await publishOperation(operationPath, publication);
324
- return { outcome: "created", world: toSummary(publication), preview };
359
+ return {
360
+ outcome: "created",
361
+ world: await this.#currentSummary(publication),
362
+ preview,
363
+ };
325
364
  }
326
365
  catch (error) {
327
366
  await rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
@@ -392,6 +431,31 @@ export class FileNativeWorldStore {
392
431
  await readPublicationAt(root);
393
432
  return (await new FileNativeAuthorityV3(root).readHead()).head;
394
433
  }
434
+ async withArchiveExport(worldId, action) {
435
+ assertIdentity(worldId, "World ID");
436
+ if ((this.#activeControlUsers.get(worldId)?.size ?? 0) > 0 ||
437
+ this.#archiveExports.has(worldId) ||
438
+ this.#openingWorldRevisions.has(worldId))
439
+ throw new FileNativeWorldCreationError("operation_conflict", "Wait for the current generation or world operation before exporting a backup");
440
+ this.#archiveExports.add(worldId);
441
+ try {
442
+ if (await this.readWorldRevisionLock(worldId))
443
+ throw new FileNativeWorldCreationError("operation_conflict", "Please apply or discard the world revision before exporting a backup");
444
+ return await action();
445
+ }
446
+ finally {
447
+ this.#archiveExports.delete(worldId);
448
+ }
449
+ }
450
+ async archiveNeedsFreshContext(worldId) {
451
+ assertIdentity(worldId, "World ID");
452
+ return ((await readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "archive-import.json"))) !== null ||
453
+ (await this.playTimeline.currentContextRequiresFreshContext(worldId)));
454
+ }
455
+ async completeArchiveFreshContext(worldId) {
456
+ assertIdentity(worldId, "World ID");
457
+ await rm(join(this.#worldsRoot, worldId, "runtime", "archive-import.json"), { force: true });
458
+ }
395
459
  async currentHeadOperationId(worldId) {
396
460
  assertIdentity(worldId, "World ID");
397
461
  await this.ensureCurrentStorage(worldId);
@@ -425,7 +489,8 @@ export class FileNativeWorldStore {
425
489
  }
426
490
  async acquireWorldRevisionLock(worldId, epochId) {
427
491
  if ((this.#activeControlUsers.get(worldId)?.size ?? 0) > 0 ||
428
- this.#openingWorldRevisions.has(worldId))
492
+ this.#openingWorldRevisions.has(worldId) ||
493
+ this.#archiveExports.has(worldId))
429
494
  return { kind: "busy" };
430
495
  this.#openingWorldRevisions.add(worldId);
431
496
  try {
@@ -581,6 +646,8 @@ export class FileNativeWorldStore {
581
646
  freezeControl(worldId, operationId) {
582
647
  assertIdentity(worldId, "World ID");
583
648
  assertIdentity(operationId, "operation ID");
649
+ if (this.#archiveExports.has(worldId))
650
+ throw new FileNativeWorldCreationError("operation_conflict", "A world backup is being exported; wait for it to finish");
584
651
  if (this.#openingWorldRevisions.has(worldId))
585
652
  throw new FileNativeWorldCreationError("operation_conflict", "World control is locked while a world revision opens");
586
653
  const users = this.#activeControlUsers.get(worldId) ?? new Set();
@@ -634,7 +701,9 @@ export class FileNativeWorldStore {
634
701
  const authorityStore = new FileNativeAuthorityV3(root);
635
702
  const authority = await authorityStore.readHead();
636
703
  const checkpoint = await readMaterializedCheckpoint(root);
637
- if (!sameMaterializedEndpoint(checkpoint, authority)) {
704
+ const cachedBackgrounds = await readTree(join(root, "runtime", "history-backgrounds"));
705
+ if (!sameMaterializedEndpoint(checkpoint, authority) ||
706
+ (checkpoint?.historyBackgroundCount ?? 0) !== cachedBackgrounds.length) {
638
707
  const recovered = await authorityStore.recover();
639
708
  let operation = null;
640
709
  if (authority.operationId !== null && authority.sequence > 0) {
@@ -654,11 +723,12 @@ export class FileNativeWorldStore {
654
723
  if (operation !== null)
655
724
  await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
656
725
  }
657
- const [state, control, history, materials] = await Promise.all([
726
+ const [state, control, history, materials, backgrounds] = await Promise.all([
658
727
  readTree(join(root, "state")),
659
728
  readTree(join(root, "control")),
660
729
  readTree(join(root, "history")),
661
730
  readOptionalJson(join(root, "runtime", "additional-materials.json")),
731
+ readTree(join(root, "runtime", "history-backgrounds")),
662
732
  ]);
663
733
  if ((materials?.head ?? "genesis") !== authority.head)
664
734
  throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
@@ -672,6 +742,7 @@ export class FileNativeWorldStore {
672
742
  }, {}),
673
743
  additionalMaterials: materials?.items ?? [],
674
744
  history: materializedHistoryRecord(history),
745
+ historyBackgrounds: materializedBackgroundRecord(backgrounds, materializedHistoryRecord(history)),
675
746
  narrativeCheckpoint: await authorityStore.readNarrativeCheckpoint(),
676
747
  };
677
748
  });
@@ -702,6 +773,7 @@ export class FileNativeWorldStore {
702
773
  }, {}),
703
774
  additionalMaterials: structuredClone(endpoint.additionalMaterials),
704
775
  narrativeCheckpoint: endpoint.narrativeCheckpoint,
776
+ historyBackgrounds: historyBackgroundRecord(endpoint.history),
705
777
  history: Object.fromEntries(endpoint.history.map(({ messageId, exactText }) => [
706
778
  messageId,
707
779
  exactText,
@@ -785,32 +857,51 @@ export class FileNativeWorldStore {
785
857
  if (input.requestDiscriminator !== undefined)
786
858
  assertIdentity(input.requestDiscriminator, "Fork request discriminator");
787
859
  if (input.sourceHead !== "genesis" &&
860
+ input.sourceHead !== "current" &&
788
861
  !/^commit:[1-9][0-9]*$/u.test(input.sourceHead))
789
862
  throw new TypeError("Source endpoint is invalid");
790
- const operationPath = this.#derivationOperationPath(input.operationId);
863
+ if (input.targetName !== undefined && !validWorldName(input.targetName))
864
+ throw new TypeError("Target world name is invalid");
865
+ if (input.publish === false && input.targetWorldRoot === undefined)
866
+ throw new TypeError("An unpublished closure requires a target directory");
867
+ const target = input.targetDataRoot === undefined
868
+ ? this
869
+ : new FileNativeWorldStore(input.targetDataRoot);
870
+ const operationPath = input.identityPrefix === "import"
871
+ ? join(target.#operationsRoot, `import-${operationDigest(input.operationId)}.json`)
872
+ : target.#derivationOperationPath(input.operationId);
791
873
  const requestFingerprint = derivationRequestFingerprint(input);
792
- const worldId = `world-${operationDigest(`derive:${input.operationId}`).slice(0, 24)}`;
793
- const finalRoot = join(this.#worldsRoot, worldId);
794
- const previous = await readOptionalJson(operationPath);
874
+ const worldId = `world-${operationDigest(`${input.identityPrefix ?? "derive"}:${input.operationId}`).slice(0, 24)}`;
875
+ const finalRoot = input.publish === false
876
+ ? resolve(input.targetWorldRoot)
877
+ : join(target.#worldsRoot, worldId);
878
+ const previous = input.publish === false
879
+ ? null
880
+ : await readOptionalJson(operationPath);
795
881
  if (previous !== null) {
796
- await this.ensureCurrentStorage(previous.worldId);
797
- if (!(await derivationPublicationMatches(join(this.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
882
+ await target.ensureCurrentStorage(previous.worldId);
883
+ if (!(await derivationPublicationMatches(join(target.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
798
884
  throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
799
885
  return {
800
886
  outcome: "derived",
801
- world: await this.#currentSummary(previous),
887
+ world: await target.#currentSummary(previous),
888
+ worldRoot: finalRoot,
802
889
  };
803
890
  }
804
891
  const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
805
892
  if (alreadyPublished !== null) {
806
- await this.ensureCurrentStorage(alreadyPublished.worldId);
893
+ if (input.publish !== false)
894
+ await target.ensureCurrentStorage(alreadyPublished.worldId);
807
895
  if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
808
896
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
809
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
810
- await publishJson(operationPath, alreadyPublished);
897
+ if (input.publish !== false) {
898
+ await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
899
+ await publishJson(operationPath, alreadyPublished);
900
+ }
811
901
  return {
812
902
  outcome: "derived",
813
- world: await this.#currentSummary(alreadyPublished),
903
+ world: await readWorldSummaryAt(finalRoot, alreadyPublished),
904
+ worldRoot: finalRoot,
814
905
  };
815
906
  }
816
907
  await this.ensureCurrentStorage(input.sourceWorldId);
@@ -819,28 +910,42 @@ export class FileNativeWorldStore {
819
910
  const sourcePublication = await readPublicationAt(sourceRoot);
820
911
  const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
821
912
  const sourceAuthority = new FileNativeAuthorityV3(sourceRoot);
913
+ const sourceHead = input.sourceHead === "current"
914
+ ? (await sourceAuthority.readHead()).head
915
+ : input.sourceHead;
916
+ if (input.requireMaterialized) {
917
+ const authorityHead = await sourceAuthority.readHead();
918
+ const materialized = await readMaterializedCheckpoint(sourceRoot);
919
+ if (materialized?.head !== authorityHead.head ||
920
+ materialized.commitDigest !== authorityHead.commitDigest)
921
+ throw new FileNativeWorldCreationError("inconsistent_materialization", "Please repair the world before exporting a backup");
922
+ }
822
923
  const [sourceGenesis, control] = await Promise.all([
823
924
  readGenesisForFork(sourceRoot, input.sourceWorldId),
824
925
  readTree(join(sourceRoot, "control")),
825
926
  ]);
826
- const staging = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
827
- await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
828
- await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
927
+ const staging = join(dirname(finalRoot), `.staging-${worldId}-${randomUUID()}`);
928
+ await mkdir(dirname(finalRoot), { recursive: true, mode: 0o700 });
929
+ if (input.publish !== false)
930
+ await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
829
931
  try {
830
932
  await mkdir(join(staging, "runtime"), { recursive: true });
831
933
  const clonedHead = await sourceAuthority.clonePrefixTo({
832
934
  targetWorldRoot: staging,
833
- selectedHead: input.sourceHead,
935
+ selectedHead: sourceHead,
834
936
  operationId: input.operationId,
835
937
  });
836
938
  const selected = await new FileNativeAuthorityV3(staging).recoverHeadResult();
837
939
  await writeSurface(staging, "state", selected.state);
838
940
  await writeSurface(staging, "control", control);
839
- await PackageScriptPermissions.write(staging, await PackageScriptPermissions.read(sourceRoot));
941
+ await PackageScriptPermissions.write(staging, input.resetPackageScriptPermissions
942
+ ? []
943
+ : await PackageScriptPermissions.read(sourceRoot));
840
944
  await WorldExtensionControls.copy(sourceRoot, staging);
841
945
  await mkdir(join(staging, "history"), { recursive: true });
842
946
  for (const file of historySurfaceFiles(selected.history))
843
947
  await writeIdempotentText(join(staging, "history", file.path), file.contents);
948
+ await replaceMaterializedTree(join(staging, "runtime", "history-backgrounds"), projectHistoryBackgrounds(selected.history));
844
949
  const genesis = {
845
950
  schemaVersion: 1,
846
951
  type: "file_native_genesis",
@@ -859,49 +964,63 @@ export class FileNativeWorldStore {
859
964
  history: genesis.history,
860
965
  });
861
966
  await writeJson(join(staging, "runtime", "additional-materials.json"), {
862
- head: input.sourceHead,
967
+ head: sourceHead,
863
968
  items: selected.additionalMaterials,
864
969
  });
865
970
  await writeJson(join(staging, "runtime", "materialized-head.json"), {
866
971
  schemaVersion: 3,
867
- head: input.sourceHead,
972
+ head: sourceHead,
868
973
  sequence: clonedHead.sequence,
869
974
  commitDigest: clonedHead.commitDigest,
975
+ ...backgroundCount(selected.history),
870
976
  });
871
977
  await input.stageTarget?.({
978
+ sourceHead,
872
979
  targetWorldRoot: staging,
873
980
  targetWorldId: worldId,
874
981
  binding: {
875
982
  worldId,
876
- parentHead: input.sourceHead,
983
+ parentHead: sourceHead,
877
984
  files: [...selected.state, ...control].reduce((files, { path, contents }, index) => {
878
985
  files[`${index < selected.state.length ? "state" : "control"}/${path}`] = contents;
879
986
  return files;
880
987
  }, {}),
881
988
  additionalMaterials: structuredClone(selected.additionalMaterials),
989
+ historyBackgrounds: historyBackgroundRecord(selected.history),
882
990
  history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
883
991
  messageId,
884
992
  exactText,
885
993
  ])),
886
994
  },
887
995
  });
996
+ if (input.identityPrefix === "import" ||
997
+ (await this.archiveNeedsFreshContext(input.sourceWorldId)))
998
+ await writeJson(join(staging, "runtime", "archive-import.json"), {
999
+ schemaVersion: 1,
1000
+ });
888
1001
  crashAtFileNativeAuthorityEdge("derivation_after_target_staging");
889
1002
  const publication = {
890
1003
  schemaVersion: 1,
891
1004
  worldId,
892
- title: derivedWorldName(sourceSummary.title),
1005
+ title: input.targetName ?? derivedWorldName(sourceSummary.title),
893
1006
  parentEndpoint: "genesis",
894
1007
  operationId: input.operationId,
895
1008
  sourceFingerprint: requestFingerprint,
896
1009
  };
897
1010
  await writeJson(join(staging, publicationFile), publication);
1011
+ await updateWorldActivity(staging, sourceHead, selected.history, Date.now());
898
1012
  await durableTree(staging);
899
1013
  crashAtFileNativeAuthorityEdge("derivation_before_publish");
900
1014
  await rename(staging, finalRoot);
901
- await syncDirectory(this.#worldsRoot);
1015
+ await syncDirectory(dirname(finalRoot));
902
1016
  crashAtFileNativeAuthorityEdge("derivation_after_publish");
903
- await publishJson(operationPath, publication);
904
- return { outcome: "derived", world: toSummary(publication) };
1017
+ if (input.publish !== false)
1018
+ await publishJson(operationPath, publication);
1019
+ return {
1020
+ outcome: "derived",
1021
+ world: await readWorldSummaryAt(finalRoot, publication),
1022
+ worldRoot: finalRoot,
1023
+ };
905
1024
  }
906
1025
  catch (error) {
907
1026
  await rm(staging, { recursive: true, force: true }).catch(() => undefined);
@@ -909,8 +1028,13 @@ export class FileNativeWorldStore {
909
1028
  if (published !== null) {
910
1029
  if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
911
1030
  throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
912
- await publishJson(operationPath, published);
913
- return { outcome: "derived", world: toSummary(published) };
1031
+ if (input.publish !== false)
1032
+ await publishJson(operationPath, published);
1033
+ return {
1034
+ outcome: "derived",
1035
+ world: await readWorldSummaryAt(finalRoot, published),
1036
+ worldRoot: finalRoot,
1037
+ };
914
1038
  }
915
1039
  throw error;
916
1040
  }
@@ -1032,11 +1156,26 @@ export class FileNativeWorldStore {
1032
1156
  !replaced.historyAppend.some(({ role }) => role === "player") ||
1033
1157
  logicalParent !== input.restoresHead)
1034
1158
  throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
1159
+ const binding = await this.bindPlayCallChainAt(input.worldId, input.restoresHead);
1160
+ const snapshot = WorldDocumentStore.open({
1161
+ layout: "world_state",
1162
+ files: Object.entries(binding.files).map(([path, contents]) => ({
1163
+ path,
1164
+ contents,
1165
+ })),
1166
+ });
1167
+ const background = snapshotBackground(captureNarrativeOrigin(snapshot, 1).value);
1035
1168
  return this.#commitHistoryChange({
1036
1169
  operationId: input.operationId,
1037
1170
  worldId: input.worldId,
1038
1171
  parentHead: input.expectedCurrentHead,
1039
- historyAppend: [{ role: "player", exactText: input.replacementText }],
1172
+ historyAppend: [
1173
+ {
1174
+ role: "player",
1175
+ exactText: input.replacementText,
1176
+ ...backgroundField(background),
1177
+ },
1178
+ ],
1040
1179
  nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
1041
1180
  stateChanges: [],
1042
1181
  mode: "timeline_revision",
@@ -1256,12 +1395,13 @@ export class FileNativeWorldStore {
1256
1395
  });
1257
1396
  crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
1258
1397
  await authorityStore.publishPrepared(preparedAppend);
1398
+ const acceptedAt = Date.now();
1259
1399
  crashAtFileNativeAuthorityEdge("after_commit_acceptance");
1260
1400
  await publishJson(this.#operationOutcomePath(input.operationId), pending);
1261
1401
  try {
1262
1402
  const recovered = await authorityStore.recover(commit.head);
1263
1403
  await assertPendingMaterializationCompatible(root, pending);
1264
- await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered);
1404
+ await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered, acceptedAt);
1265
1405
  crashAtFileNativeAuthorityEdge("after_materialization");
1266
1406
  const outcome = { ...pending, outcome: "committed" };
1267
1407
  await publishJson(this.#operationOutcomePath(input.operationId), outcome);
@@ -1328,9 +1468,10 @@ export class FileNativeWorldStore {
1328
1468
  return {
1329
1469
  endpoint: recovered.head,
1330
1470
  state: recovered.state,
1331
- history: recovered.history.map(({ role, exactText }) => ({
1471
+ history: recovered.history.map(({ role, exactText, background }) => ({
1332
1472
  role,
1333
1473
  exactText,
1474
+ ...backgroundField(background),
1334
1475
  })),
1335
1476
  additionalMaterials: recovered.additionalMaterials,
1336
1477
  };
@@ -1636,7 +1777,11 @@ async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1636
1777
  commit.auditParent.head !== outcome.parentHead ||
1637
1778
  commit.head !== outcome.head ||
1638
1779
  commit.mode !== outcome.mode ||
1639
- !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
1780
+ !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
1781
+ role,
1782
+ exactText,
1783
+ ...backgroundField(background),
1784
+ })), outcome.historyAppend) ||
1640
1785
  !isDeepStrictEqual(commit.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
1641
1786
  throw corruptOperationOutcome();
1642
1787
  return commit;
@@ -1725,12 +1870,10 @@ async function readMaterializedCheckpoint(root) {
1725
1870
  if (value === null)
1726
1871
  return null;
1727
1872
  if (!isRecord(value) ||
1728
- !hasExactKeys(value, [
1729
- "schemaVersion",
1730
- "head",
1731
- "sequence",
1732
- "commitDigest",
1733
- ]) ||
1873
+ !hasRequiredAndOptionalKeys(value, ["schemaVersion", "head", "sequence", "commitDigest"], ["historyBackgroundCount"]) ||
1874
+ (value.historyBackgroundCount !== undefined &&
1875
+ (!Number.isSafeInteger(value.historyBackgroundCount) ||
1876
+ Number(value.historyBackgroundCount) < 1)) ||
1734
1877
  value.schemaVersion !== 3 ||
1735
1878
  typeof value.head !== "string" ||
1736
1879
  !Number.isSafeInteger(value.sequence) ||
@@ -1753,16 +1896,20 @@ function sameMaterializedEndpoint(checkpoint, authority) {
1753
1896
  }
1754
1897
  function isPlayHistoryAppendInput(value) {
1755
1898
  return (isRecord(value) &&
1756
- hasExactKeys(value, ["role", "exactText"]) &&
1899
+ hasRequiredAndOptionalKeys(value, ["role", "exactText"], ["background"]) &&
1900
+ (value.background === undefined ||
1901
+ (isHistoryBackground(value.background) &&
1902
+ (value.role !== "player" || value.background.kind === "snapshot"))) &&
1757
1903
  (value.role === "player" || value.role === "narrator") &&
1758
1904
  typeof value.exactText === "string");
1759
1905
  }
1760
- async function materializeRecoveredEndpoint(root, authority, endpoint) {
1906
+ async function materializeRecoveredEndpoint(root, authority, endpoint, lastActivityAt = null) {
1761
1907
  if (authority.head !== endpoint.head)
1762
1908
  throw new FileNativeWorldCreationError("world_corrupt", "Recovered endpoint does not match the Authority head to materialize");
1763
1909
  await replaceMaterializedTree(join(root, "state"), endpoint.state);
1764
1910
  crashAtFileNativeAuthorityEdge("after_state_materialization");
1765
1911
  await replaceMaterializedTree(join(root, "history"), historySurfaceFiles(endpoint.history));
1912
+ await replaceMaterializedTree(join(root, "runtime", "history-backgrounds"), projectHistoryBackgrounds(endpoint.history));
1766
1913
  await publishJson(join(root, "runtime", "additional-materials.json"), {
1767
1914
  head: authority.head,
1768
1915
  items: endpoint.additionalMaterials,
@@ -1772,7 +1919,9 @@ async function materializeRecoveredEndpoint(root, authority, endpoint) {
1772
1919
  head: authority.head,
1773
1920
  sequence: authority.sequence,
1774
1921
  commitDigest: authority.commitDigest,
1922
+ ...backgroundCount(endpoint.history),
1775
1923
  });
1924
+ await updateWorldActivity(root, authority.head, endpoint.history, lastActivityAt);
1776
1925
  }
1777
1926
  async function replaceMaterializedTree(root, files) {
1778
1927
  const next = new Map();
@@ -1900,7 +2049,11 @@ function authorityCommitMatchesInput(commit, input) {
1900
2049
  commit.auditParent.head === input.parentHead &&
1901
2050
  commit.timelineParent.head ===
1902
2051
  (input.timelineRevision?.restoresHead ?? input.parentHead) &&
1903
- isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })), input.historyAppend) &&
2052
+ isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
2053
+ role,
2054
+ exactText,
2055
+ ...backgroundField(background),
2056
+ })), input.historyAppend) &&
1904
2057
  stateChangesMatch &&
1905
2058
  isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
1906
2059
  isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
@@ -1989,7 +2142,9 @@ async function readGenesisForFork(worldRoot, worldId) {
1989
2142
  const prefix = `${worldId}.`;
1990
2143
  const history = value.history.map((message, index) => {
1991
2144
  if (!isRecord(message) ||
1992
- !hasExactKeys(message, ["messageId", "role", "exactText"]) ||
2145
+ !hasRequiredAndOptionalKeys(message, ["messageId", "role", "exactText"], ["background"]) ||
2146
+ (message.background !== undefined &&
2147
+ !isHistoryBackground(message.background)) ||
1993
2148
  typeof message.messageId !== "string" ||
1994
2149
  (message.role !== "player" && message.role !== "narrator") ||
1995
2150
  typeof message.exactText !== "string")
@@ -2007,6 +2162,7 @@ async function readGenesisForFork(worldRoot, worldId) {
2007
2162
  messageId,
2008
2163
  role,
2009
2164
  exactText: message.exactText,
2165
+ ...backgroundField(message.background),
2010
2166
  };
2011
2167
  });
2012
2168
  let additionalMaterials;
@@ -2021,14 +2177,25 @@ async function readGenesisForFork(worldRoot, worldId) {
2021
2177
  async function readPublicationAt(root) {
2022
2178
  return readJson(join(root, publicationFile));
2023
2179
  }
2024
- async function readWorldSummaryAt(root, publication) {
2180
+ async function readWorldSummaryAt(root, publication, serializeRebuild) {
2025
2181
  const published = publication ?? (await readPublicationAt(root));
2182
+ const { lastActivityAt, excerpt, playerTurns } = await readWorldActivity(root, serializeRebuild);
2183
+ const summary = {
2184
+ ...toSummary(published),
2185
+ title: await readWorldTitleAt(root, published),
2186
+ lastActivityAt,
2187
+ excerpt,
2188
+ playerTurns,
2189
+ };
2190
+ return summary;
2191
+ }
2192
+ async function readWorldTitleAt(root, published) {
2026
2193
  const metadata = await readOptionalJson(join(root, localMetadataFile));
2027
2194
  if (metadata === null)
2028
- return toSummary(published);
2195
+ return published.title;
2029
2196
  if (!isWorldLocalMetadata(metadata) || metadata.worldId !== published.worldId)
2030
2197
  throw new FileNativeWorldCreationError("world_corrupt", "Local world shell is corrupt");
2031
- return { ...toSummary(published), title: metadata.name };
2198
+ return metadata.name;
2032
2199
  }
2033
2200
  async function readJson(path) {
2034
2201
  return JSON.parse(await readFile(path, "utf8"));
@@ -2067,6 +2234,16 @@ function derivationRequestFingerprint(input) {
2067
2234
  sourceHead: input.sourceHead,
2068
2235
  hostPresetId: input.hostPresetId,
2069
2236
  requestDiscriminator: input.requestDiscriminator ?? null,
2237
+ ...(input.retainCompleteTimeline ? { retainCompleteTimeline: true } : {}),
2238
+ ...(input.targetName === undefined &&
2239
+ input.identityPrefix === undefined &&
2240
+ !input.resetPackageScriptPermissions
2241
+ ? {}
2242
+ : {
2243
+ targetName: input.targetName ?? null,
2244
+ identityPrefix: input.identityPrefix ?? "derive",
2245
+ resetPackageScriptPermissions: input.resetPackageScriptPermissions ?? false,
2246
+ }),
2070
2247
  }));
2071
2248
  }
2072
2249
  async function derivationPublicationMatches(worldRoot, publication, input, requestFingerprint) {
@@ -2122,3 +2299,21 @@ function crashAtFileNativeAuthorityEdge(edge) {
2122
2299
  }
2123
2300
  }
2124
2301
  import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
2302
+ function backgroundField(background) {
2303
+ return background === undefined ? {} : { background };
2304
+ }
2305
+ function materializedBackgroundRecord(files, history) {
2306
+ return Object.fromEntries(files.map(({ path, contents }) => {
2307
+ const id = path.replace(/\.json$/u, "");
2308
+ const value = JSON.parse(contents);
2309
+ if (path !== `${id}.json` ||
2310
+ !Object.hasOwn(history, id) ||
2311
+ !isHistoryBackground(value))
2312
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized history background is invalid or unreachable");
2313
+ return [id, value];
2314
+ }));
2315
+ }
2316
+ function backgroundCount(messages) {
2317
+ const count = messages.filter(({ background }) => background !== undefined).length;
2318
+ return count === 0 ? {} : { historyBackgroundCount: count };
2319
+ }