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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/node/protocol/appPreferences.js +4 -2
- package/dist/node/protocol/playFailure.js +13 -0
- package/dist/node/protocol/protagonistTemplates.js +13 -0
- package/dist/node/protocol/v1.js +25 -19
- package/dist/node/protocol/worldArchives.js +7 -0
- package/dist/node/runtime/V1Runtime.js +48 -6
- package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
- package/dist/node/runtime/archive/WorldArchives.js +389 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
- package/dist/node/runtime/content/ContentWorkspace.js +5 -0
- package/dist/node/runtime/history/HistoryBackground.js +84 -0
- package/dist/node/runtime/history/HistoryQuery.js +7 -2
- package/dist/node/runtime/history/HistoryRendering.js +31 -4
- package/dist/node/runtime/history/StoryExport.js +33 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
- package/dist/node/runtime/model/ModelHost.js +12 -1
- package/dist/node/runtime/model/ModelHostStream.js +11 -3
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +2 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +5 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +91 -4
- package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
- package/dist/node/runtime/play/PlayCallChain.js +212 -61
- package/dist/node/runtime/play/PlayCallChainError.js +3 -1
- package/dist/node/runtime/play/PlayContinuity.js +3 -20
- package/dist/node/runtime/play/PlayFailure.js +20 -0
- package/dist/node/runtime/play/PlayTraceRetention.js +10 -11
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +12 -59
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +2 -2
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +7 -4
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +11 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +250 -55
- package/dist/node/runtime/world/WorldActivity.js +143 -0
- package/dist/node/runtime/world/WorldYamlRendering.js +28 -0
- package/dist/node/server/createServer.js +48 -0
- package/dist/node/shared/default-preset-host.js +18 -8
- package/dist/node/shared/default-setting-improvement-prompt.js +4 -0
- package/dist/node/shared/history-use-guidance.js +2 -2
- package/dist/node/shared/ordered-play-prompts.js +2 -2
- package/dist/node/shared/sample-packages/index.js +10 -0
- package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
- package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
- package/dist/web/assets/index-DxNtxzLR.js +1255 -0
- package/dist/web/assets/{index-CVN48xrw.css → index-DzOnu9uv.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- 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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
793
|
-
const finalRoot =
|
|
794
|
-
|
|
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
|
|
797
|
-
if (!(await derivationPublicationMatches(join(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
810
|
-
|
|
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
|
|
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(
|
|
827
|
-
await mkdir(
|
|
828
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
1015
|
+
await syncDirectory(dirname(finalRoot));
|
|
902
1016
|
crashAtFileNativeAuthorityEdge("derivation_after_publish");
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
913
|
-
|
|
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: [
|
|
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 }) => ({
|
|
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
|
-
!
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
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 }) => ({
|
|
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
|
-
!
|
|
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
|
|
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
|
|
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
|
+
}
|