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,14 +1,15 @@
1
- import { isNarrativeOrigin, isHistoryBackground, } from "../history/HistoryBackground.js";
2
- import { createHash, randomBytes, randomUUID } from "node:crypto";
3
- import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
1
+ import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  /**
7
- * Durable progress facts for the one play advance currently being settled.
7
+ * The single in-progress play advance of a call chain, if any.
8
8
  *
9
- * Every stage is immutable. `current.json` is only a small lookup projection;
10
- * deleting it cannot change Authority and a repeated user operation keeps the
11
- * same deterministic advance identity.
9
+ * It exists only between `begin` and `markSettled`. A marker that survives a
10
+ * restart means the advance was interrupted; the runtime marks the chain
11
+ * interrupted and the player retries. Legacy records from earlier layouts are
12
+ * ignored, so they never block a world from opening.
12
13
  */
13
14
  export class FileNativePlayAdvanceStore {
14
15
  #worldsRoot;
@@ -18,121 +19,41 @@ export class FileNativePlayAdvanceStore {
18
19
  async begin(base) {
19
20
  assertBase(base);
20
21
  const current = await this.readCurrent(base.worldId, base.chainId);
21
- if (current !== null && current.settled === null) {
22
- if (!sameAdvanceBase(current.base, base))
22
+ if (current !== null) {
23
+ if (!sameAdvanceBase(current, base))
23
24
  throw new Error("Another play advance is still awaiting settlement");
24
25
  return;
25
26
  }
26
- const basePath = this.#factPath(base, "base.json");
27
- const existing = await readOptionalJson(basePath);
28
- if (existing === null)
29
- await publishImmutableJson(basePath, base);
30
- else {
31
- assertBase(existing);
32
- if (!sameAdvanceBase(existing, base))
33
- throw new Error("A play advance identity is bound to different data");
34
- }
35
- await publishJson(this.#currentPath(base.worldId, base.chainId), {
36
- schemaVersion: 1,
37
- worldId: base.worldId,
38
- chainId: base.chainId,
39
- advanceId: base.advanceId,
40
- });
41
- }
42
- async recordProviderCompleted(base, response) {
43
- const durableBase = await this.#assertCurrent(base);
44
- await publishImmutableJson(this.#factPath(base, "provider-completed.json"), {
45
- schemaVersion: 1,
46
- stage: "provider_completed",
47
- response: structuredClone(response),
48
- recordedAt: durableBase.createdAt,
49
- });
50
- }
51
- async recordSettlementPrepared(base, settlement) {
52
- const durableBase = await this.#assertCurrent(base);
53
- await publishImmutableJson(this.#factPath(base, "settlement-prepared.json"), {
54
- schemaVersion: 1,
55
- stage: "settlement_prepared",
56
- settlement: structuredClone(settlement),
57
- recordedAt: durableBase.createdAt,
58
- });
59
- }
60
- /**
61
- * Return the advance's secret resolution seed, creating it on first need.
62
- * Recovery reads the same fact, so every tool call in the advance draws from
63
- * one persisted stream and a crashed batch cannot resample.
64
- */
65
- async resolutionSeed(base) {
66
- const durableBase = await this.#assertCurrent(base);
67
- const path = this.#factPath(base, "resolution-seed.json");
68
- if ((await readOptionalJson(path)) === null) {
69
- try {
70
- await publishImmutableJson(path, {
71
- schemaVersion: 1,
72
- stage: "resolution_seed",
73
- seed: randomBytes(32).toString("hex"),
74
- recordedAt: durableBase.createdAt,
75
- });
76
- }
77
- catch (error) {
78
- // A concurrent writer may have published first; the stored fact wins.
79
- if ((await readOptionalJson(path)) === null)
80
- throw error;
81
- }
82
- }
83
- const fact = await readJson(path);
84
- assertResolutionSeedFact(fact);
85
- return fact.seed;
86
- }
87
- async markSettled(base, head) {
88
- const durableBase = await this.#assertCurrent(base);
89
- await publishImmutableJson(this.#factPath(base, "settled.json"), {
90
- schemaVersion: 1,
91
- stage: "settled",
92
- head,
93
- recordedAt: durableBase.createdAt,
94
- });
27
+ await publishJson(this.#markerPath(base.worldId, base.chainId), base);
95
28
  }
