narraeon 0.6.0 → 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 +11 -0
- package/README.zh-CN.md +8 -0
- package/dist/node/protocol/v1.js +0 -2
- package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
- package/dist/node/runtime/V1Runtime.js +1 -5
- 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 +0 -10
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
- 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 +32 -207
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
- package/dist/node/runtime/play/PlayCallChain.js +338 -366
- 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/setting/FileNativeSettingImprovementStore.js +3 -15
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
- package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
- 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 +0 -12
- package/dist/node/server/mobile.js +20 -0
- package/dist/node/server/startWebServer.js +15 -8
- package/dist/web/assets/{index-BgBL8G-A.js → index-7GzLSUpU.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
- package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { HistoryQuery, historyInputs } from "../history/HistoryQuery.js";
|
|
2
3
|
import { renderHistoryResult } from "../history/HistoryRendering.js";
|
|
3
4
|
import { stateDirectoryHandle, parseStateDirectoryHandle, } from "../world/StateDirectoryHandles.js";
|
|
4
5
|
import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
6
|
import { stringify } from "yaml";
|
|
7
7
|
import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
8
8
|
import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
|
|
@@ -56,66 +56,16 @@ export class FileNativePlayDocuments {
|
|
|
56
56
|
get committedSnapshot() {
|
|
57
57
|
return this.#committedSnapshot;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.restoreAuthorizationCheckpoint(previousAuthorization);
|
|
67
|
-
for (const [ref, locators] of injected.documents) {
|
|
68
|
-
for (const locator of locators ?? [null])
|
|
69
|
-
authorizeRead(this.#reads, this.#candidate.snapshot.id, ref, locator);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
59
|
+
/**
|
|
60
|
+
* Read authorizations live only in memory (ADR-0044). A fresh binding starts
|
|
61
|
+
* from the bootstrap's own reads; writes are guarded at commit time by the
|
|
62
|
+
* previous-content hash of each changed file.
|
|
63
|
+
*/
|
|
64
|
+
bindBootstrap(bootstrap) {
|
|
65
|
+
this.#reads = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
|
|
72
66
|
// Creation capabilities come from this prefix, not past directory slots.
|
|
73
67
|
this.#declaredDirectories = declaredStateDirectories(bootstrap);
|
|
74
68
|
}
|
|
75
|
-
authorizationCheckpoint() {
|
|
76
|
-
return {
|
|
77
|
-
schemaVersion: 1,
|
|
78
|
-
kind: "play_document_authorizations",
|
|
79
|
-
stateFingerprint: fingerprintStateFiles(this.#candidate.files),
|
|
80
|
-
documents: [...this.#reads.documents.entries()]
|
|
81
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
82
|
-
.map(([shortRef, locators]) => ({
|
|
83
|
-
shortRef,
|
|
84
|
-
locators: locators === null
|
|
85
|
-
? null
|
|
86
|
-
: locators
|
|
87
|
-
.filter((locator, index, all) => all.findIndex((candidate) => sameLocator(candidate, locator)) === index)
|
|
88
|
-
.map((locator) => structuredClone(locator)),
|
|
89
|
-
})),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
restoreAuthorizationCheckpoint(checkpoint) {
|
|
93
|
-
if (!isPlayDocumentAuthorizationCheckpoint(checkpoint))
|
|
94
|
-
throw new TypeError("The play-document authorization checkpoint has an invalid format");
|
|
95
|
-
if (checkpoint.stateFingerprint !==
|
|
96
|
-
fingerprintStateFiles(this.#candidate.files))
|
|
97
|
-
throw new TypeError("The play-document authorization checkpoint does not match the current state");
|
|
98
|
-
const documents = new Map();
|
|
99
|
-
for (const { shortRef, locators } of checkpoint.documents) {
|
|
100
|
-
if (documentDescriptorByRef(this.#candidate.snapshot, shortRef) === null)
|
|
101
|
-
throw new TypeError(`The play-document authorization checkpoint refers to missing @${shortRef}`);
|
|
102
|
-
const restoredLocators = locators?.map((locator) => {
|
|
103
|
-
const selected = this.#candidate.snapshot.query({
|
|
104
|
-
kind: "select_node",
|
|
105
|
-
document: { shortRef },
|
|
106
|
-
locator,
|
|
107
|
-
});
|
|
108
|
-
if (selected.kind !== "select_node" || !selected.ok)
|
|
109
|
-
throw new TypeError(`The play-document authorization checkpoint refers to a missing node in @${shortRef}`);
|
|
110
|
-
return structuredClone(selected.node.locator);
|
|
111
|
-
});
|
|
112
|
-
documents.set(shortRef, restoredLocators ?? null);
|
|
113
|
-
}
|
|
114
|
-
this.#reads = {
|
|
115
|
-
snapshotId: this.#candidate.snapshot.id,
|
|
116
|
-
documents,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
69
|
execute(call, history, options = {}) {
|
|
120
70
|
let historyQuery;
|
|
121
71
|
const query = () => (historyQuery ??= new HistoryQuery(options.historyScope ?? "local-document-context", historyInputs(history)));
|
|
@@ -254,15 +204,6 @@ function authorizeToolRead(authorizations, snapshot, result) {
|
|
|
254
204
|
return;
|
|
255
205
|
authorizeRead(authorizations, snapshot.id, authorization.shortRef, authorization.locator);
|
|
256
206
|
}
|
|
257
|
-
function sameLocator(left, right) {
|
|
258
|
-
if (left === null || right === null)
|
|
259
|
-
return left === right;
|
|
260
|
-
const leftPath = "yaml" in left ? left.yaml : left.markdown;
|
|
261
|
-
const rightPath = "yaml" in right ? right.yaml : right.markdown;
|
|
262
|
-
return ("yaml" in left === "yaml" in right &&
|
|
263
|
-
leftPath.length === rightPath.length &&
|
|
264
|
-
leftPath.every((segment, index) => segment === rightPath[index]));
|
|
265
|
-
}
|
|
266
207
|
function parseYamlHandleSegment(segment) {
|
|
267
208
|
return /^(?:0|[1-9][0-9]*)$/u.test(segment) ? Number(segment) : segment;
|
|
268
209
|
}
|
|
@@ -914,64 +855,6 @@ export function fingerprintControl(files) {
|
|
|
914
855
|
hash.update(path).update("\0").update(contents).update("\0");
|
|
915
856
|
return `sha256:${hash.digest("hex")}`;
|
|
916
857
|
}
|
|
917
|
-
export function isPlayDocumentAuthorizationCheckpoint(value) {
|
|
918
|
-
if (!record(value) ||
|
|
919
|
-
!hasExactKeys(value, [
|
|
920
|
-
"schemaVersion",
|
|
921
|
-
"kind",
|
|
922
|
-
"stateFingerprint",
|
|
923
|
-
"documents",
|
|
924
|
-
]) ||
|
|
925
|
-
value.schemaVersion !== 1 ||
|
|
926
|
-
value.kind !== "play_document_authorizations" ||
|
|
927
|
-
typeof value.stateFingerprint !== "string" ||
|
|
928
|
-
!/^sha256:[0-9a-f]{64}$/u.test(value.stateFingerprint) ||
|
|
929
|
-
!Array.isArray(value.documents) ||
|
|
930
|
-
value.documents.length > 100_000)
|
|
931
|
-
return false;
|
|
932
|
-
const refs = new Set();
|
|
933
|
-
for (const document of value.documents) {
|
|
934
|
-
if (!record(document) ||
|
|
935
|
-
!hasExactKeys(document, ["shortRef", "locators"]) ||
|
|
936
|
-
typeof document.shortRef !== "string" ||
|
|
937
|
-
!/^[a-z][a-z0-9-]*$/u.test(document.shortRef) ||
|
|
938
|
-
refs.has(document.shortRef) ||
|
|
939
|
-
(document.locators !== null &&
|
|
940
|
-
(!Array.isArray(document.locators) ||
|
|
941
|
-
document.locators.length > 100_000 ||
|
|
942
|
-
!document.locators.every(isPersistedAuthorizedLocator))))
|
|
943
|
-
return false;
|
|
944
|
-
refs.add(document.shortRef);
|
|
945
|
-
}
|
|
946
|
-
return true;
|
|
947
|
-
}
|
|
948
|
-
function fingerprintStateFiles(files) {
|
|
949
|
-
const hash = createHash("sha256");
|
|
950
|
-
for (const [path, contents] of Object.entries(files)
|
|
951
|
-
.filter(([path]) => path.startsWith("state/"))
|
|
952
|
-
.sort(([left], [right]) => left.localeCompare(right)))
|
|
953
|
-
hash.update(path).update("\0").update(contents).update("\0");
|
|
954
|
-
return `sha256:${hash.digest("hex")}`;
|
|
955
|
-
}
|
|
956
|
-
function isPersistedAuthorizedLocator(value) {
|
|
957
|
-
if (!record(value))
|
|
958
|
-
return false;
|
|
959
|
-
if (hasExactKeys(value, ["yaml"]))
|
|
960
|
-
return (Array.isArray(value.yaml) &&
|
|
961
|
-
value.yaml.length <= 64 &&
|
|
962
|
-
value.yaml.every((segment) => typeof segment === "string" ||
|
|
963
|
-
(Number.isSafeInteger(segment) && Number(segment) >= 0)));
|
|
964
|
-
if (hasExactKeys(value, ["markdown"]))
|
|
965
|
-
return (Array.isArray(value.markdown) &&
|
|
966
|
-
value.markdown.length <= 64 &&
|
|
967
|
-
value.markdown.every((segment) => typeof segment === "string"));
|
|
968
|
-
return false;
|
|
969
|
-
}
|
|
970
|
-
function hasExactKeys(value, expected) {
|
|
971
|
-
const keys = Object.keys(value);
|
|
972
|
-
return (keys.length === expected.length &&
|
|
973
|
-
expected.every((key) => Object.hasOwn(value, key)));
|
|
974
|
-
}
|
|
975
858
|
function record(value) {
|
|
976
859
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
977
860
|
}
|
|
@@ -59,8 +59,8 @@ export async function runPlayFollowupRequests(input) {
|
|
|
59
59
|
failure ??= outcome.failure;
|
|
60
60
|
}
|
|
61
61
|
const extension = failure === undefined
|
|
62
|
-
? await input.artifacts.completeExtension(input.context.operationId, completed)
|
|
63
|
-
: await input.artifacts.failExtension(input.context.operationId, "recovery_required", failure, completed);
|
|
62
|
+
? await input.artifacts.completeExtension(input.context.worldId, input.context.operationId, completed)
|
|
63
|
+
: await input.artifacts.failExtension(input.context.worldId, input.context.operationId, "recovery_required", failure, completed);
|
|
64
64
|
return { outcomes, extension };
|
|
65
65
|
}
|
|
66
66
|
async function runOne(input, followup, prefix, requestSignal) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { captureNarrativeOrigin } from "../history/HistoryBackground.js";
|
|
2
|
-
import { FileNativePlayDocuments
|
|
2
|
+
import { FileNativePlayDocuments } from "./PlayDocumentTools.js";
|
|
3
3
|
import { currentPlayPrompt, playPromptRunsThroughEvents, } from "./PlayPromptRun.js";
|
|
4
4
|
import { isPlayerRoundMarker, playerInputAppend } from "./PlayContinuity.js";
|
|
5
5
|
import { PlayCallChainError } from "./PlayCallChainError.js";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Split a context at a selected player message for an edit: the prefix before
|
|
8
|
+
* it is kept, and the replacement is appended once the world has rewound and
|
|
9
|
+
* committed it (PlayCallChain.revisePlayer).
|
|
9
10
|
*/
|
|
10
11
|
export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
11
12
|
const prefix = structuredClone(source.events.slice(0, eventIndex));
|
|
12
13
|
const transcript = transcriptThroughEvents(source.transcript, prefix);
|
|
13
14
|
const player = source.events[eventIndex];
|
|
14
15
|
return {
|
|
15
|
-
// This append precedes the operation's materialization check, as before.
|
|
16
16
|
withReplacement(replacement) {
|
|
17
17
|
const events = [
|
|
18
18
|
...prefix,
|
|
@@ -46,7 +46,7 @@ export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
|
46
46
|
pagePrefixAtEndpoint(endpoint) {
|
|
47
47
|
if (prefix.length === 0)
|
|
48
48
|
return null;
|
|
49
|
-
//
|
|
49
|
+
// A fresh context keeps the page before the edit as a detached prefix.
|
|
50
50
|
const prefixTranscript = transcriptThroughEvents(source.transcript, prefix);
|
|
51
51
|
return assembleRetainedTrace(source, endpoint, {
|
|
52
52
|
events: prefix,
|
|
@@ -119,7 +119,6 @@ function assembleRetainedTrace(source, endpoint, selection) {
|
|
|
119
119
|
completedTools: source.completedTools
|
|
120
120
|
.filter(({ key }) => completedKeys === null || completedKeys.has(key))
|
|
121
121
|
.map((item) => structuredClone(item)),
|
|
122
|
-
documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(source, selection.events, documents.authorizationCheckpoint()),
|
|
123
122
|
changedDocuments: changedDocumentsAtHead(source.changedDocuments, endpoint.baseline.state, endpoint.selected.state),
|
|
124
123
|
nextMaterials: structuredClone(selection.materials),
|
|
125
124
|
nextEventId: Math.max(0, ...selection.events.map(({ id }) => id)) + 1,
|
|
@@ -132,29 +131,16 @@ function assembleRetainedTrace(source, endpoint, selection) {
|
|
|
132
131
|
export function restorePlayDocuments(files, context, events) {
|
|
133
132
|
const documents = new FileNativePlayDocuments(files);
|
|
134
133
|
try {
|
|
135
|
-
|
|
134
|
+
// Read authorizations are memory-only: a restored prefix always starts from
|
|
135
|
+
// its bootstrap, and stale write authority is caught by the commit's hash.
|
|
136
136
|
const runs = playPromptRunsThroughEvents(context, events);
|
|
137
|
-
|
|
138
|
-
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
139
|
-
if (checkpoint !== undefined)
|
|
140
|
-
documents.restoreAuthorizationCheckpoint(checkpoint.authorization);
|
|
137
|
+
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
141
138
|
}
|
|
142
139
|
catch (error) {
|
|
143
140
|
throw new PlayCallChainError(`Call-chain document authorization could not be restored: ${error instanceof Error ? error.message : "invalid checkpoint"}。`);
|
|
144
141
|
}
|
|
145
142
|
return documents;
|
|
146
143
|
}
|
|
147
|
-
function documentAuthorizationThroughEvents(context, events) {
|
|
148
|
-
const checkpoints = context.documentAuthorizationCheckpoints;
|
|
149
|
-
if (checkpoints === undefined || checkpoints.length === 0)
|
|
150
|
-
return undefined;
|
|
151
|
-
const selectedEventIds = new Set(events.map(({ id }) => id));
|
|
152
|
-
const selected = checkpoints.findLast(({ afterEventId }) => afterEventId === 0 || selectedEventIds.has(afterEventId));
|
|
153
|
-
// Legacy V1 records had no durable dynamic authorization. If the selected
|
|
154
|
-
// prefix predates their first lazily-written checkpoint, retain the exact
|
|
155
|
-
// old recovery behavior and rebuild only bootstrap authorization.
|
|
156
|
-
return selected === undefined ? undefined : structuredClone(selected);
|
|
157
|
-
}
|
|
158
144
|
export function independentContextCopy(source) {
|
|
159
145
|
const context = structuredClone(source);
|
|
160
146
|
context.continuityContextId ??= source.chainId;
|
|
@@ -162,20 +148,6 @@ export function independentContextCopy(source) {
|
|
|
162
148
|
delete context.branchedBeforePlayer;
|
|
163
149
|
return context;
|
|
164
150
|
}
|
|
165
|
-
function authorizationCheckpointsThroughEvents(context, events, fallback) {
|
|
166
|
-
const eventIds = new Set(events.map(({ id }) => id));
|
|
167
|
-
const selected = (context.documentAuthorizationCheckpoints ?? [])
|
|
168
|
-
.filter(({ afterEventId }) => afterEventId === 0 || eventIds.has(afterEventId))
|
|
169
|
-
.map((checkpoint) => structuredClone(checkpoint));
|
|
170
|
-
if (selected.length > 0)
|
|
171
|
-
return selected;
|
|
172
|
-
return [
|
|
173
|
-
{
|
|
174
|
-
afterEventId: Math.max(0, ...events.map(({ id }) => id)),
|
|
175
|
-
authorization: structuredClone(fallback),
|
|
176
|
-
},
|
|
177
|
-
];
|
|
178
|
-
}
|
|
179
151
|
function transcriptThroughEvents(transcript, events) {
|
|
180
152
|
const result = [];
|
|
181
153
|
let cursor = 0;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { validPromptCompilation, validLogicalMessages, validPromptTools, validPlayFollowup, validProviderKind, validCacheStrategy, validToolStrategy, } from "../prompt/PromptCompilationCodec.js";
|
|
2
2
|
import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
5
|
+
import { linkExclusive } from "../FileNativeExclusiveLink.js";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { isDeepStrictEqual } from "node:util";
|
|
7
8
|
import { fictionGameplay } from "../../protocol/gameplay.js";
|
|
@@ -969,13 +970,11 @@ async function publishJson(path, value) {
|
|
|
969
970
|
const handle = await open(temporary, "wx", 0o600);
|
|
970
971
|
try {
|
|
971
972
|
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
972
|
-
await handle.sync();
|
|
973
973
|
}
|
|
974
974
|
finally {
|
|
975
975
|
await handle.close();
|
|
976
976
|
}
|
|
977
977
|
await rename(temporary, path);
|
|
978
|
-
await syncDirectory(dirname(path));
|
|
979
978
|
}
|
|
980
979
|
finally {
|
|
981
980
|
await rm(temporary, { force: true });
|
|
@@ -988,14 +987,12 @@ async function publishImmutableSource(path, contents) {
|
|
|
988
987
|
const handle = await open(temporary, "wx", 0o600);
|
|
989
988
|
try {
|
|
990
989
|
await handle.writeFile(contents, "utf8");
|
|
991
|
-
await handle.sync();
|
|
992
990
|
}
|
|
993
991
|
finally {
|
|
994
992
|
await handle.close();
|
|
995
993
|
}
|
|
996
994
|
try {
|
|
997
|
-
await
|
|
998
|
-
await syncDirectory(dirname(path));
|
|
995
|
+
await linkExclusive(temporary, path);
|
|
999
996
|
}
|
|
1000
997
|
catch (error) {
|
|
1001
998
|
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
@@ -1008,15 +1005,6 @@ async function publishImmutableSource(path, contents) {
|
|
|
1008
1005
|
await rm(temporary, { force: true });
|
|
1009
1006
|
}
|
|
1010
1007
|
}
|
|
1011
|
-
async function syncDirectory(path) {
|
|
1012
|
-
const handle = await open(path, "r");
|
|
1013
|
-
try {
|
|
1014
|
-
await handle.sync();
|
|
1015
|
-
}
|
|
1016
|
-
finally {
|
|
1017
|
-
await handle.close();
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
1008
|
function isRecord(value) {
|
|
1021
1009
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1022
1010
|
}
|