narraeon 0.5.7 → 0.6.1
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 +13 -0
- package/README.zh-CN.md +10 -0
- package/dist/node/protocol/gameplay.js +69 -0
- package/dist/node/protocol/resolution.js +274 -0
- package/dist/node/protocol/v1.js +15 -2
- package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
- package/dist/node/runtime/RuntimeErrorLog.js +95 -0
- package/dist/node/runtime/V1Runtime.js +58 -7
- package/dist/node/runtime/archive/WorldArchives.js +14 -46
- package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
- package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
- package/dist/node/runtime/config/dataRootLock.js +108 -0
- package/dist/node/runtime/content/ContentWorkspace.js +10 -12
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +50 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
- package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
- package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
- package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +31 -170
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +36 -239
- package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
- package/dist/node/runtime/play/PlayCallChain.js +359 -359
- package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
- package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
- package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
- package/dist/node/runtime/play/TabletopResolution.js +372 -0
- package/dist/node/runtime/play/TabletopRules.js +141 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -10
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +35 -2
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
- package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
- package/dist/node/runtime/world/FileNativeWorldStore.js +266 -783
- package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
- package/dist/node/runtime/world/WorldActivity.js +14 -22
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
- package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
- package/dist/node/runtime/world/WorldQueue.js +46 -0
- package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -12
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
- package/dist/node/server/ConversationStream.js +2 -0
- package/dist/node/server/mobile.js +20 -0
- package/dist/node/server/startWebServer.js +15 -8
- package/dist/node/shared/ordered-author-prompts.js +8 -0
- package/dist/node/shared/ordered-play-prompts.js +23 -0
- package/dist/node/shared/tabletop-guidance.js +100 -0
- package/dist/web/assets/index-7GzLSUpU.js +1323 -0
- package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
- package/dist/web/assets/index-DxNtxzLR.js +0 -1255
- package/dist/web/assets/index-DzOnu9uv.css +0 -1
|
@@ -1,62 +1,33 @@
|
|
|
1
|
+
import { isResolutionReceipt } from "../../protocol/resolution.js";
|
|
1
2
|
import { legacyModelBindingFailure, projectLegacyModelBindingFailure, } from "./LegacyModelBindingFailure.js";
|
|
2
3
|
import { isPlayFailureKind, } from "../../protocol/playFailure.js";
|
|
3
4
|
import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
|
|
4
5
|
import { encodePlayPromptRun, decodePlayPromptRun, } from "./PlayPromptRun.js";
|
|
5
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { chmod, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
9
|
+
import { linkExclusive } from "../FileNativeExclusiveLink.js";
|
|
7
10
|
import { dirname, join, resolve } from "node:path";
|
|
8
11
|
import { isDeepStrictEqual } from "node:util";
|
|
9
|
-
import { cloneFilePhysically } from "../FileNativePhysicalClone.js";
|
|
10
12
|
export class FileNativePlayTimelineStore {
|
|
11
13
|
#worldsRoot;
|
|
12
14
|
constructor(dataRoot) {
|
|
13
15
|
this.#worldsRoot = join(resolve(dataRoot), "worlds-file-native");
|
|
14
16
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Convert the cumulative call-chain record written by every released
|
|
17
|
-
* Narraeon version through v0.1.1 into immutable context segments. The
|
|
18
|
-
* source digest makes interrupted retries reproduce the same generation and
|
|
19
|
-
* identities; callers publish the world-wide storage marker only after this
|
|
20
|
-
* method has completed.
|
|
21
|
-
*/
|
|
22
|
-
async migrateReleasedRecord(worldId, value, sourceDigest) {
|
|
23
|
-
assertIdentity(worldId, "World ID");
|
|
24
|
-
if (!/^[a-f0-9]{64}$/u.test(sourceDigest))
|
|
25
|
-
throw new TypeError("Released call-chain source digest is invalid");
|
|
26
|
-
assertReleasedPersistedPlayCallChain(value, worldId);
|
|
27
|
-
const contexts = [
|
|
28
|
-
...(value.previousContexts ?? []).map((context) => structuredClone(context)),
|
|
29
|
-
releasedCurrentContext(value),
|
|
30
|
-
];
|
|
31
|
-
const seen = new Set();
|
|
32
|
-
const generation = `released:${sourceDigest}`;
|
|
33
|
-
let previousChainId = null;
|
|
34
|
-
for (const context of contexts) {
|
|
35
|
-
assertIdentity(context.chainId, "Call-chain ID");
|
|
36
|
-
if (seen.has(context.chainId))
|
|
37
|
-
throw new Error("Released play-call-chain contexts contain a duplicate identity");
|
|
38
|
-
seen.add(context.chainId);
|
|
39
|
-
await this.persist({
|
|
40
|
-
...structuredClone(context),
|
|
41
|
-
events: context.events.map(normalizePlayEvent),
|
|
42
|
-
schemaVersion: 3,
|
|
43
|
-
kind: "play_call_chain",
|
|
44
|
-
worldId,
|
|
45
|
-
previousChainId,
|
|
46
|
-
timelineGeneration: generation,
|
|
47
|
-
documentAuthorizationCheckpoints: structuredClone(context.documentAuthorizationCheckpoints ?? []),
|
|
48
|
-
});
|
|
49
|
-
previousChainId = context.chainId;
|
|
50
|
-
}
|
|
51
|
-
const migrated = await this.readAllContexts(worldId);
|
|
52
|
-
if (migrated.length !== contexts.length ||
|
|
53
|
-
migrated.some((context, index) => context.chainId !== contexts[index]?.chainId))
|
|
54
|
-
throw new Error("Released play-call-chain migration did not reproduce its context chain");
|
|
55
|
-
}
|
|
56
17
|
async persist(value, cursor) {
|
|
57
18
|
return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor);
|
|
58
19
|
}
|
|
59
20
|
/** Write a sealed context segment without making it the active timeline. */
|
|
21
|
+
/** Physically remove the segments of contexts a rewind discarded. */
|
|
22
|
+
async discardContexts(worldId, chainIds) {
|
|
23
|
+
for (const chainId of chainIds) {
|
|
24
|
+
assertIdentity(chainId, "Call-chain ID");
|
|
25
|
+
await rm(this.#contextRoot(worldId, chainId), {
|
|
26
|
+
recursive: true,
|
|
27
|
+
force: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
60
31
|
async persistDetached(value, cursor) {
|
|
61
32
|
return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor, false);
|
|
62
33
|
}
|
|
@@ -72,7 +43,6 @@ export class FileNativePlayTimelineStore {
|
|
|
72
43
|
const transcriptCount = prefixLength(input.source.transcript, input.target.transcript);
|
|
73
44
|
const eventCount = prefixLength(input.source.events, input.target.events);
|
|
74
45
|
const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
|
|
75
|
-
const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
|
|
76
46
|
if (input.retainCompleteContext ||
|
|
77
47
|
input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
|
|
78
48
|
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
|
|
@@ -113,19 +83,11 @@ export class FileNativePlayTimelineStore {
|
|
|
113
83
|
count: completedToolCount,
|
|
114
84
|
immutable: true,
|
|
115
85
|
}),
|
|
116
|
-
cloneNumberedPrefix({
|
|
117
|
-
sourceRoot,
|
|
118
|
-
targetRoot,
|
|
119
|
-
directory: "authorization",
|
|
120
|
-
count: authorizationCheckpointCount,
|
|
121
|
-
immutable: true,
|
|
122
|
-
}),
|
|
123
86
|
]);
|
|
124
87
|
return this.persistStaged(input.targetWorldRoot, input.target, {
|
|
125
88
|
transcriptCount,
|
|
126
89
|
eventCount,
|
|
127
90
|
completedToolCount,
|
|
128
|
-
authorizationCheckpointCount,
|
|
129
91
|
});
|
|
130
92
|
}
|
|
131
93
|
async #persistAtRuntimeRoot(runtimeRoot, value, cursor, publishHead = true) {
|
|
@@ -155,7 +117,6 @@ export class FileNativePlayTimelineStore {
|
|
|
155
117
|
eventCount: Math.min(supplied.eventCount, value.events.length),
|
|
156
118
|
transcriptCount: Math.min(supplied.transcriptCount, value.transcript.length),
|
|
157
119
|
completedToolCount: Math.min(supplied.completedToolCount, value.completedTools.length),
|
|
158
|
-
authorizationCheckpointCount: Math.min(supplied.authorizationCheckpointCount, value.documentAuthorizationCheckpoints?.length ?? 0),
|
|
159
120
|
};
|
|
160
121
|
const lastRequestDigest = value.lastRequest === null
|
|
161
122
|
? null
|
|
@@ -186,9 +147,6 @@ export class FileNativePlayTimelineStore {
|
|
|
186
147
|
}
|
|
187
148
|
for (let index = previous.completedToolCount; index < value.completedTools.length; index += 1)
|
|
188
149
|
await publishImmutableJson(join(root, "completed-tools", numbered(index + 1)), value.completedTools[index]);
|
|
189
|
-
const checkpoints = value.documentAuthorizationCheckpoints ?? [];
|
|
190
|
-
for (let index = previous.authorizationCheckpointCount; index < checkpoints.length; index += 1)
|
|
191
|
-
await publishImmutableJson(join(root, "authorization", numbered(index + 1)), checkpoints[index]);
|
|
192
150
|
const promptRuns = value.promptRuns ?? [];
|
|
193
151
|
for (let index = supplied.promptRunCount ?? 0; index < promptRuns.length; index += 1) {
|
|
194
152
|
await publishImmutableJson(join(root, "prompt-runs", numbered(index + 1)), encodePlayPromptRun(promptRuns[index]));
|
|
@@ -202,7 +160,6 @@ export class FileNativePlayTimelineStore {
|
|
|
202
160
|
eventCount: value.events.length,
|
|
203
161
|
transcriptCount: value.transcript.length,
|
|
204
162
|
completedToolCount: value.completedTools.length,
|
|
205
|
-
authorizationCheckpointCount: checkpoints.length,
|
|
206
163
|
promptRunCount: promptRuns.length,
|
|
207
164
|
changedDocuments: structuredClone(value.changedDocuments),
|
|
208
165
|
nextEventId: value.nextEventId,
|
|
@@ -227,7 +184,6 @@ export class FileNativePlayTimelineStore {
|
|
|
227
184
|
eventCount: value.events.length,
|
|
228
185
|
transcriptCount: value.transcript.length,
|
|
229
186
|
completedToolCount: value.completedTools.length,
|
|
230
|
-
authorizationCheckpointCount: checkpoints.length,
|
|
231
187
|
promptRunCount: promptRuns.length,
|
|
232
188
|
};
|
|
233
189
|
}
|
|
@@ -262,16 +218,14 @@ export class FileNativePlayTimelineStore {
|
|
|
262
218
|
if (metadata === null)
|
|
263
219
|
return null;
|
|
264
220
|
const { index: indexValue, state: stateValue, root } = metadata;
|
|
265
|
-
const [baseValue, continuationValue, transcript,
|
|
221
|
+
const [baseValue, continuationValue, transcript, events, completedTools, promptRuns,] = await Promise.all([
|
|
266
222
|
readJson(join(root, "base.json")),
|
|
267
223
|
readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`)),
|
|
268
224
|
readNumbered(join(root, "transcript"), stateValue.transcriptCount),
|
|
269
225
|
readNumberedValidated(join(root, "events"), stateValue.eventCount, assertPlayEvent),
|
|
270
226
|
readNumbered(join(root, "completed-tools"), stateValue.completedToolCount),
|
|
271
|
-
readNumbered(join(root, "authorization"), stateValue.authorizationCheckpointCount),
|
|
272
227
|
readNumbered(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0).then((records) => records.map(decodePlayPromptRun)),
|
|
273
228
|
]);
|
|
274
|
-
const events = rawEvents.map(normalizePlayEvent);
|
|
275
229
|
assertContextBase(baseValue);
|
|
276
230
|
assertContextContinuation(continuationValue);
|
|
277
231
|
if (baseValue.worldId !== worldId ||
|
|
@@ -334,7 +288,6 @@ export class FileNativePlayTimelineStore {
|
|
|
334
288
|
transcript,
|
|
335
289
|
events,
|
|
336
290
|
completedTools,
|
|
337
|
-
documentAuthorizationCheckpoints: checkpoints,
|
|
338
291
|
promptRuns,
|
|
339
292
|
changedDocuments: structuredClone(stateValue.changedDocuments),
|
|
340
293
|
nextMaterials: structuredClone(continuationValue.nextMaterials),
|
|
@@ -359,7 +312,6 @@ export class FileNativePlayTimelineStore {
|
|
|
359
312
|
eventCount: stateValue.eventCount,
|
|
360
313
|
transcriptCount: stateValue.transcriptCount,
|
|
361
314
|
completedToolCount: stateValue.completedToolCount,
|
|
362
|
-
authorizationCheckpointCount: stateValue.authorizationCheckpointCount,
|
|
363
315
|
promptRunCount: stateValue.promptRunCount ?? 0,
|
|
364
316
|
},
|
|
365
317
|
};
|
|
@@ -546,7 +498,7 @@ export class FileNativePlayTimelineStore {
|
|
|
546
498
|
assertPlayEvent(event);
|
|
547
499
|
if (event.id !== eventId)
|
|
548
500
|
throw new Error("Play timeline event identity is inconsistent");
|
|
549
|
-
return projectLegacyModelBindingFailure(
|
|
501
|
+
return projectLegacyModelBindingFailure(event);
|
|
550
502
|
}
|
|
551
503
|
newGeneration() {
|
|
552
504
|
return `timeline:${randomUUID()}`;
|
|
@@ -760,7 +712,6 @@ function emptyPersistenceCursor() {
|
|
|
760
712
|
eventCount: 0,
|
|
761
713
|
transcriptCount: 0,
|
|
762
714
|
completedToolCount: 0,
|
|
763
|
-
authorizationCheckpointCount: 0,
|
|
764
715
|
};
|
|
765
716
|
}
|
|
766
717
|
function numbered(index) {
|
|
@@ -829,7 +780,6 @@ function assertContextState(value) {
|
|
|
829
780
|
!validCount(value.eventCount) ||
|
|
830
781
|
!validCount(value.transcriptCount) ||
|
|
831
782
|
!validCount(value.completedToolCount) ||
|
|
832
|
-
!validCount(value.authorizationCheckpointCount) ||
|
|
833
783
|
(value.promptRunCount !== undefined && !validCount(value.promptRunCount)) ||
|
|
834
784
|
!Array.isArray(value.changedDocuments) ||
|
|
835
785
|
!validCount(value.nextEventId) ||
|
|
@@ -864,65 +814,6 @@ function assertTimelineHead(value) {
|
|
|
864
814
|
typeof value.generation !== "string")
|
|
865
815
|
throw new Error("Play timeline head has an invalid shape");
|
|
866
816
|
}
|
|
867
|
-
function assertReleasedPersistedPlayCallChain(value, worldId) {
|
|
868
|
-
if (!isRecord(value) ||
|
|
869
|
-
(value.schemaVersion !== 1 && value.schemaVersion !== 2) ||
|
|
870
|
-
value.kind !== "play_call_chain" ||
|
|
871
|
-
value.worldId !== worldId ||
|
|
872
|
-
!isReleasedPlayContext(value) ||
|
|
873
|
-
(value.previousContexts !== undefined &&
|
|
874
|
-
(!Array.isArray(value.previousContexts) ||
|
|
875
|
-
value.previousContexts.some((context) => !isReleasedPlayContext(context)))))
|
|
876
|
-
throw new Error("Released play-call-chain durable data has an invalid shape");
|
|
877
|
-
}
|
|
878
|
-
function isReleasedPlayContext(value) {
|
|
879
|
-
if (!isRecord(value))
|
|
880
|
-
return false;
|
|
881
|
-
if (typeof value.chainId !== "string" ||
|
|
882
|
-
typeof value.baselineHead !== "string" ||
|
|
883
|
-
(value.continuityContextId !== undefined &&
|
|
884
|
-
(typeof value.continuityContextId !== "string" ||
|
|
885
|
-
value.continuityContextId.length === 0)) ||
|
|
886
|
-
(value.baselineHistoryLength !== undefined &&
|
|
887
|
-
!validCount(value.baselineHistoryLength)) ||
|
|
888
|
-
typeof value.parentHead !== "string" ||
|
|
889
|
-
!validReleasedDerivedFrom(value.derivedFrom) ||
|
|
890
|
-
!validReleasedBranchedBeforePlayer(value.branchedBeforePlayer) ||
|
|
891
|
-
!isRecord(value.playPreset) ||
|
|
892
|
-
(value.followups !== undefined && !Array.isArray(value.followups)) ||
|
|
893
|
-
(value.playPresetScriptsEnabled !== undefined &&
|
|
894
|
-
typeof value.playPresetScriptsEnabled !== "boolean") ||
|
|
895
|
-
(value.modelBinding !== undefined &&
|
|
896
|
-
!validModelHostBinding(value.modelBinding)) ||
|
|
897
|
-
(value.status !== "ready" &&
|
|
898
|
-
value.status !== "running" &&
|
|
899
|
-
value.status !== "interrupted") ||
|
|
900
|
-
typeof value.canRetry !== "boolean" ||
|
|
901
|
-
!isRecord(value.bootstrap) ||
|
|
902
|
-
!Array.isArray(value.tools) ||
|
|
903
|
-
!Array.isArray(value.transcript) ||
|
|
904
|
-
!Array.isArray(value.events) ||
|
|
905
|
-
!Array.isArray(value.completedTools) ||
|
|
906
|
-
!Array.isArray(value.changedDocuments) ||
|
|
907
|
-
!Array.isArray(value.nextMaterials) ||
|
|
908
|
-
!validCount(value.nextEventId) ||
|
|
909
|
-
!validCount(value.exchange) ||
|
|
910
|
-
(value.lastRequest !== null && !isRecord(value.lastRequest)) ||
|
|
911
|
-
!validCount(value.lastRequestAttempt) ||
|
|
912
|
-
(value.lastFailure !== null && typeof value.lastFailure !== "string") ||
|
|
913
|
-
(value.lastFailureKind !== undefined &&
|
|
914
|
-
!isPlayFailureKind(value.lastFailureKind)) ||
|
|
915
|
-
typeof value.updatedAt !== "number")
|
|
916
|
-
return false;
|
|
917
|
-
try {
|
|
918
|
-
for (const event of value.events)
|
|
919
|
-
assertPlayEvent(event);
|
|
920
|
-
}
|
|
921
|
-
catch {
|
|
922
|
-
return false;
|
|
923
|
-
}
|
|
924
|
-
return validReleasedAuthorizationCheckpoints(value.documentAuthorizationCheckpoints, value.events);
|
|
925
|
-
}
|
|
926
817
|
function validModelHostBinding(value) {
|
|
927
818
|
return (isRecord(value) &&
|
|
928
819
|
(value.provider === "chat_completions" ||
|
|
@@ -945,54 +836,6 @@ function validModelHostBinding(value) {
|
|
|
945
836
|
value.cacheStrategy === "explicit_cliproxyapi_message" ||
|
|
946
837
|
value.cacheStrategy === "provider_managed"));
|
|
947
838
|
}
|
|
948
|
-
function validReleasedDerivedFrom(value) {
|
|
949
|
-
return (value === undefined ||
|
|
950
|
-
(isRecord(value) &&
|
|
951
|
-
typeof value.worldId === "string" &&
|
|
952
|
-
typeof value.chainId === "string" &&
|
|
953
|
-
typeof value.head === "string"));
|
|
954
|
-
}
|
|
955
|
-
function validReleasedBranchedBeforePlayer(value) {
|
|
956
|
-
return (value === undefined ||
|
|
957
|
-
(isRecord(value) &&
|
|
958
|
-
typeof value.worldId === "string" &&
|
|
959
|
-
typeof value.chainId === "string" &&
|
|
960
|
-
Number.isSafeInteger(value.eventId) &&
|
|
961
|
-
Number(value.eventId) > 0));
|
|
962
|
-
}
|
|
963
|
-
function validReleasedAuthorizationCheckpoints(value, events) {
|
|
964
|
-
if (value === undefined)
|
|
965
|
-
return true;
|
|
966
|
-
if (!Array.isArray(value) || value.length > 100_000)
|
|
967
|
-
return false;
|
|
968
|
-
const eventIds = new Set(events.flatMap((event) => isRecord(event) && Number.isSafeInteger(event.id) && Number(event.id) > 0
|
|
969
|
-
? [Number(event.id)]
|
|
970
|
-
: []));
|
|
971
|
-
let previousEventId = -1;
|
|
972
|
-
for (const checkpoint of value) {
|
|
973
|
-
if (!isRecord(checkpoint) ||
|
|
974
|
-
Object.keys(checkpoint).length !== 2 ||
|
|
975
|
-
!Object.hasOwn(checkpoint, "afterEventId") ||
|
|
976
|
-
!Object.hasOwn(checkpoint, "authorization") ||
|
|
977
|
-
!Number.isSafeInteger(checkpoint.afterEventId) ||
|
|
978
|
-
Number(checkpoint.afterEventId) < 0 ||
|
|
979
|
-
Number(checkpoint.afterEventId) <= previousEventId ||
|
|
980
|
-
(Number(checkpoint.afterEventId) !== 0 &&
|
|
981
|
-
!eventIds.has(Number(checkpoint.afterEventId))) ||
|
|
982
|
-
!isRecord(checkpoint.authorization))
|
|
983
|
-
return false;
|
|
984
|
-
previousEventId = Number(checkpoint.afterEventId);
|
|
985
|
-
}
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
function releasedCurrentContext(value) {
|
|
989
|
-
const { schemaVersion: _schemaVersion, kind: _kind, worldId: _worldId, previousContexts: _previousContexts, ...context } = value;
|
|
990
|
-
void _schemaVersion;
|
|
991
|
-
void _kind;
|
|
992
|
-
void _worldId;
|
|
993
|
-
void _previousContexts;
|
|
994
|
-
return structuredClone(context);
|
|
995
|
-
}
|
|
996
839
|
function assertPlayEvent(value) {
|
|
997
840
|
if (!isRecord(value) ||
|
|
998
841
|
!Number.isSafeInteger(value.id) ||
|
|
@@ -1028,6 +871,9 @@ function assertPlayEvent(value) {
|
|
|
1028
871
|
return;
|
|
1029
872
|
}
|
|
1030
873
|
if (value.kind === "tool_result") {
|
|
874
|
+
if (value.resolution !== undefined &&
|
|
875
|
+
!isResolutionReceipt(value.resolution))
|
|
876
|
+
throw new Error("Invalid resolution receipt");
|
|
1031
877
|
if (typeof value.callId !== "string" ||
|
|
1032
878
|
typeof value.name !== "string" ||
|
|
1033
879
|
typeof value.ok !== "boolean" ||
|
|
@@ -1091,6 +937,9 @@ function assertTimelineEventSummary(value) {
|
|
|
1091
937
|
return;
|
|
1092
938
|
}
|
|
1093
939
|
if (value.kind === "tool_result") {
|
|
940
|
+
if (value.resolution !== undefined &&
|
|
941
|
+
!isResolutionReceipt(value.resolution))
|
|
942
|
+
throw new Error("Invalid resolution receipt");
|
|
1094
943
|
if (typeof value.callId !== "string" ||
|
|
1095
944
|
typeof value.name !== "string" ||
|
|
1096
945
|
typeof value.ok !== "boolean" ||
|
|
@@ -1133,43 +982,7 @@ function validOptionalString(value) {
|
|
|
1133
982
|
return value === undefined || typeof value === "string";
|
|
1134
983
|
}
|
|
1135
984
|
function validOptionalUsage(value) {
|
|
1136
|
-
return
|
|
1137
|
-
(isRecord(value) &&
|
|
1138
|
-
validUsageToken(value.inputTokens) &&
|
|
1139
|
-
validUsageToken(value.outputTokens) &&
|
|
1140
|
-
(value.provenance === undefined || validFullUsage(value))));
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* v0.1.1 stored only input/output usage. Keep that released shape readable,
|
|
1144
|
-
* but expose one current protocol projection with honest unavailable fields.
|
|
1145
|
-
*/
|
|
1146
|
-
function normalizePlayEvent(event) {
|
|
1147
|
-
if ((event.kind !== "assistant" && event.kind !== "followup") ||
|
|
1148
|
-
event.usage === undefined ||
|
|
1149
|
-
validFullUsage(event.usage))
|
|
1150
|
-
return structuredClone(event);
|
|
1151
|
-
const legacy = event.usage;
|
|
1152
|
-
const unavailable = "unavailable";
|
|
1153
|
-
const provider = (value) => value === null ? unavailable : "provider";
|
|
1154
|
-
const usage = {
|
|
1155
|
-
inputTokens: legacy.inputTokens,
|
|
1156
|
-
uncachedInputTokens: null,
|
|
1157
|
-
cacheReadTokens: null,
|
|
1158
|
-
cacheWriteTokens: null,
|
|
1159
|
-
reasoningTokens: null,
|
|
1160
|
-
outputTokens: legacy.outputTokens,
|
|
1161
|
-
totalTokens: null,
|
|
1162
|
-
provenance: {
|
|
1163
|
-
inputTokens: provider(legacy.inputTokens),
|
|
1164
|
-
uncachedInputTokens: unavailable,
|
|
1165
|
-
cacheReadTokens: unavailable,
|
|
1166
|
-
cacheWriteTokens: unavailable,
|
|
1167
|
-
reasoningTokens: unavailable,
|
|
1168
|
-
outputTokens: provider(legacy.outputTokens),
|
|
1169
|
-
totalTokens: unavailable,
|
|
1170
|
-
},
|
|
1171
|
-
};
|
|
1172
|
-
return { ...structuredClone(event), usage };
|
|
985
|
+
return value === undefined || validFullUsage(value);
|
|
1173
986
|
}
|
|
1174
987
|
function validFullUsage(value) {
|
|
1175
988
|
if (!isRecord(value) || !isRecord(value.provenance))
|
|
@@ -1225,12 +1038,18 @@ async function cloneNumberedPrefix(input) {
|
|
|
1225
1038
|
await cloneTimelineFile(join(input.sourceRoot, input.directory, numbered(index)), join(input.targetRoot, input.directory, numbered(index)), input.immutable);
|
|
1226
1039
|
}
|
|
1227
1040
|
async function cloneTimelineFile(source, target, immutable) {
|
|
1228
|
-
await
|
|
1229
|
-
|
|
1230
|
-
target,
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1041
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1042
|
+
try {
|
|
1043
|
+
await copyFile(source, target, constants.COPYFILE_EXCL);
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
1047
|
+
throw error;
|
|
1048
|
+
await assertSameFileBytes(source, target);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (immutable)
|
|
1052
|
+
await chmod(target, 0o444);
|
|
1234
1053
|
}
|
|
1235
1054
|
async function assertSameFileBytes(source, target) {
|
|
1236
1055
|
const [sourceBytes, targetBytes] = await Promise.all([
|
|
@@ -1272,9 +1091,7 @@ async function publishJson(path, value) {
|
|
|
1272
1091
|
encoding: "utf8",
|
|
1273
1092
|
mode: 0o600,
|
|
1274
1093
|
});
|
|
1275
|
-
await syncFile(temporary);
|
|
1276
1094
|
await rename(temporary, path);
|
|
1277
|
-
await syncDirectory(dirname(path));
|
|
1278
1095
|
}
|
|
1279
1096
|
async function publishImmutableJson(path, value) {
|
|
1280
1097
|
const contents = `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -1286,10 +1103,8 @@ async function publishImmutableJson(path, value) {
|
|
|
1286
1103
|
flag: "wx",
|
|
1287
1104
|
mode: 0o600,
|
|
1288
1105
|
});
|
|
1289
|
-
await syncFile(temporary);
|
|
1290
1106
|
try {
|
|
1291
|
-
await
|
|
1292
|
-
await syncDirectory(dirname(path));
|
|
1107
|
+
await linkExclusive(temporary, path);
|
|
1293
1108
|
}
|
|
1294
1109
|
catch (error) {
|
|
1295
1110
|
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
@@ -1304,21 +1119,3 @@ async function publishImmutableJson(path, value) {
|
|
|
1304
1119
|
await rm(temporary, { force: true });
|
|
1305
1120
|
}
|
|
1306
1121
|
}
|
|
1307
|
-
async function syncFile(path) {
|
|
1308
|
-
const handle = await open(path, "r");
|
|
1309
|
-
try {
|
|
1310
|
-
await handle.sync();
|
|
1311
|
-
}
|
|
1312
|
-
finally {
|
|
1313
|
-
await handle.close();
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
async function syncDirectory(path) {
|
|
1317
|
-
const handle = await open(path, "r");
|
|
1318
|
-
try {
|
|
1319
|
-
await handle.sync();
|
|
1320
|
-
}
|
|
1321
|
-
finally {
|
|
1322
|
-
await handle.close();
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { parseDocument } from "yaml";
|
|
2
|
+
import { isMap, isSeq, parseDocument } from "yaml";
|
|
3
3
|
import { builtinPlayPrompts, isPromptMessageRole, } from "../../shared/ordered-play-prompts.js";
|
|
4
4
|
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
5
5
|
export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
@@ -24,20 +24,23 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
24
24
|
!isPromptMessageRole(item.messageRole))
|
|
25
25
|
return fail();
|
|
26
26
|
const messageRole = item.messageRole === undefined ? {} : { messageRole: item.messageRole };
|
|
27
|
-
const exact = (keys) => Object.keys(item).
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const exact = (keys, optional = []) => Object.keys(item).every((key) => keys.includes(key) ||
|
|
28
|
+
optional.includes(key) ||
|
|
29
|
+
key === "messageRole" ||
|
|
30
|
+
key === "id" ||
|
|
31
|
+
key === "kind") && keys.every((key) => Object.hasOwn(item, key));
|
|
30
32
|
if (item.kind === "world" && exact(["id", "kind"])) {
|
|
31
33
|
worlds++;
|
|
32
34
|
return { id: item.id, kind: "world", ...messageRole };
|
|
33
35
|
}
|
|
34
36
|
if (item.kind === "user" &&
|
|
35
|
-
exact(["id", "kind", "name", "enabled", "body"]) &&
|
|
37
|
+
exact(["id", "kind", "name", "enabled", "body"], ["gameplay"]) &&
|
|
36
38
|
typeof item.name === "string" &&
|
|
37
39
|
item.name.trim().length > 0 &&
|
|
38
40
|
/^[^\r\n\0]{1,160}$/u.test(item.name) &&
|
|
39
41
|
typeof item.body === "string" &&
|
|
40
|
-
typeof item.enabled === "boolean"
|
|
42
|
+
typeof item.enabled === "boolean" &&
|
|
43
|
+
(item.gameplay === undefined || item.gameplay === "tabletop"))
|
|
41
44
|
return {
|
|
42
45
|
id: item.id,
|
|
43
46
|
kind: "user",
|
|
@@ -45,6 +48,9 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
45
48
|
enabled: item.enabled,
|
|
46
49
|
body: item.body,
|
|
47
50
|
...messageRole,
|
|
51
|
+
...(item.gameplay === "tabletop"
|
|
52
|
+
? { gameplay: "tabletop" }
|
|
53
|
+
: {}),
|
|
48
54
|
};
|
|
49
55
|
const builtin = catalog.find((entry) => entry.id === item.builtin);
|
|
50
56
|
if (item.kind !== "builtin" ||
|
|
@@ -70,6 +76,54 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
70
76
|
return fail();
|
|
71
77
|
return entries;
|
|
72
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Insert the tabletop builtins that an ordered preset does not list yet.
|
|
81
|
+
* play.tabletop goes just before the world placeholder and author.tabletop just
|
|
82
|
+
* after author.guidance. Files that are not ordered v2 or do not parse come back
|
|
83
|
+
* unchanged, and nothing else in them is rewritten. The same object is returned
|
|
84
|
+
* when nothing needs inserting, so callers can detect a change by identity.
|
|
85
|
+
*/
|
|
86
|
+
export function withTabletopPromptBuiltins(files) {
|
|
87
|
+
const source = files["preset.yaml"];
|
|
88
|
+
if (source === undefined)
|
|
89
|
+
return files;
|
|
90
|
+
const document = parseDocument(source, { uniqueKeys: true });
|
|
91
|
+
if (document.errors.length > 0 ||
|
|
92
|
+
document.warnings.length > 0 ||
|
|
93
|
+
!isMap(document.contents) ||
|
|
94
|
+
document.get("format") !== "narraeon.play-preset/v2")
|
|
95
|
+
return files;
|
|
96
|
+
let changed = false;
|
|
97
|
+
const playPrompts = document.get("playPrompts", true);
|
|
98
|
+
if (isSeq(playPrompts) && !hasEntry(playPrompts, "play.tabletop")) {
|
|
99
|
+
const world = playPrompts.items.findIndex((item) => entryField(item, "kind") === "world");
|
|
100
|
+
playPrompts.items.splice(world < 0 ? playPrompts.items.length : world, 0, document.createNode({
|
|
101
|
+
id: "play.tabletop",
|
|
102
|
+
kind: "builtin",
|
|
103
|
+
builtin: "play.tabletop",
|
|
104
|
+
enabled: true,
|
|
105
|
+
}));
|
|
106
|
+
changed = true;
|
|
107
|
+
}
|
|
108
|
+
const authorPrompts = document.get("authorPrompts", true);
|
|
109
|
+
if (isSeq(authorPrompts) && !hasEntry(authorPrompts, "author.tabletop")) {
|
|
110
|
+
const guidance = authorPrompts.items.findIndex((item) => entryField(item, "id") === "author.guidance");
|
|
111
|
+
authorPrompts.items.splice(guidance < 0 ? authorPrompts.items.length : guidance + 1, 0, document.createNode({
|
|
112
|
+
id: "author.tabletop",
|
|
113
|
+
kind: "builtin",
|
|
114
|
+
builtin: "author.tabletop",
|
|
115
|
+
enabled: true,
|
|
116
|
+
}));
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
return changed ? { ...files, "preset.yaml": document.toString() } : files;
|
|
120
|
+
}
|
|
121
|
+
function hasEntry(sequence, id) {
|
|
122
|
+
return sequence.items.some((item) => entryField(item, "id") === id);
|
|
123
|
+
}
|
|
124
|
+
function entryField(item, key) {
|
|
125
|
+
return isMap(item) ? item.get(key) : undefined;
|
|
126
|
+
}
|
|
73
127
|
/** Recognized v1 content becomes user text, never a locked builtin inferred from a path. */
|
|
74
128
|
export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
75
129
|
const document = parseDocument(files["frame.yaml"] ?? "", {
|
|
@@ -185,5 +239,13 @@ export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
|
185
239
|
if ((path.startsWith("blocks/") || path.startsWith("prompts/")) &&
|
|
186
240
|
path.endsWith(".md"))
|
|
187
241
|
add(path, false);
|
|
242
|
+
// The tabletop builtin joins converted presets just before the world placeholder.
|
|
243
|
+
const worldIndex = entries.findIndex((entry) => entry.kind === "world");
|
|
244
|
+
entries.splice(worldIndex, 0, {
|
|
245
|
+
id: "play.tabletop",
|
|
246
|
+
kind: "builtin",
|
|
247
|
+
builtin: "play.tabletop",
|
|
248
|
+
enabled: true,
|
|
249
|
+
});
|
|
188
250
|
return entries;
|
|
189
251
|
}
|