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.
Files changed (62) hide show
  1. package/README.md +13 -0
  2. package/README.zh-CN.md +10 -0
  3. package/dist/node/protocol/gameplay.js +69 -0
  4. package/dist/node/protocol/resolution.js +274 -0
  5. package/dist/node/protocol/v1.js +15 -2
  6. package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
  7. package/dist/node/runtime/RuntimeErrorLog.js +95 -0
  8. package/dist/node/runtime/V1Runtime.js +58 -7
  9. package/dist/node/runtime/archive/WorldArchives.js +14 -46
  10. package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
  11. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
  12. package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
  14. package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
  15. package/dist/node/runtime/config/dataRootLock.js +108 -0
  16. package/dist/node/runtime/content/ContentWorkspace.js +10 -12
  17. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +50 -46
  18. package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
  21. package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
  22. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
  23. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +31 -170
  24. package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
  25. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +36 -239
  26. package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
  27. package/dist/node/runtime/play/PlayCallChain.js +359 -359
  28. package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
  29. package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
  30. package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
  31. package/dist/node/runtime/play/TabletopResolution.js +372 -0
  32. package/dist/node/runtime/play/TabletopRules.js +141 -0
  33. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -10
  34. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +35 -2
  35. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
  36. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
  37. package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
  38. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
  39. package/dist/node/runtime/world/FileNativeWorldStore.js +266 -783
  40. package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
  41. package/dist/node/runtime/world/WorldActivity.js +14 -22
  42. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
  43. package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
  44. package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
  45. package/dist/node/runtime/world/WorldQueue.js +46 -0
  46. package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
  47. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -12
  48. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
  49. package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
  50. package/dist/node/server/ConversationStream.js +2 -0
  51. package/dist/node/server/mobile.js +20 -0
  52. package/dist/node/server/startWebServer.js +15 -8
  53. package/dist/node/shared/ordered-author-prompts.js +8 -0
  54. package/dist/node/shared/ordered-play-prompts.js +23 -0
  55. package/dist/node/shared/tabletop-guidance.js +100 -0
  56. package/dist/web/assets/index-7GzLSUpU.js +1323 -0
  57. package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
  58. package/dist/web/index.html +2 -2
  59. package/package.json +2 -1
  60. package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
  61. package/dist/web/assets/index-DxNtxzLR.js +0 -1255
  62. package/dist/web/assets/index-DzOnu9uv.css +0 -1
@@ -27,6 +27,7 @@ import { SettingImprovementRollbackError, SettingImprovementSession, } from "./s
27
27
  import { FileNativeWorldStore } from "./world/FileNativeWorldStore.js";
28
28
  import { WorldDocumentStore } from "./world/WorldDocumentStore.js";
29
29
  import { FileNativeAiFailureLog } from "./model/AiFailureLog.js";
30
+ import { FileNativeRuntimeErrorLog } from "./RuntimeErrorLog.js";
30
31
  import { ProtagonistTemplateStore } from "./config/ProtagonistTemplateStore.js";
31
32
  import { AppPreferencesStore } from "./config/AppPreferencesStore.js";
32
33
  import { FileNativeWorldRevisionStore } from "./world-revision/FileNativeWorldRevisionStore.js";