96
29
  async readCurrent(worldId, chainId) {
97
30
  assertIdentity(worldId, "World ID");
98
31
  assertIdentity(chainId, "Call-chain ID");
99
- const pointer = await readOptionalJson(this.#currentPath(worldId, chainId));
100
- if (pointer === null)
32
+ const value = await readOptionalJson(this.#markerPath(worldId, chainId));
33
+ if (value === null)
101
34
  return null;
102
- assertPointer(pointer);
103
- if (pointer.worldId !== worldId || pointer.chainId !== chainId)
104
- throw new Error("Play advance pointer belongs to another context");
105
- const recordRoot = this.#recordRoot(worldId, chainId, pointer.advanceId);
106
- const [base, providerCompleted, settlementPrepared, settled] = await Promise.all([
107
- readJson(join(recordRoot, "base.json")),
108
- readOptionalJson(join(recordRoot, "provider-completed.json")),
109
- readOptionalJson(join(recordRoot, "settlement-prepared.json")),
110
- readOptionalJson(join(recordRoot, "settled.json")),
111
- ]);
112
- assertBase(base);
113
- if (base.worldId !== worldId ||
114
- base.chainId !== chainId ||
115
- base.advanceId !== pointer.advanceId)
116
- throw new Error("Play advance durable identity is inconsistent");
117
- assertProviderCompleted(providerCompleted);
118
- assertSettlementPrepared(settlementPrepared);
119
- assertSettled(settled);
120
- return { base, providerCompleted, settlementPrepared, settled };
35
+ assertBase(value);
36
+ if (value.worldId !== worldId || value.chainId !== chainId)
37
+ throw new Error("Play advance marker belongs to another context");
38
+ return value;
121
39
  }
122
- async #assertCurrent(base) {
40
+ async markSettled(base) {
123
41
  const current = await this.readCurrent(base.worldId, base.chainId);
124
- if (current === null || !sameAdvanceBase(current.base, base))
42
+ if (current === null)
43
+ return;
44
+ if (!sameAdvanceBase(current, base))
125
45
  throw new Error("Play advance is no longer current");
126
- return current.base;
46
+ await rm(this.#markerPath(base.worldId, base.chainId), { force: true });
127
47
  }
128
- #currentPath(worldId, chainId) {
129
- return join(this.#worldsRoot, worldId, "runtime", "play-advances", digest(chainId), "current.json");
130
- }
131
- #recordRoot(worldId, chainId, advanceId) {
132
- return join(this.#worldsRoot, worldId, "runtime", "play-advances", digest(chainId), "records", digest(advanceId));
48
+ /** Remove the in-progress marker and all advance records of a discarded chain. */
49
+ async discardChain(worldId, chainId) {
50
+ await rm(dirname(this.#markerPath(worldId, chainId)), {
51
+ recursive: true,
52
+ force: true,
53
+ });
133
54
  }
134
- #factPath(base, name) {
135
- return join(this.#recordRoot(base.worldId, base.chainId, base.advanceId), name);
55
+ #markerPath(worldId, chainId) {
56
+ return join(this.#worldsRoot, worldId, "runtime", "play-advances", digest(chainId), "in-progress.json");
136
57
  }
137
58
  }
138
59
  function sameAdvanceBase(left, right) {
@@ -142,15 +63,6 @@ function sameAdvanceBase(left, right) {
142
63
  void rightCreatedAt;
143
64
  return isDeepStrictEqual(leftIdentity, rightIdentity);
144
65
  }
145
- function assertResolutionSeedFact(value) {
146
- if (!isRecord(value) ||
147
- value.schemaVersion !== 1 ||
148
- value.stage !== "resolution_seed" ||
149
- typeof value.seed !== "string" ||
150
- !/^[a-f0-9]{64}$/.test(value.seed) ||
151
- typeof value.recordedAt !== "number")
152
- throw new Error("Invalid persisted resolution seed");
153
- }
154
66
  function assertBase(value) {
155
67
  if (!isRecord(value) ||
156
68
  value.schemaVersion !== 1 ||
@@ -188,40 +100,6 @@ function assertBase(value) {
188
100
  Number(value.attempt) < 1)
189
101
  throw new Error("Response advance has an invalid shape");
190
102
  }
191
- function assertPointer(value) {
192
- if (!isRecord(value) ||
193
- value.schemaVersion !== 1 ||
194
- typeof value.worldId !== "string" ||
195
- typeof value.chainId !== "string" ||
196
- typeof value.advanceId !== "string")
197
- throw new Error("Play advance pointer has an invalid shape");
198
- }
199
- function assertProviderCompleted(value) {
200
- if (value !== null &&
201
- (value.schemaVersion !== 1 ||
202
- value.stage !== "provider_completed" ||
203
- !isRecord(value.response) ||
204
- typeof value.recordedAt !== "number"))
205
- throw new Error("Provider-completed play fact has an invalid shape");
206
- }
207
- function assertSettlementPrepared(value) {
208
- if (value !== null &&
209
- (value.schemaVersion !== 1 ||
210
- value.stage !== "settlement_prepared" ||
211
- !isRecord(value.settlement) ||
212
- (value.settlement.background !== undefined &&
213
- !isHistoryBackground(value.settlement.background)) ||
214
- typeof value.recordedAt !== "number"))
215
- throw new Error("Settlement-prepared play fact has an invalid shape");
216
- }
217
- function assertSettled(value) {
218
- if (value !== null &&
219
- (value.schemaVersion !== 1 ||
220
- value.stage !== "settled" ||
221
- typeof value.head !== "string" ||
222
- typeof value.recordedAt !== "number"))
223
- throw new Error("Settled play fact has an invalid shape");
224
- }
225
103
  function digest(value) {
226
104
  return createHash("sha256").update(value).digest("hex");
227
105
  }
@@ -232,12 +110,9 @@ function assertIdentity(value, label) {
232
110
  function isRecord(value) {
233
111
  return typeof value === "object" && value !== null && !Array.isArray(value);
234
112
  }
235
- async function readJson(path) {
236
- return JSON.parse(await readFile(path, "utf8"));
237
- }
238
113
  async function readOptionalJson(path) {
239
114
  try {
240
- return await readJson(path);
115
+ return JSON.parse(await readFile(path, "utf8"));
241
116
  }
242
117
  catch (error) {
243
118
  if (isNodeError(error) && error.code === "ENOENT")
@@ -245,6 +120,7 @@ async function readOptionalJson(path) {
245
120
  throw error;
246
121
  }
247
122
  }
123
+ /** Atomic publish: write a temporary file, then rename over the target. */
248
124
  async function publishJson(path, value) {
249
125
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
250
126
  const temporary = `${path}.${randomUUID()}.tmp`;
@@ -252,58 +128,7 @@ async function publishJson(path, value) {
252
128
  encoding: "utf8",
253
129
  mode: 0o600,
254
130
  });
255
- await syncFile(temporary);
256
131
  await rename(temporary, path);
257
- await syncDirectory(dirname(path));
258
- }
259
- async function publishImmutableJson(path, value) {
260
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
261
- const existing = await readOptionalJson(path);
262
- if (existing !== null) {
263
- if (!isDeepStrictEqual(existing, value))
264
- throw new Error("Immutable play-advance fact already has different data");
265
- return;
266
- }
267
- const temporary = `${path}.${randomUUID()}.tmp`;
268
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
269
- encoding: "utf8",
270
- mode: 0o600,
271
- });
272
- await syncFile(temporary);
273
- try {
274
- await link(temporary, path);
275
- }
276
- catch (error) {
277
- if (!isNodeError(error) || error.code !== "EEXIST")
278
- throw error;
279
- const published = await readJson(path);
280
- if (!isDeepStrictEqual(published, value))
281
- throw new Error("Concurrent immutable play-advance fact conflicts", {
282
- cause: error,
283
- });
284
- }
285
- finally {
286
- await rm(temporary, { force: true });
287
- }
288
- await syncDirectory(dirname(path));
289
- }
290
- async function syncFile(path) {
291
- const handle = await open(path, "r");
292
- try {
293
- await handle.sync();
294
- }
295
- finally {
296
- await handle.close();
297
- }
298
- }
299
- async function syncDirectory(path) {
300
- const handle = await open(path, "r");
301
- try {
302
- await handle.sync();
303
- }
304
- finally {
305
- await handle.close();
306
- }
307
132
  }
308
133
  function isNodeError(error) {
309
134
  return error instanceof Error && "code" in error;