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.
Files changed (41) hide show
  1. package/README.md +11 -0
  2. package/README.zh-CN.md +8 -0
  3. package/dist/node/protocol/v1.js +0 -2
  4. package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
  5. package/dist/node/runtime/V1Runtime.js +1 -5
  6. package/dist/node/runtime/archive/WorldArchives.js +14 -46
  7. package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
  8. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
  9. package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
  10. package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
  11. package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
  12. package/dist/node/runtime/config/dataRootLock.js +108 -0
  13. package/dist/node/runtime/content/ContentWorkspace.js +0 -10
  14. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
  15. package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
  16. package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
  17. package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
  18. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
  20. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
  21. package/dist/node/runtime/play/PlayCallChain.js +338 -366
  22. package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
  23. package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
  24. package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
  25. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +3 -15
  26. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
  27. package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
  28. package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
  29. package/dist/node/runtime/world/WorldActivity.js +14 -22
  30. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
  31. package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
  32. package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
  33. package/dist/node/runtime/world/WorldQueue.js +46 -0
  34. package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
  35. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +0 -12
  36. package/dist/node/server/mobile.js +20 -0
  37. package/dist/node/server/startWebServer.js +15 -8
  38. package/dist/web/assets/{index-BgBL8G-A.js → index-7GzLSUpU.js} +2 -2
  39. package/dist/web/index.html +1 -1
  40. package/package.json +2 -1
  41. 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
- bindBootstrap(bootstrap, previousAuthorization) {
60
- const injected = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
61
- this.#reads = injected;
62
- // Refreshing a prefix retains the native conversation. Its exact reads
63
- // remain valid only on the identical state tree, including our own writes.
64
- if (previousAuthorization?.stateFingerprint ===
65
- fingerprintStateFiles(this.#candidate.files)) {
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, } from "./PlayDocumentTools.js";
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
- * Prepare the selected player's prefix at the existing pre-commit seam.
8
- * Endpoint reads, revision submission and publication belong to the operation.
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
- // Preserve the fresh path's post-commit prefix reconstruction seam.
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
- const checkpoint = documentAuthorizationThroughEvents(context, events);
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
- if (checkpoint === undefined || runs.length > 0)
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 { link, mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
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 link(temporary, path);
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
  }