@@ -44,6 +45,7 @@ export class V1Runtime {
44
45
  #corrections;
45
46
  #playCallChains;
46
47
  #failureLog;
48
+ #errorLog;
47
49
  #preferences;
48
50
  #protagonistTemplates;
49
51
  #settingImprovements;
@@ -65,6 +67,10 @@ export class V1Runtime {
65
67
  input.logRoot === undefined
66
68
  ? undefined
67
69
  : new FileNativeAiFailureLog(join(input.logRoot, "ai-failures"));
70
+ this.#errorLog =
71
+ input.logRoot === undefined
72
+ ? undefined
73
+ : new FileNativeRuntimeErrorLog(join(input.logRoot, "runtime-errors"));
68
74
  this.#playCallChains = new PlayCallChain(this.#worlds, this.#compiler, this.#artifacts, this.#failureLog, (id) => this.#conversationChanges.publish(`play:${id}`));
69
75
  this.worldArchives = new WorldArchives({
70
76
  dataRoot: input.dataRoot,
@@ -150,9 +156,11 @@ export class V1Runtime {
150
156
  async handle(request, playCallChainObserver) {
151
157
  try {
152
158
  const result = await this.#dispatch(request, playCallChainObserver);
159
+ await this.#recordPlayInterruption(request, result);
153
160
  return { protocol: v1Protocol, result };
154
161
  }
155
162
  catch (error) {
163
+ await this.#errorLog?.recordRequestFailure(request, error);
156
164
  if (error instanceof WorldArchiveError)
157
165
  throw new V1ProtocolError(error.code, error.message, { cause: error });
158
166
  if (request.type === "world.archive.export" &&
@@ -176,6 +184,32 @@ export class V1Runtime {
176
184
  }
177
185
  }
178
186
  }
187
+ async #recordPlayInterruption(request, result) {
188
+ if (this.#errorLog === undefined ||
189
+ (request.type !== "play.chain.start" &&
190
+ request.type !== "play.chain.append"))
191
+ return;
192
+ const chain = result;
193
+ if (chain?.status !== "interrupted" ||
194
+ typeof chain.lastFailure !== "string" ||
195
+ chain.events?.at(-1)?.kind === "cancellation")
196
+ return;
197
+ await this.#errorLog.recordPlayInterruption(request, {
198
+ message: chain.lastFailure,
199
+ ...(chain.lastFailureKind === undefined
200
+ ? {}
201
+ : { kind: chain.lastFailureKind }),
202
+ });
203
+ }
204
+ async recordConversationFailure(target, error) {
205
+ await this.#errorLog?.recordRequestFailure({
206
+ type: `${target.kind}.observe`,
207
+ targetId: target.id,
208
+ ...("sessionId" in target && target.sessionId !== undefined
209
+ ? { sessionId: target.sessionId }
210
+ : {}),
211
+ }, error);
212
+ }
179
213
  subscribeConversation(target, changed) {
180
214
  return this.#conversationChanges.subscribe(`${target.kind}:${target.id}`, changed);
181
215
  }
@@ -258,7 +292,19 @@ export class V1Runtime {
258
292
  case "model.models":
259
293
  return this.#models.listModels(request);
260
294
  case "content.create":
261
- return this.#content.createCurrentTreeContentPackage();
295
+ return this.#content.createCurrentTreeContentPackage(request.gameplay);
296
+ case "content.gameplay.set": {
297
+ try {
298
+ return await this.#content.setCurrentTreeContentGameplayScheme(request.packageId, request.scheme);
299
+ }
300
+ catch (error) {
301
+ if (error instanceof InvalidContentTreeError)
302
+ throw new V1ProtocolError("invalid_request", error.message, {
303
+ cause: error,
304
+ });
305
+ throw error;
306
+ }
307
+ }
262
308
  case "content.create-sample":
263
309
  return this.#content.createSampleContentPackage(request.locale);
264
310
  case "content.scripts.read":
@@ -272,7 +318,16 @@ export class V1Runtime {
272
318
  case "content.read":
273
319
  return this.#content.readCurrentTreeContentPackage(request.packageId);
274
320
  case "content.replace":
275
- return this.#content.replaceCurrentTreeContentPackage(request.packageId, request.files);
321
+ try {
322
+ return await this.#content.replaceCurrentTreeContentPackage(request.packageId, request.files);
323
+ }
324
+ catch (error) {
325
+ if (error instanceof InvalidContentTreeError)
326
+ throw new V1ProtocolError("invalid_request", error.message, {
327
+ cause: error,
328
+ });
329
+ throw error;
330
+ }
276
331
  case "content.copy":
277
332
  return this.#content.copyCurrentTreeContentPackage(request.packageId);
278
333
  case "content.delete":
@@ -441,6 +496,7 @@ export class V1Runtime {
441
496
  case "world.delete":
442
497
  return playCall(async () => {
443
498
  await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
499
+ await this.#playCallChains.abortWorld(request.worldId);
444
500
  this.#playCallChains.forgetWorld(request.worldId);
445
501
  return this.#worlds.deleteWorld(request.worldId);
446
502
  });
@@ -521,11 +577,6 @@ export class V1Runtime {
521
577
  case "artifacts.debug":
522
578
  await this.#reconcileArtifacts(request.worldId);
523
579
  return this.#frontendDebug(request.worldId, request.operationId);
524
- case "world.repair-materialization": {
525
- const result = await this.#worlds.repairMaterialization(request.worldId);
526
- await this.#reconcileArtifacts(request.worldId);
527
- return result;
528
- }
529
580
  case "world.control-draft.save":
530
581
  await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
531
582
  return this.#worlds.saveControlDraft(request.worldId, request.files);
@@ -1,12 +1,13 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
- import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
3
+ import { mkdir, readFile, writeFile, readdir, rename, rm, stat, } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { worldArchiveFormat, worldArchiveLimits, } from "../../protocol/worldArchives.js";
6
6
  import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
7
7
  import { PlayCallChain } from "../play/PlayCallChain.js";
8
8
  import { FileNativePromptCompiler } from "../prompt/FileNativePromptCompiler.js";
9
- import { FileNativeAuthorityV3 } from "../world/FileNativeAuthorityV3.js";
9
+ import { WorldLedgerAuthority } from "../world/WorldLedgerAuthority.js";
10
+ import { forgetLedger } from "../world/WorldTurnLedger.js";
10
11
  import { FileNativeWorldStore, } from "../world/FileNativeWorldStore.js";
11
12
  import { archivePath, extractArchive, inventory, streamFile, writeArchive, WorldArchiveError, } from "./WorldArchiveZip.js";
12
13
  export class WorldArchives {
@@ -43,8 +44,6 @@ export class WorldArchives {
43
44
  const path = this.#uploadPath(uploadId);
44
45
  try {
45
46
  await streamFile(source, path, this.#limits.archiveBytes);
46
- await syncFile(path);
47
- crashAt("upload");
48
47
  return { uploadId };
49
48
  }
50
49
  catch (error) {
@@ -69,7 +68,6 @@ export class WorldArchives {
69
68
  targetName: source.title,
70
69
  publish: false,
71
70
  targetWorldRoot: join(root, "world"),
72
- requireMaterialized: true,
73
71
  resetPackageScriptPermissions: true,
74
72
  retainCompleteTimeline: true,
75
73
  });
@@ -84,7 +82,7 @@ export class WorldArchives {
84
82
  exportedAt: new Date().toISOString(),
85
83
  worldName: source.title,
86
84
  archivedWorldId: fork.world.worldId,
87
- head: (await new FileNativeAuthorityV3(fork.worldRoot).readHead())
85
+ head: (await new WorldLedgerAuthority(fork.worldRoot).readHead())
88
86
  .head,
89
87
  files,
90
88
  };
@@ -152,7 +150,6 @@ export class WorldArchives {
152
150
  published.worldId !== worldId)
153
151
  throw new WorldArchiveError("integrity_failed", "Import publication does not match its operation");
154
152
  await this.#worlds.ensureCurrentStorage(worldId);
155
- await durableJson(join(this.#dataRoot, "operations", `import-${createHash("sha256").update(input.operationId).digest("hex")}.json`), JSON.parse(await readFile(join(targetRoot, "publication.json"), "utf8")));
156
153
  const world = await this.#worlds.readWorldSummary(worldId);
157
154
  return { outcome: "imported", world };
158
155
  }
@@ -166,7 +163,6 @@ export class WorldArchives {
166
163
  });
167
164
  const extracted = join(quarantine, "extracted");
168
165
  await mkdir(extracted, { recursive: true, mode: 0o700 });
169
- crashAt("quarantine");
170
166
  let actual;
171
167
  try {
172
168
  actual = await extractArchive(upload, extracted, this.#limits);
@@ -176,7 +172,6 @@ export class WorldArchives {
176
172
  throw new WorldArchiveError("invalid_archive", "Upload is missing; upload the ZIP again");
177
173
  throw error;
178
174
  }
179
- crashAt("unpack");
180
175
  if (!actual.has("narraeon-world.json"))
181
176
  throw new WorldArchiveError("invalid_archive", "Archive manifest is missing");
182
177
  let value;
@@ -196,6 +191,7 @@ export class WorldArchives {
196
191
  }
197
192
  const sourceRoot = join(quarantine, "worlds-file-native", manifest.archivedWorldId);
198
193
  await mkdir(dirname(sourceRoot), { recursive: true });
194
+ forgetLedger(sourceRoot);
199
195
  try {
200
196
  await rename(join(extracted, "world"), sourceRoot);
201
197
  }
@@ -214,18 +210,16 @@ export class WorldArchives {
214
210
  if (publication?.worldId !== manifest.archivedWorldId)
215
211
  throw new Error("Archive world identity mismatch");
216
212
  await sourceWorlds.ensureCurrentStorage(manifest.archivedWorldId);
217
- const authority = new FileNativeAuthorityV3(sourceRoot);
218
- const history = await authority.readHistory();
219
- if (history.head !== manifest.head)
213
+ // 结构校验:端点与清单一致,账本能倒推回 genesis;投影在分叉时重建。
214
+ const authority = new WorldLedgerAuthority(sourceRoot);
215
+ const head = await authority.readHead();
216
+ if (head.head !== manifest.head)
220
217
  throw new Error("Archive head mismatch");
221
- await authority.recover("genesis");
222
- for (const commit of history.commits)
223
- await authority.recover(commit.head);
218
+ await authority.verifyStructure();
224
219
  }
225
220
  catch (error) {
226
221
  throw new WorldArchiveError("integrity_failed", "Archive Authority failed its complete audit", { cause: error });
227
222
  }
228
- crashAt("audit");
229
223
  const sourceChains = new PlayCallChain(sourceWorlds, new FileNativePromptCompiler(), new FileNativeArtifactStore(quarantine));
230
224
  let result;
231
225
  try {
@@ -244,7 +238,6 @@ export class WorldArchives {
244
238
  catch (error) {
245
239
  throw new WorldArchiveError("integrity_failed", "Archive world closure cannot be restored", { cause: error });
246
240
  }
247
- crashAt("publish");
248
241
  return { outcome: "imported", world: result.world };
249
242
  }
250
243
  finally {
@@ -335,34 +328,14 @@ function archiveBaseName(name) {
335
328
  .replaceAll(/^[.-]+|[.-]+$/gu, "")
336
329
  .slice(0, 80) || "world");
337
330
  }
338
- async function syncFile(path) {
339
- const file = await open(path, "r");
340
- try {
341
- await file.sync();
342
- }
343
- finally {
344
- await file.close();
345
- }
346
- }
347
331
  async function durableJson(path, value) {
348
332
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
349
333
  const temporary = `${path}.${randomUUID()}.tmp`;
350
- const file = await open(temporary, "wx", 0o600);
351
- try {
352
- await file.writeFile(JSON.stringify(value));
353
- await file.sync();
354
- }
355
- finally {
356
- await file.close();
357
- }
334
+ await writeFile(temporary, JSON.stringify(value), {
335
+ flag: "wx",
336
+ mode: 0o600,
337
+ });
358
338
  await rename(temporary, path);
359
- const directory = await open(dirname(path), "r");
360
- try {
361
- await directory.sync();
362
- }
363
- finally {
364
- await directory.close();
365
- }
366
339
  }
367
340
  async function appVersion() {
368
341
  let directory = import.meta.dirname;
@@ -382,8 +355,3 @@ async function appVersion() {
382
355
  directory = parent;
383
356
  }
384
357
  }
385
- function crashAt(edge) {
386
- if (process.env.NODE_ENV === "test" &&
387
- process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_WORLD_ARCHIVE_EDGE === edge)
388
- process.exit(89);
389
- }
@@ -0,0 +1,330 @@
1
+ import { isWorldExtensionControl } from "../../protocol/worldExtensions.js";
2
+ import { isFrozenArtifactPresentation } from "../play/FileNativePlayPresetStore.js";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { mkdir, open, rename, rm } from "node:fs/promises";
5
+ import { dirname } from "node:path";
6
+ export class ArtifactStoreInvariantError extends Error {
7
+ constructor(message, options) {
8
+ super(message, options);
9
+ this.name = "ArtifactStoreInvariantError";
10
+ }
11
+ }
12
+ export class ArtifactStoreCorruptionError extends ArtifactStoreInvariantError {
13
+ constructor(message, options) {
14
+ super(message, options);
15
+ this.name = "ArtifactStoreCorruptionError";
16
+ }
17
+ }
18
+ export function artifactSequenceFileName(sequence) {
19
+ return `${String(sequence).padStart(12, "0")}.json`;
20
+ }
21
+ export function identityHash(value) {
22
+ return createHash("sha256").update(value, "utf8").digest("hex");
23
+ }
24
+ export function rawRecordFingerprint(record) {
25
+ const content = { ...record };
26
+ delete content.recordFingerprint;
27
+ return `sha256:${createHash("sha256").update(JSON.stringify(content)).digest("hex")}`;
28
+ }
29
+ export function payloadFingerprint(output, payload) {
30
+ return `sha256:${createHash("sha256").update(JSON.stringify({ output, payload })).digest("hex")}`;
31
+ }
32
+ export function stableSerialize(value) {
33
+ if (value === null || typeof value !== "object")
34
+ return JSON.stringify(value);
35
+ if (Array.isArray(value))
36
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
37
+ return `{${Object.keys(value)
38
+ .sort()
39
+ .map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
40
+ .join(",")}}`;
41
+ }
42
+ /** Heads are linear: genesis is round 0 and commit:N is round N. */
43
+ export function headOrdinal(head) {
44
+ if (head === "genesis")
45
+ return 0;
46
+ const match = /^commit:([1-9][0-9]*)$/u.exec(head);
47
+ if (match === null)
48
+ throw new ArtifactStoreInvariantError("Artifact head is invalid");
49
+ return Number(match[1]);
50
+ }
51
+ /**
52
+ * Writes a JSON file by temporary file and rename. The temporary name is
53
+ * unique, so an interrupted write never leaves a partial file under `path`.
54
+ */
55
+ export async function writeArtifactJson(path, value) {
56
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
57
+ const target = `${path}.${randomUUID()}.tmp`;
58
+ let handle;
59
+ let created = false;
60
+ try {
61
+ handle = await open(target, "wx", 0o600);
62
+ created = true;
63
+ await handle.writeFile(JSON.stringify(value), "utf8");
64
+ await handle.close();
65
+ handle = undefined;
66
+ await rename(target, path);
67
+ }
68
+ catch (error) {
69
+ await handle?.close().catch(() => undefined);
70
+ if (created)
71
+ await rm(target, { force: true }).catch(() => undefined);
72
+ throw error;
73
+ }
74
+ }
75
+ export function parseArtifactJson(bytes, label) {
76
+ try {
77
+ return JSON.parse(bytes);
78
+ }
79
+ catch (error) {
80
+ throw new ArtifactStoreCorruptionError(`${label} JSON is corrupt`, error instanceof Error ? { cause: error } : undefined);
81
+ }
82
+ }
83
+ export function isRecord(value) {
84
+ return typeof value === "object" && value !== null && !Array.isArray(value);
85
+ }
86
+ export function hasExactKeys(value, required, optional = []) {
87
+ const allowed = new Set([...required, ...optional]);
88
+ return (required.every((key) => Object.hasOwn(value, key)) &&
89
+ Object.keys(value).every((key) => allowed.has(key)));
90
+ }
91
+ export function isNonEmptyString(value) {
92
+ return typeof value === "string" && value.trim() !== "";
93
+ }
94
+ export function isSha256(value) {
95
+ return typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value);
96
+ }
97
+ export function isMissing(error) {
98
+ return isRecord(error) && error.code === "ENOENT";
99
+ }
100
+ export function isJsonValue(value) {
101
+ if (value === null || typeof value === "string" || typeof value === "boolean")
102
+ return true;
103
+ if (typeof value === "number")
104
+ return Number.isFinite(value);
105
+ if (Array.isArray(value))
106
+ return value.every(isJsonValue);
107
+ if (typeof value !== "object")
108
+ return false;
109
+ return Object.values(value).every(isJsonValue);
110
+ }
111
+ export function validAttachment(value) {
112
+ return (isRecord(value) &&
113
+ hasExactKeys(value, ["contextId", "eventId", "runId", "head"]) &&
114
+ isNonEmptyString(value.head) &&
115
+ isNonEmptyString(value.contextId) &&
116
+ isNonEmptyString(value.runId) &&
117
+ typeof value.eventId === "number" &&
118
+ Number.isSafeInteger(value.eventId) &&
119
+ value.eventId > 0);
120
+ }
121
+ export function assertRawRecord(record) {
122
+ if (!isRecord(record) ||
123
+ !hasExactKeys(record, [
124
+ "schemaVersion",
125
+ "recordId",
126
+ "sequence",
127
+ "worldId",
128
+ "parentHead",
129
+ "head",
130
+ "operationId",
131
+ "playPresetId",
132
+ "playPresetRevision",
133
+ "playPresetScriptsEnabled",
134
+ "requestId",
135
+ "requestAttempt",
136
+ "output",
137
+ "channel",
138
+ "contentType",
139
+ "payload",
140
+ "save",
141
+ "projection",
142
+ "invalidation",
143
+ "toolCallId",
144
+ "toolName",
145
+ "callFingerprint",
146
+ "payloadFingerprint",
147
+ "recordFingerprint",
148
+ "status",
149
+ ], [
150
+ "key",
151
+ "renderer",
152
+ "rendererRevision",
153
+ "frozenPresentation",
154
+ "extensionControl",
155
+ "clearedBy",
156
+ ]) ||
157
+ (record.clearedBy !== undefined &&
158
+ !isClearedBy(record.clearedBy, record.status)) ||
159
+ (record.extensionControl !== undefined &&
160
+ !isWorldExtensionControl(record.extensionControl)) ||
161
+ (record.frozenPresentation !== undefined &&
162
+ !isFrozenArtifactPresentation(record.frozenPresentation)) ||
163
+ record.schemaVersion !== 1 ||
164
+ !isNonEmptyString(record.recordId) ||
165
+ !Number.isSafeInteger(record.sequence) ||
166
+ record.sequence < 1 ||
167
+ !isNonEmptyString(record.worldId) ||
168
+ !isNonEmptyString(record.operationId) ||
169
+ !isNonEmptyString(record.output) ||
170
+ !isNonEmptyString(record.channel) ||
171
+ !isNonEmptyString(record.parentHead) ||
172
+ !isNonEmptyString(record.playPresetId) ||
173
+ !isNonEmptyString(record.playPresetRevision) ||
174
+ !isNonEmptyString(record.requestId) ||
175
+ !Number.isSafeInteger(record.requestAttempt) ||
176
+ record.requestAttempt < 1 ||
177
+ (record.head !== null && !isNonEmptyString(record.head)) ||
178
+ typeof record.playPresetScriptsEnabled !== "boolean" ||
179
+ !["text/plain", "text/markdown", "application/json", "text/html"].includes(record.contentType) ||
180
+ !["none", "operation", "commit"].includes(record.save) ||
181
+ !["append", "replace", "upsert", "transient", "hidden"].includes(record.projection) ||
182
+ ![
183
+ "new_operation",
184
+ "head_change",
185
+ "operation_end",
186
+ "explicit_clear",
187
+ "never",
188
+ ].includes(record.invalidation) ||
189
+ !["pending", "active", "superseded", "cleared"].includes(record.status) ||
190
+ typeof record.toolCallId !== "string" ||
191
+ record.toolName !== "artifact_emit" ||
192
+ !isSha256(record.callFingerprint) ||
193
+ !isSha256(record.payloadFingerprint) ||
194
+ !isSha256(record.recordFingerprint) ||
195
+ (record.renderer === undefined) !==
196
+ (record.rendererRevision === undefined) ||
197
+ (record.renderer !== undefined && !isNonEmptyString(record.renderer)) ||
198
+ (record.rendererRevision !== undefined &&
199
+ !isNonEmptyString(record.rendererRevision)) ||
200
+ (record.projection === "upsert"
201
+ ? !isNonEmptyString(record.key)
202
+ : record.key !== undefined))
203
+ throw new ArtifactStoreCorruptionError("Artifact raw record is corrupt");
204
+ if (!isJsonValue(record.payload))
205
+ throw new ArtifactStoreCorruptionError("Artifact payload is not a JSON value");
206
+ if (record.payloadFingerprint !==
207
+ payloadFingerprint(record.output, record.payload))
208
+ throw new ArtifactStoreCorruptionError("Artifact payload fingerprint does not match");
209
+ if (record.recordFingerprint !== rawRecordFingerprint(record))
210
+ throw new ArtifactStoreCorruptionError("Artifact raw-record fingerprint does not match");
211
+ }
212
+ function isClearedBy(value, status) {
213
+ return (isRecord(value) &&
214
+ hasExactKeys(value, [
215
+ "operationId",
216
+ "requestId",
217
+ "requestAttempt",
218
+ "priorStatus",
219
+ ]) &&
220
+ status === "cleared" &&
221
+ isNonEmptyString(value.operationId) &&
222
+ isNonEmptyString(value.requestId) &&
223
+ Number.isSafeInteger(value.requestAttempt) &&
224
+ value.requestAttempt >= 1 &&
225
+ (value.priorStatus === "pending" || value.priorStatus === "active"));
226
+ }
227
+ export function assertOperation(operation) {
228
+ const context = operation.context;
229
+ if (!isRecord(operation) ||
230
+ !hasExactKeys(operation, [
231
+ "schemaVersion",
232
+ "context",
233
+ "status",
234
+ "extensionStatus",
235
+ "completedRequests",
236
+ "artifactProjectionStatus",
237
+ "requestAttempts",
238
+ ], ["message", "head", "requests"]) ||
239
+ (operation.requests !== undefined &&
240
+ (!Array.isArray(operation.requests) ||
241
+ !operation.requests.every((item) => isRecord(item) &&
242
+ hasExactKeys(item, [
243
+ "requestId",
244
+ "displayName",
245
+ "mounts",
246
+ "status",
247
+ ]) &&
248
+ isNonEmptyString(item.requestId) &&
249
+ isNonEmptyString(item.displayName) &&
250
+ Array.isArray(item.mounts) &&
251
+ item.mounts.every((mount) => typeof mount === "string" &&
252
+ [
253
+ "story",
254
+ "sidebar",
255
+ "composer_above",
256
+ "composer_below",
257
+ "overlay",
258
+ "debug",
259
+ ].includes(mount)) &&
260
+ ["running", "completed", "failed"].includes(item.status)))) ||
261
+ operation.schemaVersion !== 1 ||
262
+ !isRecord(context) ||
263
+ !hasExactKeys(context, [
264
+ "worldId",
265
+ "parentHead",
266
+ "operationId",
267
+ "playPresetId",
268
+ "playPresetRevision",
269
+ "playPresetScriptsEnabled",
270
+ ], ["attachment"]) ||
271
+ (context.attachment !== undefined &&
272
+ !validAttachment(context.attachment)) ||
273
+ !isNonEmptyString(context.worldId) ||
274
+ !isNonEmptyString(context.parentHead) ||
275
+ !isNonEmptyString(context.operationId) ||
276
+ !isNonEmptyString(context.playPresetId) ||
277
+ !isNonEmptyString(context.playPresetRevision) ||
278
+ typeof context.playPresetScriptsEnabled !== "boolean" ||
279
+ !["running", "core_failed", "core_committed", "completed"].includes(operation.status) ||
280
+ ![
281
+ "not_started",
282
+ "running",
283
+ "completed",
284
+ "failed",
285
+ "cancelled",
286
+ "budget_exceeded",
287
+ "unknown",
288
+ "recovery_required",
289
+ "superseded",
290
+ ].includes(operation.extensionStatus) ||
291
+ !Array.isArray(operation.completedRequests) ||
292
+ !operation.completedRequests.every(isNonEmptyString) ||
293
+ (operation.message !== undefined &&
294
+ typeof operation.message !== "string") ||
295
+ !validArtifactOperationState(operation) ||
296
+ !["active", "superseded"].includes(operation.artifactProjectionStatus) ||
297
+ !isRecord(operation.requestAttempts) ||
298
+ !Object.entries(operation.requestAttempts).every(([request, attempt]) => request.trim() !== "" &&
299
+ typeof attempt === "number" &&
300
+ Number.isSafeInteger(attempt) &&
301
+ attempt >= 1))
302
+ throw new ArtifactStoreCorruptionError("Artifact operation state is corrupt");
303
+ }
304
+ function validArtifactOperationState(operation) {
305
+ if (operation.status === "running")
306
+ return (operation.extensionStatus === "not_started" &&
307
+ operation.completedRequests.length === 0 &&
308
+ operation.message === undefined &&
309
+ operation.head === undefined);
310
+ if (operation.status === "core_failed")
311
+ return (operation.extensionStatus === "failed" &&
312
+ operation.completedRequests.length === 0 &&
313
+ operation.artifactProjectionStatus === "superseded" &&
314
+ operation.head === undefined);
315
+ if (operation.status === "completed")
316
+ return (operation.extensionStatus === "completed" &&
317
+ isNonEmptyString(operation.head) &&
318
+ operation.message === undefined);
319
+ if (!isNonEmptyString(operation.head))
320
+ return false;
321
+ if (operation.extensionStatus === "completed")
322
+ return false;
323
+ if (operation.extensionStatus === "not_started" ||
324
+ operation.extensionStatus === "running")
325
+ return (operation.completedRequests.length === 0 &&
326
+ operation.message === undefined);
327
+ return (isNonEmptyString(operation.message) &&
328
+ (operation.extensionStatus !== "superseded" ||
329
+ operation.artifactProjectionStatus === "superseded"));
330
+ }