narraeon 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +29 -0
- package/dist/node/runtime/V1Runtime.js +59 -23
- package/dist/node/runtime/model/FileNativeModelAdapters.js +691 -275
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +74 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1039 -0
- package/dist/node/runtime/play/PlayCallChain.js +609 -330
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +1 -4
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -30
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +626 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +449 -191
- package/dist/web/assets/index-DHV_VWl3.js +1192 -0
- package/dist/web/assets/index-DjLCkP99.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
|
@@ -6,9 +6,13 @@ import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, }
|
|
|
6
6
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
7
7
|
import { PlayerViewRenderer } from "./PlayerViewRenderer.js";
|
|
8
8
|
import { WorldDocumentStore } from "./WorldDocumentStore.js";
|
|
9
|
+
import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
|
|
10
|
+
import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
|
|
11
|
+
import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
|
|
9
12
|
import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
|
|
10
13
|
const publicationFile = "publication.json";
|
|
11
14
|
const localMetadataFile = "local.json";
|
|
15
|
+
const playAuthorityHeadFile = "play-authority-head.json";
|
|
12
16
|
export class FileNativeWorldCreationError extends Error {
|
|
13
17
|
code;
|
|
14
18
|
constructor(code, message, options) {
|
|
@@ -29,6 +33,11 @@ export class FileNativeWorldStore {
|
|
|
29
33
|
#promptCompiler;
|
|
30
34
|
#activeControlUsers = new Map();
|
|
31
35
|
operations;
|
|
36
|
+
playTimeline;
|
|
37
|
+
playAdvances;
|
|
38
|
+
#storageMigrator;
|
|
39
|
+
#storageMigrations = new Map();
|
|
40
|
+
#currentStorage = new Set();
|
|
32
41
|
constructor(dataRoot, options = {}) {
|
|
33
42
|
const root = resolve(dataRoot);
|
|
34
43
|
this.#worldsRoot = join(root, "worlds-file-native");
|
|
@@ -36,6 +45,64 @@ export class FileNativeWorldStore {
|
|
|
36
45
|
this.#promptCompiler =
|
|
37
46
|
options.promptCompiler ?? new FileNativePromptCompiler();
|
|
38
47
|
this.operations = new FileNativeWorldOperationCoordinator(root);
|
|
48
|
+
this.playTimeline = new FileNativePlayTimelineStore(root);
|
|
49
|
+
this.playAdvances = new FileNativePlayAdvanceStore(root);
|
|
50
|
+
this.#storageMigrator = new FileNativeWorldStorageMigrator(root, this.playTimeline);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Upgrade a formally released cumulative layout before current Authority or
|
|
54
|
+
* play-timeline behavior crosses its seam. Concurrent callers share one
|
|
55
|
+
* in-process promise; the durable world lock serializes processes.
|
|
56
|
+
*/
|
|
57
|
+
async ensureCurrentStorage(worldId) {
|
|
58
|
+
assertIdentity(worldId, "World ID");
|
|
59
|
+
if (this.#currentStorage.has(worldId))
|
|
60
|
+
return;
|
|
61
|
+
const existing = this.#storageMigrations.get(worldId);
|
|
62
|
+
if (existing !== undefined)
|
|
63
|
+
return existing;
|
|
64
|
+
const migration = this.#ensureCurrentStorageSerialized(worldId);
|
|
65
|
+
this.#storageMigrations.set(worldId, migration);
|
|
66
|
+
try {
|
|
67
|
+
await migration;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (this.#storageMigrations.get(worldId) === migration)
|
|
71
|
+
this.#storageMigrations.delete(worldId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async #ensureCurrentStorageSerialized(worldId) {
|
|
75
|
+
try {
|
|
76
|
+
try {
|
|
77
|
+
await readAuthorityHead(join(this.#worldsRoot, worldId));
|
|
78
|
+
this.#currentStorage.add(worldId);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!isNodeError(error) || error.code !== "ENOENT")
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
await this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
86
|
+
await this.#storageMigrator.migrate(worldId);
|
|
87
|
+
});
|
|
88
|
+
this.#currentStorage.add(worldId);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof FileNativeWorldCreationError)
|
|
92
|
+
throw error;
|
|
93
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
94
|
+
const root = await stat(join(this.#worldsRoot, worldId)).catch((statError) => {
|
|
95
|
+
if (isNodeError(statError) && statError.code === "ENOENT")
|
|
96
|
+
return null;
|
|
97
|
+
throw statError;
|
|
98
|
+
});
|
|
99
|
+
if (root === null)
|
|
100
|
+
throw new FileNativeWorldNotFoundError({ cause: error });
|
|
101
|
+
}
|
|
102
|
+
if (error instanceof WorldOperationBusyError)
|
|
103
|
+
throw new FileNativeWorldCreationError("operation_conflict", "World storage is being upgraded by another durable operation", { cause: error });
|
|
104
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Released world storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
|
|
105
|
+
}
|
|
39
106
|
}
|
|
40
107
|
async listWorlds() {
|
|
41
108
|
await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
|
|
@@ -90,6 +157,7 @@ export class FileNativeWorldStore {
|
|
|
90
157
|
}
|
|
91
158
|
return this.operations.withExclusiveWorldStateMutation(worldId, async () => {
|
|
92
159
|
await rm(root, { recursive: true, force: true });
|
|
160
|
+
this.#currentStorage.delete(worldId);
|
|
93
161
|
return { deleted: true };
|
|
94
162
|
});
|
|
95
163
|
}
|
|
@@ -169,8 +237,17 @@ export class FileNativeWorldStore {
|
|
|
169
237
|
additionalMaterials: initialMaterials,
|
|
170
238
|
};
|
|
171
239
|
await writeJson(join(stagingRoot, "runtime", "genesis.json"), genesis);
|
|
240
|
+
await writeJson(join(stagingRoot, "runtime", "play-genesis-timeline.json"), {
|
|
241
|
+
schemaVersion: 1,
|
|
242
|
+
worldId,
|
|
243
|
+
history: genesis.history,
|
|
244
|
+
});
|
|
245
|
+
await writeJson(join(stagingRoot, "runtime", playAuthorityHeadFile), genesisAuthorityHead());
|
|
172
246
|
await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
|
|
247
|
+
schemaVersion: 2,
|
|
173
248
|
head: "genesis",
|
|
249
|
+
sequence: 0,
|
|
250
|
+
commitDigest: null,
|
|
174
251
|
});
|
|
175
252
|
await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
|
|
176
253
|
const publication = {
|
|
@@ -215,6 +292,7 @@ export class FileNativeWorldStore {
|
|
|
215
292
|
}
|
|
216
293
|
async readSurface(worldId, surface) {
|
|
217
294
|
assertIdentity(worldId, "World ID");
|
|
295
|
+
await this.ensureCurrentStorage(worldId);
|
|
218
296
|
const root = join(this.#worldsRoot, worldId);
|
|
219
297
|
await readPublicationAt(root);
|
|
220
298
|
if (surface === "runtime") {
|
|
@@ -236,28 +314,37 @@ export class FileNativeWorldStore {
|
|
|
236
314
|
* must never pair a mutable state tree with a different response head.
|
|
237
315
|
*/
|
|
238
316
|
async renderPlayerViewsAtHead(worldId, head) {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
317
|
+
const root = join(this.#worldsRoot, worldId);
|
|
318
|
+
const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
|
|
319
|
+
const [state, control] = await Promise.all([
|
|
320
|
+
materialized?.head === head
|
|
321
|
+
? readTree(join(root, "state"))
|
|
322
|
+
: this.recoverEndpoint(worldId, head).then(({ state }) => state),
|
|
323
|
+
this.readSurface(worldId, "control"),
|
|
324
|
+
]);
|
|
241
325
|
const source = control.find(({ path }) => path === "player-views.yaml");
|
|
242
326
|
return new PlayerViewRenderer().render({
|
|
243
|
-
snapshot: openWorldDocumentSnapshot(
|
|
327
|
+
snapshot: openWorldDocumentSnapshot(state),
|
|
244
328
|
control: source?.contents ?? "",
|
|
245
329
|
});
|
|
246
330
|
}
|
|
247
331
|
async currentHead(worldId) {
|
|
248
332
|
assertIdentity(worldId, "World ID");
|
|
333
|
+
await this.ensureCurrentStorage(worldId);
|
|
249
334
|
const root = join(this.#worldsRoot, worldId);
|
|
250
335
|
await readPublicationAt(root);
|
|
251
|
-
return (await
|
|
336
|
+
return (await readAuthorityHead(root)).head;
|
|
252
337
|
}
|
|
253
338
|
async currentHeadOperationId(worldId) {
|
|
254
339
|
assertIdentity(worldId, "World ID");
|
|
340
|
+
await this.ensureCurrentStorage(worldId);
|
|
255
341
|
const root = join(this.#worldsRoot, worldId);
|
|
256
342
|
await readPublicationAt(root);
|
|
257
|
-
return (
|
|
343
|
+
return (await readAuthorityHead(root)).operationId;
|
|
258
344
|
}
|
|
259
345
|
async saveControlDraft(worldId, files) {
|
|
260
346
|
assertIdentity(worldId, "World ID");
|
|
347
|
+
await this.ensureCurrentStorage(worldId);
|
|
261
348
|
const root = join(this.#worldsRoot, worldId);
|
|
262
349
|
await readPublicationAt(root);
|
|
263
350
|
for (const file of files)
|
|
@@ -350,6 +437,7 @@ export class FileNativeWorldStore {
|
|
|
350
437
|
}
|
|
351
438
|
async #readControlDraft(worldId) {
|
|
352
439
|
assertIdentity(worldId, "World ID");
|
|
440
|
+
await this.ensureCurrentStorage(worldId);
|
|
353
441
|
const root = join(this.#worldsRoot, worldId);
|
|
354
442
|
await readPublicationAt(root);
|
|
355
443
|
const [state, draft] = await Promise.all([
|
|
@@ -363,6 +451,7 @@ export class FileNativeWorldStore {
|
|
|
363
451
|
/** Bind the current Authority endpoint for a model-directed play call chain. */
|
|
364
452
|
async bindPlayCallChain(worldId) {
|
|
365
453
|
assertIdentity(worldId, "World ID");
|
|
454
|
+
await this.ensureCurrentStorage(worldId);
|
|
366
455
|
const root = join(this.#worldsRoot, worldId);
|
|
367
456
|
try {
|
|
368
457
|
await readPublicationAt(root);
|
|
@@ -384,23 +473,16 @@ export class FileNativeWorldStore {
|
|
|
384
473
|
}
|
|
385
474
|
try {
|
|
386
475
|
return await this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (found < 0)
|
|
394
|
-
throw new FileNativeWorldCreationError("world_corrupt", "World materialized head does not belong to the authority commit chain");
|
|
395
|
-
materializedIndex = found + 1;
|
|
396
|
-
}
|
|
397
|
-
for (let index = materializedIndex; index < publishedAuthority.commits.length; index += 1) {
|
|
398
|
-
const commit = publishedAuthority.commits[index];
|
|
399
|
-
await materializePlayCommit(root, index + 1, {
|
|
476
|
+
const authority = await readAuthorityHead(root);
|
|
477
|
+
const checkpoint = await readMaterializedCheckpoint(root);
|
|
478
|
+
if (!sameMaterializedEndpoint(checkpoint, authority)) {
|
|
479
|
+
const missing = await readAuthorityTail(root, authority, checkpoint);
|
|
480
|
+
for (const commit of missing) {
|
|
481
|
+
await materializePlayCommit(root, commit.sequence, {
|
|
400
482
|
mode: commit.mode,
|
|
401
483
|
historyAppend: commit.historyAppend,
|
|
402
484
|
nextMaterials: commit.nextAdditionalMaterials,
|
|
403
|
-
stateChanges: commit.stateChanges
|
|
485
|
+
stateChanges: commit.stateChanges,
|
|
404
486
|
...(commit.timelineRevision === undefined
|
|
405
487
|
? {}
|
|
406
488
|
: { timelineRevision: commit.timelineRevision }),
|
|
@@ -410,35 +492,37 @@ export class FileNativeWorldStore {
|
|
|
410
492
|
worldId,
|
|
411
493
|
parentHead: commit.parentHead,
|
|
412
494
|
head: commit.head,
|
|
495
|
+
commitDigest: playCommitDigest(commit),
|
|
413
496
|
historyAppend: commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })),
|
|
414
497
|
nextAdditionalMaterials: commit.nextAdditionalMaterials,
|
|
415
498
|
mode: commit.mode,
|
|
416
499
|
});
|
|
417
500
|
}
|
|
418
501
|
await publishJson(join(root, "runtime", "materialized-head.json"), {
|
|
419
|
-
|
|
502
|
+
schemaVersion: 2,
|
|
503
|
+
head: authority.head,
|
|
504
|
+
sequence: authority.sequence,
|
|
505
|
+
commitDigest: authority.commitDigest,
|
|
420
506
|
});
|
|
421
507
|
}
|
|
422
|
-
const [state, control,
|
|
508
|
+
const [state, control, history, materials] = await Promise.all([
|
|
423
509
|
readTree(join(root, "state")),
|
|
424
510
|
readTree(join(root, "control")),
|
|
425
|
-
|
|
511
|
+
readTree(join(root, "history")),
|
|
426
512
|
readOptionalJson(join(root, "runtime", "additional-materials.json")),
|
|
427
|
-
this.recoverEndpoint(worldId),
|
|
428
513
|
]);
|
|
514
|
+
if ((materials?.head ?? "genesis") !== authority.head)
|
|
515
|
+
throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
|
|
429
516
|
return {
|
|
430
517
|
worldId,
|
|
431
|
-
parentHead: authority
|
|
518
|
+
parentHead: authority.head,
|
|
432
519
|
files: [...state, ...control].reduce((files, { path, contents }, index) => {
|
|
433
520
|
files[`${index < state.length ? "state" : "control"}/${path}`] =
|
|
434
521
|
contents;
|
|
435
522
|
return files;
|
|
436
523
|
}, {}),
|
|
437
524
|
additionalMaterials: materials?.items ?? [],
|
|
438
|
-
history:
|
|
439
|
-
messageId,
|
|
440
|
-
exactText,
|
|
441
|
-
])),
|
|
525
|
+
history: materializedHistoryRecord(worldId, history),
|
|
442
526
|
};
|
|
443
527
|
});
|
|
444
528
|
}
|
|
@@ -479,6 +563,7 @@ export class FileNativeWorldStore {
|
|
|
479
563
|
}
|
|
480
564
|
async recoverEndpoint(worldId, head) {
|
|
481
565
|
assertIdentity(worldId, "World ID");
|
|
566
|
+
await this.ensureCurrentStorage(worldId);
|
|
482
567
|
const root = join(this.#worldsRoot, worldId);
|
|
483
568
|
await readPublicationAt(root);
|
|
484
569
|
const genesis = await readJson(join(root, "runtime", "genesis.json"));
|
|
@@ -540,6 +625,7 @@ export class FileNativeWorldStore {
|
|
|
540
625
|
}
|
|
541
626
|
async repairMaterialization(worldId) {
|
|
542
627
|
assertIdentity(worldId, "World ID");
|
|
628
|
+
await this.ensureCurrentStorage(worldId);
|
|
543
629
|
try {
|
|
544
630
|
const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
|
|
545
631
|
await this.bindPlayCallChain(worldId);
|
|
@@ -572,6 +658,7 @@ export class FileNativeWorldStore {
|
|
|
572
658
|
const finalRoot = join(this.#worldsRoot, worldId);
|
|
573
659
|
const previous = await readOptionalJson(operationPath);
|
|
574
660
|
if (previous !== null) {
|
|
661
|
+
await this.ensureCurrentStorage(previous.worldId);
|
|
575
662
|
if (!(await derivationPublicationMatches(join(this.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
|
|
576
663
|
throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
|
|
577
664
|
return {
|
|
@@ -581,6 +668,7 @@ export class FileNativeWorldStore {
|
|
|
581
668
|
}
|
|
582
669
|
const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
|
|
583
670
|
if (alreadyPublished !== null) {
|
|
671
|
+
await this.ensureCurrentStorage(alreadyPublished.worldId);
|
|
584
672
|
if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
|
|
585
673
|
throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
|
|
586
674
|
await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
|
|
@@ -590,6 +678,7 @@ export class FileNativeWorldStore {
|
|
|
590
678
|
world: await this.#currentSummary(alreadyPublished),
|
|
591
679
|
};
|
|
592
680
|
}
|
|
681
|
+
await this.ensureCurrentStorage(input.sourceWorldId);
|
|
593
682
|
const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
|
|
594
683
|
const sourcePublication = await readPublicationAt(sourceRoot);
|
|
595
684
|
const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
|
|
@@ -628,12 +717,18 @@ export class FileNativeWorldStore {
|
|
|
628
717
|
additionalMaterials: cloned.genesisMaterials,
|
|
629
718
|
};
|
|
630
719
|
await writeJson(join(staging, "runtime", "genesis.json"), genesis);
|
|
720
|
+
await writeJson(join(staging, "runtime", "play-genesis-timeline.json"), {
|
|
721
|
+
schemaVersion: 1,
|
|
722
|
+
worldId,
|
|
723
|
+
history: genesis.history,
|
|
724
|
+
});
|
|
631
725
|
await writeJson(join(staging, "runtime", "additional-materials.json"), {
|
|
632
726
|
head: "genesis",
|
|
633
727
|
items: cloned.genesisMaterials,
|
|
634
728
|
});
|
|
635
729
|
for (const [index, commit] of cloned.commits.entries()) {
|
|
636
|
-
|
|
730
|
+
const digest = playCommitDigest(commit);
|
|
731
|
+
await publishImmutableJson(join(staging, "runtime", "play-commits", `${digest}.json`), commit);
|
|
637
732
|
await materializePlayCommit(staging, index + 1, {
|
|
638
733
|
mode: commit.mode,
|
|
639
734
|
historyAppend: commit.historyAppend,
|
|
@@ -644,14 +739,21 @@ export class FileNativeWorldStore {
|
|
|
644
739
|
: { timelineRevision: commit.timelineRevision }),
|
|
645
740
|
});
|
|
646
741
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
742
|
+
const clonedHead = cloned.commits.at(-1);
|
|
743
|
+
await writeJson(join(staging, "runtime", playAuthorityHeadFile), clonedHead === undefined
|
|
744
|
+
? genesisAuthorityHead()
|
|
745
|
+
: {
|
|
746
|
+
schemaVersion: 2,
|
|
747
|
+
head: clonedHead.head,
|
|
748
|
+
sequence: clonedHead.sequence,
|
|
749
|
+
commitDigest: playCommitDigest(clonedHead),
|
|
750
|
+
operationId: clonedHead.operationId,
|
|
652
751
|
});
|
|
653
752
|
await writeJson(join(staging, "runtime", "materialized-head.json"), {
|
|
753
|
+
schemaVersion: 2,
|
|
654
754
|
head: input.sourceHead,
|
|
755
|
+
sequence: clonedHead?.sequence ?? 0,
|
|
756
|
+
commitDigest: clonedHead === undefined ? null : playCommitDigest(clonedHead),
|
|
655
757
|
});
|
|
656
758
|
const materializedState = await readTree(join(staging, "state"));
|
|
657
759
|
if (!isDeepStrictEqual(materializedState, selected.state))
|
|
@@ -698,7 +800,11 @@ export class FileNativeWorldStore {
|
|
|
698
800
|
assertIdentity(operationId, "operation ID");
|
|
699
801
|
let directRecord;
|
|
700
802
|
try {
|
|
701
|
-
|
|
803
|
+
let value = await readOptionalJson(this.#operationOutcomePath(operationId));
|
|
804
|
+
if (isReleasedCommittedOperationOutcome(value)) {
|
|
805
|
+
await this.ensureCurrentStorage(value.worldId);
|
|
806
|
+
value = await readOptionalJson(this.#operationOutcomePath(operationId));
|
|
807
|
+
}
|
|
702
808
|
if (value === null)
|
|
703
809
|
directRecord = null;
|
|
704
810
|
else {
|
|
@@ -711,46 +817,18 @@ export class FileNativeWorldStore {
|
|
|
711
817
|
throw error;
|
|
712
818
|
throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
|
|
713
819
|
}
|
|
820
|
+
if (directRecord?.outcome === "acceptance_prepared")
|
|
821
|
+
return resolvePreparedOperationOutcome(this.#worldsRoot, operationId, directRecord);
|
|
714
822
|
const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
|
|
715
823
|
if (direct?.outcome === "committed" ||
|
|
716
824
|
direct?.outcome === "committed_materialization_pending") {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, direct.worldId), operationId, direct);
|
|
725
|
-
await assertPendingMaterializationCompatible(join(this.#worldsRoot, direct.worldId), operationId);
|
|
726
|
-
}
|
|
727
|
-
return direct;
|
|
728
|
-
}
|
|
729
|
-
const worlds = await readDirectoryNames(this.#worldsRoot);
|
|
730
|
-
for (const worldId of worlds) {
|
|
731
|
-
const authority = await readAcceptedAuthority(join(this.#worldsRoot, worldId));
|
|
732
|
-
const commitIndex = authority?.commits.findIndex((candidate) => candidate.operationId === operationId) ?? -1;
|
|
733
|
-
const commit = authority?.commits[commitIndex];
|
|
734
|
-
if (commit !== undefined) {
|
|
735
|
-
const materialized = await readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "materialized-head.json"));
|
|
736
|
-
const materializedIndex = materialized?.head === "genesis"
|
|
737
|
-
? -1
|
|
738
|
-
: (authority?.commits.findIndex(({ head }) => head === materialized?.head) ?? -1);
|
|
739
|
-
return {
|
|
740
|
-
outcome: materializedIndex >= commitIndex
|
|
741
|
-
? "committed"
|
|
742
|
-
: "committed_materialization_pending",
|
|
743
|
-
worldId,
|
|
744
|
-
parentHead: commit.parentHead,
|
|
745
|
-
head: commit.head,
|
|
746
|
-
historyAppend: commit.historyAppend.map(({ role, exactText }) => ({
|
|
747
|
-
role,
|
|
748
|
-
exactText,
|
|
749
|
-
})),
|
|
750
|
-
nextAdditionalMaterials: commit.nextAdditionalMaterials,
|
|
751
|
-
mode: commit.mode,
|
|
752
|
-
};
|
|
753
|
-
}
|
|
825
|
+
const worldRoot = join(this.#worldsRoot, direct.worldId);
|
|
826
|
+
const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, direct);
|
|
827
|
+
const materialized = await readMaterializedCheckpoint(worldRoot);
|
|
828
|
+
if (materialized !== null && materialized.sequence >= commit.sequence)
|
|
829
|
+
return { ...direct, outcome: "committed" };
|
|
830
|
+
await assertPendingMaterializationCompatible(worldRoot, operationId, direct.commitDigest);
|
|
831
|
+
return { ...direct, outcome: "committed_materialization_pending" };
|
|
754
832
|
}
|
|
755
833
|
return direct?.outcome === "in_progress"
|
|
756
834
|
? { outcome: "in_progress" }
|
|
@@ -773,6 +851,7 @@ export class FileNativeWorldStore {
|
|
|
773
851
|
throw new TypeError("Edited player message cannot be empty");
|
|
774
852
|
if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
|
|
775
853
|
throw new TypeError("Timeline-revision request fingerprint is invalid");
|
|
854
|
+
await this.ensureCurrentStorage(input.worldId);
|
|
776
855
|
const existing = await this.getOperationOutcome(input.operationId);
|
|
777
856
|
if (isCommittedOutcome(existing)) {
|
|
778
857
|
const authority = await readAcceptedAuthority(join(this.#worldsRoot, existing.worldId));
|
|
@@ -821,6 +900,7 @@ export class FileNativeWorldStore {
|
|
|
821
900
|
});
|
|
822
901
|
}
|
|
823
902
|
async commitCorrection(input) {
|
|
903
|
+
await this.ensureCurrentStorage(input.worldId);
|
|
824
904
|
try {
|
|
825
905
|
const commit = async () => {
|
|
826
906
|
let operationReserved = input.stateOperationClaim !== undefined;
|
|
@@ -856,6 +936,7 @@ export class FileNativeWorldStore {
|
|
|
856
936
|
* separate commits; there is no synthetic call-chain-wide settlement boundary.
|
|
857
937
|
*/
|
|
858
938
|
async commitPlayStep(input) {
|
|
939
|
+
await this.ensureCurrentStorage(input.worldId);
|
|
859
940
|
if (input.historyAppend.length === 0 && input.stateChanges.length === 0)
|
|
860
941
|
throw new TypeError("A call-chain commit cannot omit both narrative and state changes");
|
|
861
942
|
return this.#commitHistoryChange({
|
|
@@ -863,24 +944,11 @@ export class FileNativeWorldStore {
|
|
|
863
944
|
mode: "play",
|
|
864
945
|
});
|
|
865
946
|
}
|
|
866
|
-
async readPlayCallChain(worldId) {
|
|
867
|
-
assertIdentity(worldId, "World ID");
|
|
868
|
-
return readOptionalJson(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"));
|
|
869
|
-
}
|
|
870
|
-
async writePlayCallChain(worldId, value) {
|
|
871
|
-
assertIdentity(worldId, "World ID");
|
|
872
|
-
await publishJson(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"), value);
|
|
873
|
-
}
|
|
874
|
-
async removePlayCallChain(worldId) {
|
|
875
|
-
assertIdentity(worldId, "World ID");
|
|
876
|
-
await rm(join(this.#worldsRoot, worldId, "runtime", "play-call-chain.json"), { force: true });
|
|
877
|
-
}
|
|
878
947
|
async #commitHistoryChange(input) {
|
|
879
948
|
const existing = await this.getOperationOutcome(input.operationId);
|
|
880
949
|
if (isCommittedOutcome(existing)) {
|
|
881
950
|
assertSameOperationOutcome(existing, input);
|
|
882
|
-
const
|
|
883
|
-
const commit = accepted?.commits.find(({ operationId }) => operationId === input.operationId);
|
|
951
|
+
const commit = await readPlayCommitByDigest(join(this.#worldsRoot, existing.worldId), existing.commitDigest);
|
|
884
952
|
if (commit?.mode !== input.mode ||
|
|
885
953
|
JSON.stringify(commit?.stateChanges) !==
|
|
886
954
|
JSON.stringify(input.stateChanges) ||
|
|
@@ -888,8 +956,14 @@ export class FileNativeWorldStore {
|
|
|
888
956
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
|
|
889
957
|
return existing;
|
|
890
958
|
}
|
|
959
|
+
const prepared = existing.outcome === "in_progress"
|
|
960
|
+
? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
|
|
961
|
+
: null;
|
|
962
|
+
if (prepared !== null)
|
|
963
|
+
await assertPreparedOperationMatches(this.#worldsRoot, input.operationId, prepared, input);
|
|
891
964
|
if (existing.outcome !== "not_started" &&
|
|
892
|
-
!(
|
|
965
|
+
!(existing.outcome === "in_progress" &&
|
|
966
|
+
(input.operationReserved === true || prepared !== null)))
|
|
893
967
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
|
|
894
968
|
const root = join(this.#worldsRoot, input.worldId);
|
|
895
969
|
try {
|
|
@@ -897,20 +971,23 @@ export class FileNativeWorldStore {
|
|
|
897
971
|
const concurrent = await this.#readOperationOutcome(input.operationId);
|
|
898
972
|
if (isCommittedOutcome(concurrent)) {
|
|
899
973
|
assertSameOperationOutcome(concurrent, input);
|
|
900
|
-
const
|
|
901
|
-
const knownCommit = accepted?.commits.find(({ operationId }) => operationId === input.operationId);
|
|
974
|
+
const knownCommit = await readPlayCommitByDigest(join(this.#worldsRoot, concurrent.worldId), concurrent.commitDigest);
|
|
902
975
|
if (knownCommit?.mode !== input.mode ||
|
|
903
976
|
!isDeepStrictEqual(knownCommit.stateChanges, input.stateChanges) ||
|
|
904
977
|
!isDeepStrictEqual(knownCommit.timelineRevision, input.timelineRevision))
|
|
905
978
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
|
|
906
979
|
return concurrent;
|
|
907
980
|
}
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
981
|
+
const concurrentPrepared = concurrent.outcome === "in_progress"
|
|
982
|
+
? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
|
|
983
|
+
: null;
|
|
984
|
+
if (concurrentPrepared !== null)
|
|
985
|
+
await assertPreparedOperationMatches(this.#worldsRoot, input.operationId, concurrentPrepared, input);
|
|
986
|
+
else if (concurrent.outcome === "in_progress" &&
|
|
987
|
+
input.operationReserved !== true)
|
|
988
|
+
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
|
|
989
|
+
const authorityPath = join(root, "runtime", playAuthorityHeadFile);
|
|
990
|
+
const authority = await readAuthorityHead(root);
|
|
914
991
|
if (authority.head !== input.parentHead) {
|
|
915
992
|
throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
|
|
916
993
|
}
|
|
@@ -959,12 +1036,14 @@ export class FileNativeWorldStore {
|
|
|
959
1036
|
], { requireOpening: false });
|
|
960
1037
|
if (inspection.status !== "usable")
|
|
961
1038
|
throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
|
|
962
|
-
const sequence = authority.
|
|
1039
|
+
const sequence = authority.sequence + 1;
|
|
963
1040
|
const head = `commit:${sequence}`;
|
|
964
1041
|
const commit = {
|
|
965
|
-
schemaVersion:
|
|
1042
|
+
schemaVersion: 2,
|
|
1043
|
+
sequence,
|
|
966
1044
|
operationId: input.operationId,
|
|
967
1045
|
parentHead: input.parentHead,
|
|
1046
|
+
parentCommitDigest: authority.commitDigest,
|
|
968
1047
|
head,
|
|
969
1048
|
mode: input.mode,
|
|
970
1049
|
historyAppend: input.historyAppend.map((message, index) => ({
|
|
@@ -985,23 +1064,32 @@ export class FileNativeWorldStore {
|
|
|
985
1064
|
}
|
|
986
1065
|
: {}),
|
|
987
1066
|
};
|
|
1067
|
+
const digest = playCommitDigest(commit);
|
|
988
1068
|
crashAtFileNativeAuthorityEdge("before_commit_acceptance");
|
|
989
|
-
await publishImmutableJson(join(root, "runtime", "play-commits", `${
|
|
990
|
-
await publishJson(authorityPath, {
|
|
991
|
-
schemaVersion: 1,
|
|
992
|
-
head,
|
|
993
|
-
commits: [...authority.commits, commit],
|
|
994
|
-
});
|
|
995
|
-
crashAtFileNativeAuthorityEdge("after_commit_acceptance");
|
|
1069
|
+
await publishImmutableJson(join(root, "runtime", "play-commits", `${digest}.json`), commit);
|
|
996
1070
|
const pending = {
|
|
997
1071
|
outcome: "committed_materialization_pending",
|
|
998
1072
|
worldId: input.worldId,
|
|
999
1073
|
parentHead: input.parentHead,
|
|
1000
1074
|
head,
|
|
1075
|
+
commitDigest: digest,
|
|
1001
1076
|
historyAppend: structuredClone(input.historyAppend),
|
|
1002
1077
|
nextAdditionalMaterials: structuredClone(input.nextMaterials),
|
|
1003
1078
|
mode: input.mode,
|
|
1004
1079
|
};
|
|
1080
|
+
await publishJson(this.#operationOutcomePath(input.operationId), {
|
|
1081
|
+
...pending,
|
|
1082
|
+
outcome: "acceptance_prepared",
|
|
1083
|
+
});
|
|
1084
|
+
crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
|
|
1085
|
+
await publishJson(authorityPath, {
|
|
1086
|
+
schemaVersion: 2,
|
|
1087
|
+
head,
|
|
1088
|
+
sequence,
|
|
1089
|
+
commitDigest: digest,
|
|
1090
|
+
operationId: input.operationId,
|
|
1091
|
+
});
|
|
1092
|
+
crashAtFileNativeAuthorityEdge("after_commit_acceptance");
|
|
1005
1093
|
await publishJson(this.#operationOutcomePath(input.operationId), pending);
|
|
1006
1094
|
try {
|
|
1007
1095
|
await materializePlayCommit(root, sequence, {
|
|
@@ -1015,7 +1103,10 @@ export class FileNativeWorldStore {
|
|
|
1015
1103
|
});
|
|
1016
1104
|
crashAtFileNativeAuthorityEdge("after_materialization");
|
|
1017
1105
|
await publishJson(join(root, "runtime", "materialized-head.json"), {
|
|
1106
|
+
schemaVersion: 2,
|
|
1018
1107
|
head,
|
|
1108
|
+
sequence,
|
|
1109
|
+
commitDigest: digest,
|
|
1019
1110
|
});
|
|
1020
1111
|
const outcome = { ...pending, outcome: "committed" };
|
|
1021
1112
|
await publishJson(this.#operationOutcomePath(input.operationId), outcome);
|
|
@@ -1038,6 +1129,7 @@ export class FileNativeWorldStore {
|
|
|
1038
1129
|
}
|
|
1039
1130
|
async readAuthorityHistory(worldId) {
|
|
1040
1131
|
assertIdentity(worldId, "World ID");
|
|
1132
|
+
await this.ensureCurrentStorage(worldId);
|
|
1041
1133
|
const root = join(this.#worldsRoot, worldId);
|
|
1042
1134
|
await readPublicationAt(root);
|
|
1043
1135
|
const authority = await readAcceptedAuthority(root);
|
|
@@ -1222,19 +1314,6 @@ async function readTree(root, prefix = "") {
|
|
|
1222
1314
|
}
|
|
1223
1315
|
return files;
|
|
1224
1316
|
}
|
|
1225
|
-
async function readDirectoryNames(root) {
|
|
1226
|
-
try {
|
|
1227
|
-
return (await readdir(root, { withFileTypes: true }))
|
|
1228
|
-
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
1229
|
-
.map((entry) => entry.name)
|
|
1230
|
-
.sort();
|
|
1231
|
-
}
|
|
1232
|
-
catch (error) {
|
|
1233
|
-
if (isNodeError(error) && error.code === "ENOENT")
|
|
1234
|
-
return [];
|
|
1235
|
-
throw error;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
1317
|
function immutableFiles(files) {
|
|
1239
1318
|
return files.map(({ path, contents }) => ({
|
|
1240
1319
|
path,
|
|
@@ -1286,13 +1365,15 @@ function assertOperationOutcomeIntegrity(value) {
|
|
|
1286
1365
|
throw corruptOperationOutcome();
|
|
1287
1366
|
return;
|
|
1288
1367
|
}
|
|
1289
|
-
if (value.outcome === "
|
|
1368
|
+
if (value.outcome === "acceptance_prepared" ||
|
|
1369
|
+
value.outcome === "committed" ||
|
|
1290
1370
|
value.outcome === "committed_materialization_pending") {
|
|
1291
1371
|
if (!hasExactKeys(value, [
|
|
1292
1372
|
"outcome",
|
|
1293
1373
|
"worldId",
|
|
1294
1374
|
"parentHead",
|
|
1295
1375
|
"head",
|
|
1376
|
+
"commitDigest",
|
|
1296
1377
|
"historyAppend",
|
|
1297
1378
|
"nextAdditionalMaterials",
|
|
1298
1379
|
"mode",
|
|
@@ -1303,6 +1384,8 @@ function assertOperationOutcomeIntegrity(value) {
|
|
|
1303
1384
|
!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
|
|
1304
1385
|
typeof value.head !== "string" ||
|
|
1305
1386
|
!/^commit:[1-9][0-9]*$/u.test(value.head) ||
|
|
1387
|
+
typeof value.commitDigest !== "string" ||
|
|
1388
|
+
!/^[a-f0-9]{64}$/u.test(value.commitDigest) ||
|
|
1306
1389
|
!Array.isArray(value.historyAppend) ||
|
|
1307
1390
|
!value.historyAppend.every(isPlayHistoryAppendInput) ||
|
|
1308
1391
|
!Array.isArray(value.nextAdditionalMaterials) ||
|
|
@@ -1315,28 +1398,85 @@ function assertOperationOutcomeIntegrity(value) {
|
|
|
1315
1398
|
}
|
|
1316
1399
|
throw corruptOperationOutcome();
|
|
1317
1400
|
}
|
|
1401
|
+
function isReleasedCommittedOperationOutcome(value) {
|
|
1402
|
+
return (isRecord(value) &&
|
|
1403
|
+
(value.outcome === "committed" ||
|
|
1404
|
+
value.outcome === "committed_materialization_pending") &&
|
|
1405
|
+
!Object.hasOwn(value, "commitDigest") &&
|
|
1406
|
+
typeof value.worldId === "string");
|
|
1407
|
+
}
|
|
1318
1408
|
function projectOperationOutcome(value) {
|
|
1319
1409
|
return structuredClone(value);
|
|
1320
1410
|
}
|
|
1411
|
+
async function readAcceptancePreparedOutcome(path) {
|
|
1412
|
+
const value = await readOptionalJson(path);
|
|
1413
|
+
if (value === null)
|
|
1414
|
+
return null;
|
|
1415
|
+
assertOperationOutcomeIntegrity(value);
|
|
1416
|
+
if (value.outcome !== "acceptance_prepared")
|
|
1417
|
+
return null;
|
|
1418
|
+
return structuredClone(value);
|
|
1419
|
+
}
|
|
1420
|
+
async function assertPreparedOperationMatches(worldsRoot, operationId, prepared, input) {
|
|
1421
|
+
const comparable = {
|
|
1422
|
+
...structuredClone(prepared),
|
|
1423
|
+
outcome: "committed_materialization_pending",
|
|
1424
|
+
};
|
|
1425
|
+
assertSameOperationOutcome(comparable, input);
|
|
1426
|
+
const commit = await readPlayCommitByDigest(join(worldsRoot, prepared.worldId), prepared.commitDigest);
|
|
1427
|
+
if (commit.operationId !== operationId ||
|
|
1428
|
+
commit.mode !== input.mode ||
|
|
1429
|
+
!isDeepStrictEqual(commit.stateChanges, input.stateChanges) ||
|
|
1430
|
+
!isDeepStrictEqual(commit.timelineRevision, input.timelineRevision))
|
|
1431
|
+
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different prepared commit payload");
|
|
1432
|
+
}
|
|
1433
|
+
async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
|
|
1434
|
+
const prepared = value;
|
|
1435
|
+
const worldRoot = join(worldsRoot, prepared.worldId);
|
|
1436
|
+
const authority = await readAuthorityHead(worldRoot);
|
|
1437
|
+
if (authority.head === prepared.parentHead)
|
|
1438
|
+
return { outcome: "in_progress" };
|
|
1439
|
+
const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, prepared);
|
|
1440
|
+
if (authority.sequence < commit.sequence)
|
|
1441
|
+
throw corruptOperationOutcome();
|
|
1442
|
+
await readAuthorityTail(worldRoot, authority, {
|
|
1443
|
+
schemaVersion: 2,
|
|
1444
|
+
head: commit.head,
|
|
1445
|
+
sequence: commit.sequence,
|
|
1446
|
+
commitDigest: prepared.commitDigest,
|
|
1447
|
+
}).catch(() => {
|
|
1448
|
+
throw corruptOperationOutcome();
|
|
1449
|
+
});
|
|
1450
|
+
const materialized = await readMaterializedCheckpoint(worldRoot);
|
|
1451
|
+
const outcome = materialized !== null && materialized.sequence >= commit.sequence
|
|
1452
|
+
? "committed"
|
|
1453
|
+
: "committed_materialization_pending";
|
|
1454
|
+
const resolved = { ...structuredClone(prepared), outcome };
|
|
1455
|
+
if (outcome === "committed_materialization_pending")
|
|
1456
|
+
await assertPendingMaterializationCompatible(worldRoot, operationId, prepared.commitDigest);
|
|
1457
|
+
return resolved;
|
|
1458
|
+
}
|
|
1321
1459
|
function corruptOperationOutcome() {
|
|
1322
1460
|
return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
|
|
1323
1461
|
}
|
|
1324
1462
|
async function assertCommittedAuthorityOutcomeMatches(worldRoot, operationId, outcome) {
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1463
|
+
const commit = await readPlayCommitByDigest(worldRoot, outcome.commitDigest);
|
|
1464
|
+
if (commit.operationId !== operationId ||
|
|
1465
|
+
commit?.parentHead !== outcome.parentHead ||
|
|
1328
1466
|
commit?.head !== outcome.head ||
|
|
1329
1467
|
commit?.mode !== outcome.mode ||
|
|
1330
1468
|
!isDeepStrictEqual(commit?.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
|
|
1331
1469
|
!isDeepStrictEqual(commit?.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
|
|
1332
1470
|
throw corruptOperationOutcome();
|
|
1471
|
+
return commit;
|
|
1333
1472
|
}
|
|
1334
|
-
async function assertPendingMaterializationCompatible(worldRoot, operationId) {
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if (commit
|
|
1473
|
+
async function assertPendingMaterializationCompatible(worldRoot, operationId, commitDigest) {
|
|
1474
|
+
const commit = commitDigest === undefined
|
|
1475
|
+
? (await readAcceptedAuthority(worldRoot))?.commits.find((candidate) => candidate.operationId === operationId)
|
|
1476
|
+
: await readPlayCommitByDigest(worldRoot, commitDigest);
|
|
1477
|
+
if (commit?.operationId !== operationId)
|
|
1339
1478
|
throw corruptOperationOutcome();
|
|
1479
|
+
const sequence = commit.sequence - 1;
|
|
1340
1480
|
for (const change of commit.stateChanges) {
|
|
1341
1481
|
const path = join(worldRoot, "state", change.relativePath);
|
|
1342
1482
|
const existing = await readOptionalText(path);
|
|
@@ -1430,55 +1570,36 @@ async function publishImmutableJson(path, value) {
|
|
|
1430
1570
|
}
|
|
1431
1571
|
async function readAcceptedAuthority(root) {
|
|
1432
1572
|
try {
|
|
1433
|
-
const published = await
|
|
1434
|
-
if (published ===
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
assertFileNativePlayCommit(commit);
|
|
1442
|
-
const canonicalName = `${createHash("sha256")
|
|
1443
|
-
.update(JSON.stringify(commit))
|
|
1444
|
-
.digest("hex")}.json`;
|
|
1445
|
-
if (name !== canonicalName)
|
|
1446
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Immutable commit does not match the durable path identity");
|
|
1447
|
-
return commit;
|
|
1448
|
-
}));
|
|
1449
|
-
const commits = [];
|
|
1450
|
-
let parent = "genesis";
|
|
1451
|
-
while (remaining.length > 0) {
|
|
1452
|
-
const matches = remaining.filter((commit) => commit.parentHead === parent);
|
|
1453
|
-
if (matches.length !== 1)
|
|
1454
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Immutable commits cannot rebuild one unique authority chain");
|
|
1455
|
-
const next = matches[0];
|
|
1456
|
-
commits.push(next);
|
|
1457
|
-
remaining.splice(remaining.indexOf(next), 1);
|
|
1458
|
-
parent = next.head;
|
|
1459
|
-
}
|
|
1460
|
-
assertFileNativePlayAuthority({
|
|
1461
|
-
schemaVersion: 1,
|
|
1462
|
-
head: parent,
|
|
1463
|
-
commits,
|
|
1464
|
-
});
|
|
1465
|
-
return { schemaVersion: 1, head: parent, commits };
|
|
1466
|
-
}
|
|
1467
|
-
assertFileNativePlayAuthority(published);
|
|
1468
|
-
const commits = [];
|
|
1469
|
-
for (const recorded of published.commits) {
|
|
1470
|
-
const digest = createHash("sha256")
|
|
1471
|
-
.update(JSON.stringify(recorded))
|
|
1472
|
-
.digest("hex");
|
|
1573
|
+
const published = await readAuthorityHead(root);
|
|
1574
|
+
if (published.sequence === 0)
|
|
1575
|
+
return null;
|
|
1576
|
+
const reverse = [];
|
|
1577
|
+
let digest = published.commitDigest;
|
|
1578
|
+
let expectedHead = published.head;
|
|
1579
|
+
let expectedSequence = published.sequence;
|
|
1580
|
+
while (digest !== null) {
|
|
1473
1581
|
const immutable = await readJson(join(root, "runtime", "play-commits", `${digest}.json`)).catch((error) => {
|
|
1474
|
-
throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint is missing an immutable commit: ${
|
|
1582
|
+
throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint is missing an immutable commit: ${expectedHead}`, { cause: error });
|
|
1475
1583
|
});
|
|
1476
1584
|
assertFileNativePlayCommit(immutable);
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1479
|
-
|
|
1585
|
+
if (playCommitDigest(immutable) !== digest ||
|
|
1586
|
+
immutable.head !== expectedHead ||
|
|
1587
|
+
immutable.sequence !== expectedSequence)
|
|
1588
|
+
throw new FileNativeWorldCreationError("world_corrupt", `Accepted endpoint does not match its immutable commit: ${expectedHead}`);
|
|
1589
|
+
reverse.push(immutable);
|
|
1590
|
+
digest = immutable.parentCommitDigest;
|
|
1591
|
+
expectedHead = immutable.parentHead;
|
|
1592
|
+
expectedSequence -= 1;
|
|
1480
1593
|
}
|
|
1481
|
-
|
|
1594
|
+
if (expectedSequence !== 0 || expectedHead !== "genesis")
|
|
1595
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Accepted Authority chain does not terminate at genesis");
|
|
1596
|
+
const authority = {
|
|
1597
|
+
schemaVersion: 2,
|
|
1598
|
+
head: published.head,
|
|
1599
|
+
commits: reverse.reverse(),
|
|
1600
|
+
};
|
|
1601
|
+
assertFileNativePlayAuthority(authority);
|
|
1602
|
+
return authority;
|
|
1482
1603
|
}
|
|
1483
1604
|
catch (error) {
|
|
1484
1605
|
if (error instanceof FileNativeWorldCreationError)
|
|
@@ -1486,10 +1607,118 @@ async function readAcceptedAuthority(root) {
|
|
|
1486
1607
|
throw new FileNativeWorldCreationError("world_corrupt", "Play Authority durable data is corrupt", { cause: error });
|
|
1487
1608
|
}
|
|
1488
1609
|
}
|
|
1610
|
+
async function readMaterializedCheckpoint(root) {
|
|
1611
|
+
const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
|
|
1612
|
+
if (value === null)
|
|
1613
|
+
return null;
|
|
1614
|
+
if (!isRecord(value) ||
|
|
1615
|
+
!hasExactKeys(value, [
|
|
1616
|
+
"schemaVersion",
|
|
1617
|
+
"head",
|
|
1618
|
+
"sequence",
|
|
1619
|
+
"commitDigest",
|
|
1620
|
+
]) ||
|
|
1621
|
+
value.schemaVersion !== 2 ||
|
|
1622
|
+
typeof value.head !== "string" ||
|
|
1623
|
+
!Number.isSafeInteger(value.sequence) ||
|
|
1624
|
+
Number(value.sequence) < 0 ||
|
|
1625
|
+
(value.commitDigest !== null &&
|
|
1626
|
+
(typeof value.commitDigest !== "string" ||
|
|
1627
|
+
!/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
|
|
1628
|
+
(Number(value.sequence) === 0
|
|
1629
|
+
? value.head !== "genesis" || value.commitDigest !== null
|
|
1630
|
+
: value.head !== `commit:${String(value.sequence)}` ||
|
|
1631
|
+
value.commitDigest === null))
|
|
1632
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Materialized world checkpoint has an invalid shape");
|
|
1633
|
+
return value;
|
|
1634
|
+
}
|
|
1635
|
+
function sameMaterializedEndpoint(checkpoint, authority) {
|
|
1636
|
+
return (checkpoint !== null &&
|
|
1637
|
+
checkpoint.head === authority.head &&
|
|
1638
|
+
checkpoint.sequence === authority.sequence &&
|
|
1639
|
+
checkpoint.commitDigest === authority.commitDigest);
|
|
1640
|
+
}
|
|
1641
|
+
async function readAuthorityTail(root, authority, checkpoint) {
|
|
1642
|
+
const targetDigest = checkpoint?.commitDigest ?? null;
|
|
1643
|
+
const targetHead = checkpoint?.head ?? "genesis";
|
|
1644
|
+
const targetSequence = checkpoint?.sequence ?? 0;
|
|
1645
|
+
if (targetSequence > authority.sequence)
|
|
1646
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Materialized checkpoint is ahead of Authority");
|
|
1647
|
+
const reverse = [];
|
|
1648
|
+
let digest = authority.commitDigest;
|
|
1649
|
+
let expectedHead = authority.head;
|
|
1650
|
+
let expectedSequence = authority.sequence;
|
|
1651
|
+
while (digest !== targetDigest) {
|
|
1652
|
+
if (digest === null)
|
|
1653
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Materialized checkpoint is not an ancestor of Authority");
|
|
1654
|
+
const commit = await readPlayCommitByDigest(root, digest);
|
|
1655
|
+
if (commit.head !== expectedHead || commit.sequence !== expectedSequence)
|
|
1656
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Authority tail does not match its immutable commit chain");
|
|
1657
|
+
reverse.push(commit);
|
|
1658
|
+
digest = commit.parentCommitDigest;
|
|
1659
|
+
expectedHead = commit.parentHead;
|
|
1660
|
+
expectedSequence -= 1;
|
|
1661
|
+
}
|
|
1662
|
+
if (expectedHead !== targetHead || expectedSequence !== targetSequence)
|
|
1663
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Materialized checkpoint identity does not match Authority ancestry");
|
|
1664
|
+
return reverse.reverse();
|
|
1665
|
+
}
|
|
1666
|
+
function genesisAuthorityHead() {
|
|
1667
|
+
return {
|
|
1668
|
+
schemaVersion: 2,
|
|
1669
|
+
head: "genesis",
|
|
1670
|
+
sequence: 0,
|
|
1671
|
+
commitDigest: null,
|
|
1672
|
+
operationId: null,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
async function readAuthorityHead(root) {
|
|
1676
|
+
const value = await readJson(join(root, "runtime", playAuthorityHeadFile));
|
|
1677
|
+
assertFileNativePlayAuthorityHead(value);
|
|
1678
|
+
return value;
|
|
1679
|
+
}
|
|
1680
|
+
function assertFileNativePlayAuthorityHead(value) {
|
|
1681
|
+
if (!isRecord(value) ||
|
|
1682
|
+
!hasExactKeys(value, [
|
|
1683
|
+
"schemaVersion",
|
|
1684
|
+
"head",
|
|
1685
|
+
"sequence",
|
|
1686
|
+
"commitDigest",
|
|
1687
|
+
"operationId",
|
|
1688
|
+
]) ||
|
|
1689
|
+
value.schemaVersion !== 2 ||
|
|
1690
|
+
typeof value.head !== "string" ||
|
|
1691
|
+
!Number.isSafeInteger(value.sequence) ||
|
|
1692
|
+
Number(value.sequence) < 0 ||
|
|
1693
|
+
(value.commitDigest !== null &&
|
|
1694
|
+
(typeof value.commitDigest !== "string" ||
|
|
1695
|
+
!/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
|
|
1696
|
+
(value.operationId !== null && typeof value.operationId !== "string") ||
|
|
1697
|
+
(Number(value.sequence) === 0
|
|
1698
|
+
? value.head !== "genesis" ||
|
|
1699
|
+
value.commitDigest !== null ||
|
|
1700
|
+
value.operationId !== null
|
|
1701
|
+
: value.head !== `commit:${String(value.sequence)}` ||
|
|
1702
|
+
value.commitDigest === null ||
|
|
1703
|
+
value.operationId === null))
|
|
1704
|
+
throw new Error("Play Authority head has an invalid shape");
|
|
1705
|
+
}
|
|
1706
|
+
function playCommitDigest(commit) {
|
|
1707
|
+
return createHash("sha256").update(JSON.stringify(commit)).digest("hex");
|
|
1708
|
+
}
|
|
1709
|
+
async function readPlayCommitByDigest(root, digest) {
|
|
1710
|
+
if (!/^[a-f0-9]{64}$/u.test(digest))
|
|
1711
|
+
throw corruptOperationOutcome();
|
|
1712
|
+
const value = await readJson(join(root, "runtime", "play-commits", `${digest}.json`));
|
|
1713
|
+
assertFileNativePlayCommit(value);
|
|
1714
|
+
if (playCommitDigest(value) !== digest)
|
|
1715
|
+
throw new FileNativeWorldCreationError("world_corrupt", "Immutable play commit does not match its durable identity");
|
|
1716
|
+
return value;
|
|
1717
|
+
}
|
|
1489
1718
|
function assertFileNativePlayAuthority(value) {
|
|
1490
1719
|
if (!isRecord(value) ||
|
|
1491
1720
|
!hasExactKeys(value, ["schemaVersion", "head", "commits"]) ||
|
|
1492
|
-
value.schemaVersion !==
|
|
1721
|
+
value.schemaVersion !== 2 ||
|
|
1493
1722
|
typeof value.head !== "string" ||
|
|
1494
1723
|
!Array.isArray(value.commits))
|
|
1495
1724
|
throw new Error("Play Authority has an invalid shape");
|
|
@@ -1497,6 +1726,7 @@ function assertFileNativePlayAuthority(value) {
|
|
|
1497
1726
|
const heads = new Set();
|
|
1498
1727
|
const commitsByHead = new Map();
|
|
1499
1728
|
let parent = "genesis";
|
|
1729
|
+
let parentDigest = null;
|
|
1500
1730
|
for (const [index, commit] of value.commits.entries()) {
|
|
1501
1731
|
assertFileNativePlayCommit(commit);
|
|
1502
1732
|
const revision = commit.timelineRevision;
|
|
@@ -1507,6 +1737,8 @@ function assertFileNativePlayAuthority(value) {
|
|
|
1507
1737
|
? replaced.timelineRevision.restoresHead
|
|
1508
1738
|
: replaced?.parentHead;
|
|
1509
1739
|
if (commit.parentHead !== parent ||
|
|
1740
|
+
commit.parentCommitDigest !== parentDigest ||
|
|
1741
|
+
commit.sequence !== index + 1 ||
|
|
1510
1742
|
commit.head !== `commit:${index + 1}` ||
|
|
1511
1743
|
operationIds.has(commit.operationId) ||
|
|
1512
1744
|
heads.has(commit.head) ||
|
|
@@ -1521,6 +1753,7 @@ function assertFileNativePlayAuthority(value) {
|
|
|
1521
1753
|
heads.add(commit.head);
|
|
1522
1754
|
commitsByHead.set(commit.head, commit);
|
|
1523
1755
|
parent = commit.head;
|
|
1756
|
+
parentDigest = playCommitDigest(commit);
|
|
1524
1757
|
}
|
|
1525
1758
|
if (value.head !== parent)
|
|
1526
1759
|
throw new Error("Play Authority head is invalid");
|
|
@@ -1532,8 +1765,10 @@ function assertFileNativePlayCommit(value) {
|
|
|
1532
1765
|
const timelineRevision = value.mode === "timeline_revision";
|
|
1533
1766
|
if (!hasExactKeys(value, [
|
|
1534
1767
|
"schemaVersion",
|
|
1768
|
+
"sequence",
|
|
1535
1769
|
"operationId",
|
|
1536
1770
|
"parentHead",
|
|
1771
|
+
"parentCommitDigest",
|
|
1537
1772
|
"head",
|
|
1538
1773
|
"mode",
|
|
1539
1774
|
"historyAppend",
|
|
@@ -1542,13 +1777,20 @@ function assertFileNativePlayCommit(value) {
|
|
|
1542
1777
|
...(correction ? ["correctionTargets", "corrects"] : []),
|
|
1543
1778
|
...(timelineRevision ? ["timelineRevision"] : []),
|
|
1544
1779
|
]) ||
|
|
1545
|
-
value.schemaVersion !==
|
|
1780
|
+
value.schemaVersion !== 2 ||
|
|
1781
|
+
!Number.isSafeInteger(value.sequence) ||
|
|
1782
|
+
Number(value.sequence) < 1 ||
|
|
1546
1783
|
typeof value.operationId !== "string" ||
|
|
1547
1784
|
value.operationId.trim() === "" ||
|
|
1548
1785
|
typeof value.parentHead !== "string" ||
|
|
1549
1786
|
!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
|
|
1787
|
+
(value.parentCommitDigest !== null &&
|
|
1788
|
+
(typeof value.parentCommitDigest !== "string" ||
|
|
1789
|
+
!/^[a-f0-9]{64}$/u.test(value.parentCommitDigest))) ||
|
|
1790
|
+
(value.parentHead === "genesis") !== (value.parentCommitDigest === null) ||
|
|
1550
1791
|
typeof value.head !== "string" ||
|
|
1551
1792
|
!/^commit:[1-9][0-9]*$/u.test(value.head) ||
|
|
1793
|
+
value.head !== `commit:${String(value.sequence)}` ||
|
|
1552
1794
|
(value.mode !== "play" &&
|
|
1553
1795
|
value.mode !== "correction" &&
|
|
1554
1796
|
value.mode !== "timeline_revision") ||
|
|
@@ -1666,19 +1908,6 @@ function isFileNativeStateChange(value) {
|
|
|
1666
1908
|
}
|
|
1667
1909
|
return true;
|
|
1668
1910
|
}
|
|
1669
|
-
async function readDirectoryFileNames(root) {
|
|
1670
|
-
try {
|
|
1671
|
-
return (await readdir(root, { withFileTypes: true }))
|
|
1672
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
1673
|
-
.map((entry) => entry.name)
|
|
1674
|
-
.sort();
|
|
1675
|
-
}
|
|
1676
|
-
catch (error) {
|
|
1677
|
-
if (isNodeError(error) && error.code === "ENOENT")
|
|
1678
|
-
return [];
|
|
1679
|
-
throw error;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
1911
|
async function materializePlayCommit(root, sequence, input) {
|
|
1683
1912
|
if (input.mode === "timeline_revision") {
|
|
1684
1913
|
if (input.timelineRevision === undefined)
|
|
@@ -1782,6 +2011,26 @@ function historySurfaceFiles(messages) {
|
|
|
1782
2011
|
};
|
|
1783
2012
|
});
|
|
1784
2013
|
}
|
|
2014
|
+
function materializedHistoryRecord(worldId, files) {
|
|
2015
|
+
const result = {};
|
|
2016
|
+
for (const file of files) {
|
|
2017
|
+
const match = /^(\d{8})-(\d+)-(player|narrator)-[a-f0-9]{12}\.md$/u.exec(file.path);
|
|
2018
|
+
if (match === null)
|
|
2019
|
+
throw new FileNativeWorldCreationError("world_corrupt", `Materialized history path is invalid: ${file.path}`);
|
|
2020
|
+
const sequence = Number.parseInt(match[1], 10);
|
|
2021
|
+
const index = Number.parseInt(match[2], 10);
|
|
2022
|
+
const role = match[3];
|
|
2023
|
+
const messageId = sequence === 0
|
|
2024
|
+
? index === 1 && role === "narrator"
|
|
2025
|
+
? `${worldId}.message.genesis.narrator`
|
|
2026
|
+
: `${worldId}.message.genesis.${index}.${role}`
|
|
2027
|
+
: `${worldId}.message.${sequence}.${index}.${role}`;
|
|
2028
|
+
if (messageId in result)
|
|
2029
|
+
throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
|
|
2030
|
+
result[messageId] = file.contents;
|
|
2031
|
+
}
|
|
2032
|
+
return result;
|
|
2033
|
+
}
|
|
1785
2034
|
async function writeIdempotentText(path, contents) {
|
|
1786
2035
|
try {
|
|
1787
2036
|
const existing = await readFile(path, "utf8");
|
|
@@ -1825,11 +2074,13 @@ function cloneAuthorityPrefix(input) {
|
|
|
1825
2074
|
...structuredClone(message),
|
|
1826
2075
|
messageId: messageIds.get(message.messageId),
|
|
1827
2076
|
}));
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2077
|
+
const clonedCommits = [];
|
|
2078
|
+
let parentCommitDigest = null;
|
|
2079
|
+
for (const commit of sourceCommits) {
|
|
2080
|
+
const cloned = {
|
|
1832
2081
|
...structuredClone(commit),
|
|
2082
|
+
schemaVersion: 2,
|
|
2083
|
+
parentCommitDigest,
|
|
1833
2084
|
operationId: `clone-${operationDigest(`${input.targetWorldId}\0${commit.head}`).slice(0, 56)}`,
|
|
1834
2085
|
historyAppend: rewriteHistory(commit.historyAppend),
|
|
1835
2086
|
nextAdditionalMaterials: rewriteMaterials(commit.nextAdditionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
|
|
@@ -1841,7 +2092,14 @@ function cloneAuthorityPrefix(input) {
|
|
|
1841
2092
|
replacementHistory: rewriteHistory(commit.timelineRevision.replacementHistory),
|
|
1842
2093
|
},
|
|
1843
2094
|
}),
|
|
1844
|
-
}
|
|
2095
|
+
};
|
|
2096
|
+
clonedCommits.push(cloned);
|
|
2097
|
+
parentCommitDigest = playCommitDigest(cloned);
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
genesisHistory: rewriteHistory(input.sourceGenesis.history),
|
|
2101
|
+
genesisMaterials: rewriteMaterials(input.sourceGenesis.additionalMaterials, messageIds, input.sourceWorldId, input.targetWorldId),
|
|
2102
|
+
commits: clonedCommits,
|
|
1845
2103
|
};
|
|
1846
2104
|
}
|
|
1847
2105
|
function rewriteMaterials(materials, messageIds, sourceWorldId, targetWorldId) {
|