narraeon 0.1.0

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 (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +164 -0
  3. package/README.zh-CN.md +164 -0
  4. package/dist/node/cli/NarraeonCli.js +163 -0
  5. package/dist/node/cli/main.js +38 -0
  6. package/dist/node/protocol/appPreferences.js +5 -0
  7. package/dist/node/protocol/contentTree.js +14 -0
  8. package/dist/node/protocol/modelConnections.js +110 -0
  9. package/dist/node/protocol/playerViews.js +1 -0
  10. package/dist/node/protocol/v1.js +278 -0
  11. package/dist/node/runtime/V1Runtime.js +625 -0
  12. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
  14. package/dist/node/runtime/config/appPaths.js +19 -0
  15. package/dist/node/runtime/content/ContentLimits.js +1 -0
  16. package/dist/node/runtime/content/ContentTreeFile.js +1 -0
  17. package/dist/node/runtime/content/ContentWorkspace.js +601 -0
  18. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
  19. package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
  20. package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
  21. package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
  22. package/dist/node/runtime/model/AiFailureLog.js +332 -0
  23. package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
  24. package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
  25. package/dist/node/runtime/model/ModelHost.js +89 -0
  26. package/dist/node/runtime/model/ModelHostStream.js +367 -0
  27. package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
  28. package/dist/node/runtime/model/ProviderStream.js +90 -0
  29. package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
  30. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
  31. package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
  32. package/dist/node/runtime/play/PlayCallChain.js +1280 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
  36. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
  37. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
  38. package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
  39. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
  40. package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
  41. package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
  42. package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
  43. package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
  44. package/dist/node/runtime/world/WorldStateRevision.js +75 -0
  45. package/dist/node/server/createRuntime.js +6 -0
  46. package/dist/node/server/createServer.js +218 -0
  47. package/dist/node/server/main.js +25 -0
  48. package/dist/node/server/startWebServer.js +28 -0
  49. package/dist/node/shared/default-play-prompts.js +85 -0
  50. package/dist/node/shared/default-preset-host.js +538 -0
  51. package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
  52. package/dist/node/shared/first-party-action-choices.js +164 -0
  53. package/dist/node/shared/first-party-generic-panels.js +105 -0
  54. package/dist/node/shared/first-party-play-preset-templates.js +41 -0
  55. package/dist/node/shared/first-party-player-view.js +179 -0
  56. package/dist/node/shared/play-preset-prompt-roles.js +4 -0
  57. package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
  58. package/dist/web/assets/index-DDx1itzT.js +1192 -0
  59. package/dist/web/index.html +14 -0
  60. package/package.json +86 -0
@@ -0,0 +1,1914 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { mkdir, open, readdir, readFile, readlink, rename, rm, stat, } from "node:fs/promises";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { validatePlayPresetArtifactPayload, } from "../play/FileNativePlayPresetStore.js";
8
+ export class ArtifactStoreInvariantError extends Error {
9
+ constructor(message, options) {
10
+ super(message, options);
11
+ this.name = "ArtifactStoreInvariantError";
12
+ }
13
+ }
14
+ export class ArtifactStoreCorruptionError extends ArtifactStoreInvariantError {
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.name = "ArtifactStoreCorruptionError";
18
+ }
19
+ }
20
+ const maxPayloadBytes = 4 * 1024 * 1024;
21
+ /**
22
+ * File-native artifact module. Raw records never change after publication;
23
+ * projection events and operation state are the only mutable layers. The
24
+ * public interface deliberately exposes no filesystem paths or provider
25
+ * transcript and is the seam used by the play-call-chain coordinator and tests.
26
+ */
27
+ export class FileNativeArtifactStore {
28
+ #root;
29
+ #memoryRecords = new Map();
30
+ #memoryEvents = new Map();
31
+ #mutationContext = new AsyncLocalStorage();
32
+ constructor(dataRoot) {
33
+ this.#root = join(resolve(dataRoot), "artifact-store");
34
+ }
35
+ async beginOperation(context) {
36
+ return this.#withWorldMutation(context.worldId, () => this.#beginOperation(context));
37
+ }
38
+ async #beginOperation(context) {
39
+ assertContext(context);
40
+ const existing = await this.#readOperation(context.operationId);
41
+ if (existing !== null && !sameContext(existing.context, context))
42
+ throw new ArtifactStoreInvariantError("Artifact operation is bound to a different frozen payload");
43
+ // A direct orchestrator/store caller must not rely on a later UI read to
44
+ // invalidate artifacts bound to the parent head. This call is safe on a
45
+ // retry because projection events are appended idempotently below.
46
+ await this.reconcileHead(context.worldId, context.parentHead, context.operationId);
47
+ const priorOperations = await this.#operationsForWorld(context.worldId);
48
+ if (existing === null)
49
+ await this.#writeOperation({
50
+ schemaVersion: 1,
51
+ context: normalizeOperationContext(context),
52
+ status: "running",
53
+ extensionStatus: "not_started",
54
+ completedRequests: [],
55
+ artifactProjectionStatus: "active",
56
+ requestAttempts: {},
57
+ });
58
+ // Operation state is indexed independently of raw records. This closes the
59
+ // no-artifact gap: a running/unknown extension is superseded even when it
60
+ // has not emitted anything yet. Its execution status is retained unless it
61
+ // is an actually open extension, while projection invalidation is tracked
62
+ // separately for late raw results.
63
+ const prior = await this.#effectiveRecords(context.worldId);
64
+ for (const priorOperation of priorOperations) {
65
+ if (priorOperation.context.operationId === context.operationId)
66
+ continue;
67
+ const openExtension = isOpenExtension(priorOperation);
68
+ const operationAlreadySuperseded = priorOperation.artifactProjectionStatus === "superseded";
69
+ const priorRecords = prior.filter(({ record, status }) => record.operationId === priorOperation.context.operationId &&
70
+ status === "active");
71
+ const invalidatedRecords = priorRecords.filter(({ record }) => openExtension ||
72
+ operationAlreadySuperseded ||
73
+ record.invalidation === "new_operation");
74
+ for (const item of invalidatedRecords)
75
+ await this.#appendEventOnce({
76
+ schemaVersion: 1,
77
+ kind: "supersede",
78
+ operationId: item.record.operationId,
79
+ recordId: item.record.recordId,
80
+ reason: "new_operation",
81
+ sequence: await this.#nextSequence(context.worldId),
82
+ });
83
+ if (openExtension) {
84
+ const next = {
85
+ ...priorOperation,
86
+ artifactProjectionStatus: "superseded",
87
+ extensionStatus: "superseded",
88
+ message: "This extension was superseded by a newer operation",
89
+ };
90
+ if (JSON.stringify(next) !== JSON.stringify(priorOperation))
91
+ await this.#writeOperation(next);
92
+ }
93
+ }
94
+ }
95
+ async beginRequestAttempt(context) {
96
+ return this.#withWorldMutation(context.worldId, () => this.#beginRequestAttempt(context));
97
+ }
98
+ async #beginRequestAttempt(context) {
99
+ assertRequestContext(context);
100
+ const operation = await this.#readOperation(context.operationId);
101
+ if (operation === null)
102
+ throw new ArtifactStoreInvariantError("Artifact request attempt is missing an operation checkpoint");
103
+ if (!sameContext(operation.context, context))
104
+ throw new ArtifactStoreInvariantError("Artifact request attempt does not match the frozen operation");
105
+ if (operation.status === "core_failed" ||
106
+ operation.status === "completed" ||
107
+ operation.artifactProjectionStatus === "superseded")
108
+ return;
109
+ const current = operation.requestAttempts[context.requestId];
110
+ if (current !== undefined && context.requestAttempt < current)
111
+ return;
112
+ // Persist the monotonic marker before projection events. If a process dies
113
+ // between these writes, cold rebuild still treats the old attempt as
114
+ // stale, and a retry below completes the missing supersede events.
115
+ if (current === undefined || context.requestAttempt > current) {
116
+ await this.#writeOperation({
117
+ ...operation,
118
+ requestAttempts: {
119
+ ...operation.requestAttempts,
120
+ [context.requestId]: context.requestAttempt,
121
+ },
122
+ });
123
+ }
124
+ const records = await this.#recordsForOperation(context.operationId);
125
+ for (const record of records) {
126
+ if (record.requestId !== context.requestId ||
127
+ record.requestAttempt >= context.requestAttempt)
128
+ continue;
129
+ const event = {
130
+ schemaVersion: 1,
131
+ kind: "supersede",
132
+ operationId: context.operationId,
133
+ recordId: record.recordId,
134
+ requestId: record.requestId,
135
+ requestAttempt: record.requestAttempt,
136
+ reason: "request_attempt_superseded",
137
+ sequence: await this.#nextSequence(context.worldId),
138
+ };
139
+ await this.#appendRecordEvent(record, event);
140
+ }
141
+ }
142
+ async reconcileHead(worldId, head, exceptOperationId) {
143
+ return this.#withWorldMutation(worldId, () => this.#reconcileHead(worldId, head, exceptOperationId));
144
+ }
145
+ async #reconcileHead(worldId, head, exceptOperationId) {
146
+ if (worldId.trim() === "" || head.trim() === "")
147
+ throw new ArtifactStoreInvariantError("Artifact head-reconciliation arguments are invalid");
148
+ const operations = await this.#operationsForWorld(worldId);
149
+ const effective = await this.#effectiveRecords(worldId);
150
+ const affected = new Set();
151
+ for (const item of effective) {
152
+ const operationId = item.record.operationId;
153
+ if (operationId === exceptOperationId || item.status !== "active")
154
+ continue;
155
+ if (item.record.invalidation !== "head_change")
156
+ continue;
157
+ const boundHead = item.head ?? item.record.parentHead;
158
+ if (boundHead === head)
159
+ continue;
160
+ await this.#appendEventOnce({
161
+ schemaVersion: 1,
162
+ kind: "supersede",
163
+ operationId,
164
+ recordId: item.record.recordId,
165
+ reason: "head_change",
166
+ sequence: await this.#nextSequence(worldId),
167
+ });
168
+ affected.add(operationId);
169
+ }
170
+ for (const operation of operations) {
171
+ const operationId = operation.context.operationId;
172
+ if (operationId === exceptOperationId)
173
+ continue;
174
+ const boundHead = operation.head ?? operation.context.parentHead;
175
+ if (boundHead === head)
176
+ continue;
177
+ const hasActiveHeadBoundRecord = effective.some(({ record, status, head: recordHead }) => record.operationId === operationId &&
178
+ status === "active" &&
179
+ record.invalidation === "head_change" &&
180
+ (recordHead ?? record.parentHead) !== head);
181
+ if (!hasActiveHeadBoundRecord && !isOpenProjectionOperation(operation))
182
+ continue;
183
+ affected.add(operationId);
184
+ }
185
+ for (const operation of operations) {
186
+ if (!affected.has(operation.context.operationId))
187
+ continue;
188
+ if (!isOpenProjectionOperation(operation))
189
+ continue;
190
+ const next = {
191
+ ...operation,
192
+ artifactProjectionStatus: "superseded",
193
+ ...(isOpenExtension(operation)
194
+ ? {
195
+ extensionStatus: "superseded",
196
+ message: "This extension was superseded by a newer head",
197
+ }
198
+ : {}),
199
+ };
200
+ if (JSON.stringify(next) !== JSON.stringify(operation))
201
+ await this.#writeOperation(next);
202
+ }
203
+ }
204
+ async emit(input) {
205
+ return this.#withWorldMutation(input.context.worldId, () => this.#emit(input));
206
+ }
207
+ async #emit(input) {
208
+ assertRequestContext(input.context);
209
+ const declaration = findDeclaration(input.context, input.output);
210
+ if (declaration === null)
211
+ return artifactFailure(`Undeclared artifact output: ${input.output}`);
212
+ const payload = normalizePayload(declaration, input.payload);
213
+ if (!payload.ok)
214
+ return artifactFailure(payload.message);
215
+ if (input.toolCallId.trim() === "")
216
+ return artifactFailure("Tool-call ID cannot be empty");
217
+ const operation = await this.#readOperation(input.context.operationId);
218
+ if (operation === null)
219
+ throw new ArtifactStoreInvariantError("Artifact emit is missing an operation checkpoint");
220
+ if (!sameContext(operation.context, input.context))
221
+ throw new ArtifactStoreInvariantError("Artifact emit does not match the frozen operation");
222
+ const currentAttempt = operation.requestAttempts[input.context.requestId];
223
+ if (currentAttempt === undefined)
224
+ throw new ArtifactStoreInvariantError("Artifact emit request attempt is not registered");
225
+ const fingerprint = payloadFingerprint(input.output, payload.value);
226
+ const callFingerprint = artifactCallFingerprint("artifact_emit", input.context, { output: declaration.name, payload: payload.value });
227
+ const prior = await this.#recordsForOperation(input.context.operationId);
228
+ const previousByCall = prior.find((record) => record.toolCallId === input.toolCallId);
229
+ if (previousByCall !== undefined) {
230
+ if (previousByCall.callFingerprint !== callFingerprint)
231
+ return artifactFailure("The same artifact tool-call ID is bound to different arguments");
232
+ await this.#repairRecordPublication(previousByCall, operation, currentAttempt);
233
+ return {
234
+ ok: true,
235
+ idempotent: true,
236
+ recordId: previousByCall.recordId,
237
+ markdown: `# Runtime artifact already exists\n\noutput=${input.output}`,
238
+ };
239
+ }
240
+ const previousEvent = (await this.#events(input.context.worldId)).find((event) => event.operationId === input.context.operationId &&
241
+ event.toolCallId === input.toolCallId);
242
+ if (previousEvent !== undefined)
243
+ return artifactFailure("The same artifact tool-call ID is bound to a different tool call");
244
+ const staleAttempt = currentAttempt !== input.context.requestAttempt;
245
+ const sameOutput = prior.filter((record) => record.requestId === input.context.requestId &&
246
+ record.requestAttempt === input.context.requestAttempt &&
247
+ record.output === input.output);
248
+ if (sameOutput.length >= declaration.maxEmits)
249
+ return artifactFailure(`Artifact ${input.output} has reached maxEmits`);
250
+ const payloadBytes = byteLength(payload.value, declaration.contentType);
251
+ if (payloadBytes > maxPayloadBytes)
252
+ return artifactFailure("Artifact exceeds the 4 MiB limit");
253
+ const totalBytes = prior
254
+ .filter((record) => record.requestId === input.context.requestId &&
255
+ record.requestAttempt === input.context.requestAttempt)
256
+ .reduce((total, record) => total + byteLength(record.payload, record.contentType), 0);
257
+ if (totalBytes + payloadBytes > input.context.maxArtifactBytes)
258
+ return artifactFailure("Follow-up request artifacts exceed maxArtifactBytes");
259
+ const record = await this.#newRecord(input, declaration, payload.value, fingerprint, callFingerprint);
260
+ const stale = isStaleOperation(operation) || staleAttempt;
261
+ if (declaration.save === "none") {
262
+ const records = this.#memoryRecords.get(input.context.operationId) ?? [];
263
+ records.push(record);
264
+ this.#memoryRecords.set(input.context.operationId, records);
265
+ await this.#appendRecordEvent(record, {
266
+ schemaVersion: 1,
267
+ kind: stale ? "supersede" : "activate",
268
+ sequence: await this.#nextSequence(input.context.worldId),
269
+ operationId: input.context.operationId,
270
+ recordId: record.recordId,
271
+ requestId: record.requestId,
272
+ requestAttempt: record.requestAttempt,
273
+ ...(stale ? { reason: "late_superseded_operation_result" } : {}),
274
+ });
275
+ if (!stale &&
276
+ operation?.status === "core_committed" &&
277
+ operation.head !== undefined) {
278
+ await this.#appendRecordEvent(record, {
279
+ schemaVersion: 1,
280
+ kind: "bind_head",
281
+ sequence: await this.#nextSequence(input.context.worldId),
282
+ operationId: input.context.operationId,
283
+ recordId: record.recordId,
284
+ head: operation.head,
285
+ });
286
+ }
287
+ }
288
+ else {
289
+ await this.#appendRaw(record);
290
+ if (stale)
291
+ await this.#appendRecordEvent(record, {
292
+ schemaVersion: 1,
293
+ kind: "supersede",
294
+ sequence: await this.#nextSequence(input.context.worldId),
295
+ operationId: input.context.operationId,
296
+ recordId: record.recordId,
297
+ requestId: record.requestId,
298
+ requestAttempt: record.requestAttempt,
299
+ reason: "late_superseded_operation_result",
300
+ });
301
+ else if (operation?.status === "core_committed" &&
302
+ operation.head !== undefined) {
303
+ await this.#appendRecordEvent(record, {
304
+ schemaVersion: 1,
305
+ kind: "bind_head",
306
+ sequence: await this.#nextSequence(input.context.worldId),
307
+ operationId: input.context.operationId,
308
+ recordId: record.recordId,
309
+ head: operation.head,
310
+ });
311
+ await this.#appendRecordEvent(record, {
312
+ schemaVersion: 1,
313
+ kind: "activate",
314
+ sequence: await this.#nextSequence(input.context.worldId),
315
+ operationId: input.context.operationId,
316
+ recordId: record.recordId,
317
+ });
318
+ }
319
+ else if (declaration.save === "operation")
320
+ await this.#appendRecordEvent(record, {
321
+ schemaVersion: 1,
322
+ kind: "activate",
323
+ sequence: await this.#nextSequence(input.context.worldId),
324
+ operationId: input.context.operationId,
325
+ recordId: record.recordId,
326
+ });
327
+ }
328
+ return {
329
+ ok: true,
330
+ recordId: record.recordId,
331
+ markdown: `# Runtime artifact accepted\n\noutput=${input.output}`,
332
+ };
333
+ }
334
+ async #repairRecordPublication(record, operation, currentAttempt) {
335
+ const events = await this.#events(record.worldId);
336
+ const hasEvent = (kind, head) => events.some((event) => event.kind === kind &&
337
+ event.operationId === record.operationId &&
338
+ event.recordId === record.recordId &&
339
+ (head === undefined || event.head === head));
340
+ const stale = isStaleOperation(operation) || currentAttempt !== record.requestAttempt;
341
+ if (stale) {
342
+ if (!hasEvent("supersede"))
343
+ await this.#appendRecordEvent(record, {
344
+ schemaVersion: 1,
345
+ kind: "supersede",
346
+ sequence: await this.#nextSequence(record.worldId),
347
+ operationId: record.operationId,
348
+ recordId: record.recordId,
349
+ requestId: record.requestId,
350
+ requestAttempt: record.requestAttempt,
351
+ reason: "late_superseded_operation_result",
352
+ });
353
+ return;
354
+ }
355
+ if (operation.status === "core_committed" && operation.head !== undefined) {
356
+ if (!hasEvent("bind_head", operation.head))
357
+ await this.#appendRecordEvent(record, {
358
+ schemaVersion: 1,
359
+ kind: "bind_head",
360
+ sequence: await this.#nextSequence(record.worldId),
361
+ operationId: record.operationId,
362
+ recordId: record.recordId,
363
+ head: operation.head,
364
+ });
365
+ if (!hasEvent("activate"))
366
+ await this.#appendRecordEvent(record, {
367
+ schemaVersion: 1,
368
+ kind: "activate",
369
+ sequence: await this.#nextSequence(record.worldId),
370
+ operationId: record.operationId,
371
+ recordId: record.recordId,
372
+ });
373
+ return;
374
+ }
375
+ if (record.save === "operation" && !hasEvent("activate"))
376
+ await this.#appendRecordEvent(record, {
377
+ schemaVersion: 1,
378
+ kind: "activate",
379
+ sequence: await this.#nextSequence(record.worldId),
380
+ operationId: record.operationId,
381
+ recordId: record.recordId,
382
+ });
383
+ }
384
+ async clear(input) {
385
+ return this.#withWorldMutation(input.context.worldId, () => this.#clear(input));
386
+ }
387
+ async #clear(input) {
388
+ assertRequestContext(input.context);
389
+ const declaration = findDeclaration(input.context, input.output);
390
+ if (declaration === null)
391
+ return artifactFailure(`Undeclared artifact output: ${input.output}`);
392
+ if (input.toolCallId.trim() === "")
393
+ return artifactFailure("Tool-call ID cannot be empty");
394
+ const operation = await this.#readOperation(input.context.operationId);
395
+ if (operation === null)
396
+ throw new ArtifactStoreInvariantError("Artifact clear is missing an operation checkpoint");
397
+ if (!sameContext(operation.context, input.context))
398
+ throw new ArtifactStoreInvariantError("Artifact clear does not match the frozen operation");
399
+ const currentAttempt = operation.requestAttempts[input.context.requestId];
400
+ if (currentAttempt === undefined)
401
+ throw new ArtifactStoreInvariantError("Artifact clear request attempt is not registered");
402
+ const clearFingerprint = artifactCallFingerprint("artifact_clear", input.context, { output: declaration.name });
403
+ const records = await this.#recordsForOperation(input.context.operationId);
404
+ const prior = records.find((record) => record.toolCallId === input.toolCallId);
405
+ if (prior !== undefined)
406
+ return artifactFailure("The same artifact tool-call ID is bound to a different tool call");
407
+ const priorCall = (await this.#events(input.context.worldId)).find((event) => event.operationId === input.context.operationId &&
408
+ event.toolCallId === input.toolCallId);
409
+ if (priorCall !== undefined) {
410
+ if (priorCall.callFingerprint !== clearFingerprint)
411
+ return artifactFailure("The same artifact tool-call ID is bound to different arguments");
412
+ return {
413
+ ok: true,
414
+ idempotent: true,
415
+ markdown: `# Runtime artifact clear confirmed\n\noutput=${input.output}`,
416
+ };
417
+ }
418
+ const staleAttempt = currentAttempt !== input.context.requestAttempt;
419
+ if (isStaleOperation(operation) || staleAttempt) {
420
+ const event = {
421
+ schemaVersion: 1,
422
+ kind: "ignored_call",
423
+ operationId: input.context.operationId,
424
+ output: declaration.name,
425
+ channel: declaration.channel,
426
+ ...(declaration.key === undefined ? {} : { key: declaration.key }),
427
+ reason: "stale_clear_ignored",
428
+ sequence: await this.#nextSequence(input.context.worldId),
429
+ toolCallId: input.toolCallId,
430
+ toolName: "artifact_clear",
431
+ callFingerprint: clearFingerprint,
432
+ requestId: input.context.requestId,
433
+ requestAttempt: input.context.requestAttempt,
434
+ };
435
+ // An ignored stale call is a durable identity ledger even for save:none:
436
+ // no raw payload is persisted, but a cold retry must distinguish the
437
+ // exact same call from a changed request/attempt/parameter.
438
+ await this.#appendEventOnce(event);
439
+ return {
440
+ ok: true,
441
+ markdown: `# Runtime artifact clear ignored\n\noutput=${input.output}`,
442
+ };
443
+ }
444
+ const event = {
445
+ schemaVersion: 1,
446
+ kind: "clear",
447
+ operationId: input.context.operationId,
448
+ output: declaration.name,
449
+ channel: declaration.channel,
450
+ ...(declaration.key === undefined ? {} : { key: declaration.key }),
451
+ reason: "explicit_clear",
452
+ sequence: await this.#nextSequence(input.context.worldId),
453
+ toolCallId: input.toolCallId,
454
+ toolName: "artifact_clear",
455
+ callFingerprint: clearFingerprint,
456
+ requestId: input.context.requestId,
457
+ requestAttempt: input.context.requestAttempt,
458
+ };
459
+ // Projection clears are durable even for save:none. Only the raw payload
460
+ // is ephemeral; otherwise a cold restart would resurrect an old panel.
461
+ await this.#appendEventOnce(event);
462
+ return {
463
+ ok: true,
464
+ markdown: `# Runtime artifact clear confirmed\n\noutput=${input.output}`,
465
+ };
466
+ }
467
+ async markCoreCommitted(context, head) {
468
+ return this.#withWorldMutation(context.worldId, () => this.#markCoreCommitted(context, head));
469
+ }
470
+ async #markCoreCommitted(context, head) {
471
+ assertContext(context);
472
+ if (head.trim() === "")
473
+ throw new ArtifactStoreInvariantError("Core commit head is invalid");
474
+ const operation = await this.#readOperation(context.operationId);
475
+ if (operation === null)
476
+ throw new ArtifactStoreInvariantError("Core commit is missing an artifact-operation checkpoint");
477
+ if (!sameContext(operation.context, context))
478
+ throw new ArtifactStoreInvariantError("Artifact operation is bound to a different frozen payload");
479
+ if (operation.status === "completed") {
480
+ if (operation.head !== head)
481
+ throw new ArtifactStoreInvariantError("Completed artifact operation has a conflicting core head");
482
+ return;
483
+ }
484
+ if (operation.status === "core_failed")
485
+ throw new ArtifactStoreInvariantError("An artifact operation whose core failed cannot be committed again");
486
+ if (operation.status === "core_committed") {
487
+ if (operation.head !== head)
488
+ throw new ArtifactStoreInvariantError("Committed artifact operation has a conflicting core head");
489
+ await this.#activateCommittedRecords(operation, context, head);
490
+ return;
491
+ }
492
+ const recoveredFromMaterializationPending = operation.status === "core_materialization_pending";
493
+ if (recoveredFromMaterializationPending && operation.head !== head)
494
+ throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization has a conflicting core head");
495
+ await this.reconcileHead(context.worldId, head, context.operationId);
496
+ const committed = {
497
+ ...operation,
498
+ status: "core_committed",
499
+ extensionStatus: recoveredFromMaterializationPending
500
+ ? "not_started"
501
+ : operation.extensionStatus,
502
+ ...(recoveredFromMaterializationPending ? { completedRequests: [] } : {}),
503
+ head,
504
+ };
505
+ if (recoveredFromMaterializationPending)
506
+ delete committed.message;
507
+ // Persist the receipt/head before projection events. A crash after this
508
+ // write is repaired by the idempotent same-head path above.
509
+ await this.#writeOperation(committed);
510
+ await this.#activateCommittedRecords(committed, context, head);
511
+ }
512
+ async markCoreMaterializationPending(context, head, message) {
513
+ return this.#withWorldMutation(context.worldId, () => this.#markCoreMaterializationPending(context, head, message));
514
+ }
515
+ async #markCoreMaterializationPending(context, head, message) {
516
+ const operation = await this.#readOperation(context.operationId);
517
+ if (operation === null)
518
+ throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization does not exist");
519
+ if (!sameContext(operation.context, context))
520
+ throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization does not match the frozen operation");
521
+ if (operation.status === "core_failed")
522
+ throw new ArtifactStoreInvariantError("An artifact operation whose core failed cannot await materialization");
523
+ if (operation.status === "core_committed" ||
524
+ operation.status === "completed")
525
+ return summary(operation);
526
+ if (operation.status === "core_materialization_pending" &&
527
+ operation.head !== head)
528
+ throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization has a conflicting core head");
529
+ const pending = {
530
+ ...operation,
531
+ status: "core_materialization_pending",
532
+ extensionStatus: "recovery_required",
533
+ message,
534
+ completedRequests: [],
535
+ head,
536
+ };
537
+ await this.#writeOperation(pending);
538
+ return summary(pending);
539
+ }
540
+ async #activateCommittedRecords(operation, context, head) {
541
+ if (operation.artifactProjectionStatus === "superseded")
542
+ return;
543
+ const effective = await this.#effectiveRecords(context.worldId);
544
+ for (const item of effective) {
545
+ const { record, status, head: boundHead } = item;
546
+ if (record.operationId !== context.operationId)
547
+ continue;
548
+ if (status === "superseded" ||
549
+ status === "cleared" ||
550
+ status === "failed")
551
+ continue;
552
+ if (boundHead !== null && boundHead !== head)
553
+ throw new ArtifactStoreInvariantError("Artifact record is bound to a different core head");
554
+ if (boundHead !== head)
555
+ await this.#appendRecordEvent(record, {
556
+ schemaVersion: 1,
557
+ kind: "bind_head",
558
+ operationId: context.operationId,
559
+ recordId: record.recordId,
560
+ head,
561
+ sequence: await this.#nextSequence(context.worldId),
562
+ });
563
+ if (status === "pending")
564
+ await this.#appendRecordEvent(record, {
565
+ schemaVersion: 1,
566
+ kind: "activate",
567
+ operationId: context.operationId,
568
+ recordId: record.recordId,
569
+ sequence: await this.#nextSequence(context.worldId),
570
+ });
571
+ }
572
+ }
573
+ async markCoreFailed(operationId, message) {
574
+ const worldId = await this.#worldForOperation(operationId);
575
+ if (worldId === null)
576
+ return;
577
+ return this.#withWorldMutation(worldId, () => this.#markCoreFailed(operationId, message));
578
+ }
579
+ async #markCoreFailed(operationId, message) {
580
+ const operation = await this.#readOperation(operationId);
581
+ if (operation === null)
582
+ return;
583
+ if (operation.status !== "running")
584
+ return;
585
+ const records = await this.#recordsForOperation(operationId);
586
+ for (const record of records)
587
+ await this.#appendEventOnce({
588
+ schemaVersion: 1,
589
+ kind: "supersede",
590
+ operationId,
591
+ recordId: record.recordId,
592
+ reason: "core_failed",
593
+ sequence: await this.#nextSequence(operation.context.worldId),
594
+ });
595
+ await this.#writeOperation({
596
+ ...operation,
597
+ status: "core_failed",
598
+ extensionStatus: "failed",
599
+ artifactProjectionStatus: "superseded",
600
+ ...(message === undefined ? {} : { message }),
601
+ });
602
+ }
603
+ async beginExtension(context) {
604
+ return this.#withWorldMutation(context.worldId, () => this.#beginExtension(context));
605
+ }
606
+ async #beginExtension(context) {
607
+ const operation = await this.#readOperation(context.operationId);
608
+ if (operation === null)
609
+ throw new ArtifactStoreInvariantError("Artifact extension is missing an operation checkpoint");
610
+ if (!sameContext(operation.context, context))
611
+ throw new ArtifactStoreInvariantError("Artifact extension does not match the frozen operation");
612
+ if (operation.status !== "core_committed" ||
613
+ operation.artifactProjectionStatus === "superseded")
614
+ return;
615
+ if (operation.extensionStatus !== "not_started")
616
+ return;
617
+ await this.#writeOperation({ ...operation, extensionStatus: "running" });
618
+ }
619
+ async completeExtension(operationId, requests) {
620
+ const worldId = await this.#worldForOperation(operationId);
621
+ if (worldId === null)
622
+ return this.#completeExtension(operationId, requests);
623
+ return this.#withWorldMutation(worldId, () => this.#completeExtension(operationId, requests));
624
+ }
625
+ async #completeExtension(operationId, requests) {
626
+ const operation = await this.#readOperation(operationId);
627
+ if (operation === null)
628
+ return {
629
+ operationId,
630
+ status: "recovery_required",
631
+ completedRequests: [],
632
+ coreCommitted: false,
633
+ };
634
+ if (operation.status === "running")
635
+ return summary(operation);
636
+ if (isTerminalExtensionStatus(operation.extensionStatus) ||
637
+ operation.extensionStatus === "unknown" ||
638
+ operation.extensionStatus === "recovery_required")
639
+ return summary(operation);
640
+ if (operation.status !== "core_committed")
641
+ throw new ArtifactStoreInvariantError("Artifact extension cannot complete before the core is committed");
642
+ const records = await this.#recordsForOperation(operationId);
643
+ for (const record of records)
644
+ if (record.save === "none" || record.invalidation === "operation_end")
645
+ await this.#appendRecordEvent(record, {
646
+ schemaVersion: 1,
647
+ kind: "supersede",
648
+ operationId,
649
+ recordId: record.recordId,
650
+ reason: "operation_end",
651
+ sequence: await this.#nextSequence(operation.context.worldId),
652
+ });
653
+ const next = {
654
+ ...operation,
655
+ status: "completed",
656
+ extensionStatus: "completed",
657
+ completedRequests: [...requests],
658
+ };
659
+ delete next.message;
660
+ await this.#writeOperation(next);
661
+ return summary(next);
662
+ }
663
+ async failExtension(operationId, status, message, requests = []) {
664
+ const worldId = await this.#worldForOperation(operationId);
665
+ if (worldId === null)
666
+ return this.#failExtension(operationId, status, message, requests);
667
+ return this.#withWorldMutation(worldId, () => this.#failExtension(operationId, status, message, requests));
668
+ }
669
+ async #failExtension(operationId, status, message, requests = []) {
670
+ const operation = await this.#readOperation(operationId);
671
+ if (operation === null)
672
+ return {
673
+ operationId,
674
+ status: "recovery_required",
675
+ completedRequests: requests,
676
+ message,
677
+ coreCommitted: false,
678
+ };
679
+ if (isTerminalExtensionStatus(operation.extensionStatus) ||
680
+ operation.extensionStatus === "unknown" ||
681
+ operation.extensionStatus === "recovery_required")
682
+ return summary(operation);
683
+ if (operation.status !== "core_committed")
684
+ throw new ArtifactStoreInvariantError("Artifact extension cannot fail before the core is committed");
685
+ const records = await this.#recordsForOperation(operationId);
686
+ for (const record of records)
687
+ if (record.save === "none" || record.invalidation === "operation_end")
688
+ await this.#appendRecordEvent(record, {
689
+ schemaVersion: 1,
690
+ kind: "supersede",
691
+ operationId,
692
+ recordId: record.recordId,
693
+ reason: "extension_failed",
694
+ sequence: await this.#nextSequence(operation.context.worldId),
695
+ });
696
+ const next = {
697
+ ...operation,
698
+ extensionStatus: status,
699
+ message,
700
+ completedRequests: [...requests],
701
+ };
702
+ await this.#writeOperation(next);
703
+ return summary(next);
704
+ }
705
+ async readExtension(operationId) {
706
+ const operation = await this.#readOperation(operationId);
707
+ return operation === null ? null : summary(operation);
708
+ }
709
+ async readExtensionSummaries(worldId) {
710
+ return (await this.#operationsForWorld(worldId))
711
+ .filter((operation) => operation.status !== "core_failed" &&
712
+ !(operation.status === "completed" &&
713
+ operation.extensionStatus === "completed" &&
714
+ operation.completedRequests.length === 0))
715
+ .sort((left, right) => left.context.operationId.localeCompare(right.context.operationId))
716
+ .map(summary);
717
+ }
718
+ async resumeExtension(operationId) {
719
+ const worldId = await this.#worldForOperation(operationId);
720
+ if (worldId === null)
721
+ return this.#resumeExtension(operationId);
722
+ return this.#withWorldMutation(worldId, () => this.#resumeExtension(operationId));
723
+ }
724
+ async #resumeExtension(operationId) {
725
+ const operation = await this.#readOperation(operationId);
726
+ if (operation === null)
727
+ return {
728
+ operationId,
729
+ status: "recovery_required",
730
+ completedRequests: [],
731
+ message: "No recoverable extension checkpoint was found",
732
+ coreCommitted: false,
733
+ };
734
+ if (operation.status === "running")
735
+ return summary(operation);
736
+ if (isTerminalExtensionStatus(operation.extensionStatus) ||
737
+ operation.extensionStatus === "recovery_required")
738
+ return summary(operation);
739
+ const next = {
740
+ ...operation,
741
+ extensionStatus: "recovery_required",
742
+ message: "Extension dispatch state cannot be replayed safely; the original request attempt requires explicit manual recovery",
743
+ };
744
+ await this.#writeOperation(next);
745
+ return summary(next);
746
+ }
747
+ async readActiveProjection(worldId, channel) {
748
+ const effective = await this.#effectiveRecords(worldId);
749
+ const operationStates = new Map();
750
+ for (const item of effective)
751
+ if (!operationStates.has(item.record.operationId)) {
752
+ const operation = await this.#readOperation(item.record.operationId);
753
+ if (operation !== null)
754
+ operationStates.set(item.record.operationId, operation);
755
+ }
756
+ const active = effective.filter(({ record, status, head }) => {
757
+ if (status !== "active" || record.projection === "hidden")
758
+ return false;
759
+ if (channel !== undefined && record.channel !== channel)
760
+ return false;
761
+ const operation = operationStates.get(record.operationId);
762
+ if (operation === undefined ||
763
+ operation.artifactProjectionStatus === "superseded")
764
+ return false;
765
+ if (record.projection === "transient" && !isActiveInteraction(operation))
766
+ return false;
767
+ if (record.save === "operation" && operation?.status === "completed")
768
+ return false;
769
+ if (record.save === "commit" && head === null)
770
+ return false;
771
+ return true;
772
+ });
773
+ const selected = selectProjection(active);
774
+ return selected.map(({ record, head }) => ({
775
+ recordId: record.recordId,
776
+ worldId: record.worldId,
777
+ operationId: record.operationId,
778
+ playPresetId: record.playPresetId,
779
+ playPresetRevision: record.playPresetRevision,
780
+ playPresetScriptsEnabled: record.playPresetScriptsEnabled,
781
+ requestId: record.requestId,
782
+ requestAttempt: record.requestAttempt,
783
+ output: record.output,
784
+ channel: record.channel,
785
+ ...(record.key === undefined ? {} : { key: record.key }),
786
+ contentType: record.contentType,
787
+ ...(record.renderer === undefined ? {} : { renderer: record.renderer }),
788
+ ...(record.rendererRevision === undefined
789
+ ? {}
790
+ : { rendererRevision: record.rendererRevision }),
791
+ payload: structuredClone(record.payload),
792
+ projection: record.projection,
793
+ save: record.save,
794
+ sequence: record.sequence,
795
+ head: head ?? record.parentHead,
796
+ }));
797
+ }
798
+ async readDebug(worldId, operationId) {
799
+ const effective = await this.#effectiveRecords(worldId);
800
+ return effective
801
+ .filter(({ record }) => operationId === undefined || record.operationId === operationId)
802
+ .sort((left, right) => left.record.sequence - right.record.sequence)
803
+ .map(({ record, status, head }) => ({
804
+ recordId: record.recordId,
805
+ sequence: record.sequence,
806
+ operationId: record.operationId,
807
+ worldId: record.worldId,
808
+ playPresetId: record.playPresetId,
809
+ playPresetRevision: record.playPresetRevision,
810
+ playPresetScriptsEnabled: record.playPresetScriptsEnabled,
811
+ parentHead: record.parentHead,
812
+ newHead: head,
813
+ requestId: record.requestId,
814
+ requestAttempt: record.requestAttempt,
815
+ output: record.output,
816
+ channel: record.channel,
817
+ ...(record.key === undefined ? {} : { key: record.key }),
818
+ contentType: record.contentType,
819
+ ...(record.renderer === undefined ? {} : { renderer: record.renderer }),
820
+ ...(record.rendererRevision === undefined
821
+ ? {}
822
+ : { rendererRevision: record.rendererRevision }),
823
+ save: record.save,
824
+ projection: record.projection,
825
+ payloadBytes: byteLength(record.payload, record.contentType),
826
+ payload: structuredClone(record.payload),
827
+ payloadFingerprint: record.payloadFingerprint,
828
+ status,
829
+ }));
830
+ }
831
+ async #newRecord(input, declaration, payload, fingerprint, callFingerprint) {
832
+ const sequence = await this.#nextSequence(input.context.worldId);
833
+ const recordId = `artifact:${sequence}:${fingerprint.slice(7, 23)}`;
834
+ const record = {
835
+ schemaVersion: 1,
836
+ recordId,
837
+ sequence,
838
+ worldId: input.context.worldId,
839
+ parentHead: input.context.parentHead,
840
+ newHead: null,
841
+ operationId: input.context.operationId,
842
+ playPresetId: input.context.playPresetId,
843
+ playPresetRevision: input.context.playPresetRevision,
844
+ playPresetScriptsEnabled: input.context.playPresetScriptsEnabled,
845
+ requestId: input.context.requestId,
846
+ requestAttempt: input.context.requestAttempt,
847
+ output: declaration.name,
848
+ channel: declaration.channel,
849
+ ...(declaration.key === undefined ? {} : { key: declaration.key }),
850
+ contentType: declaration.contentType,
851
+ ...(declaration.renderer === undefined
852
+ ? {}
853
+ : { renderer: declaration.renderer }),
854
+ ...(declaration.rendererRevision === undefined
855
+ ? {}
856
+ : { rendererRevision: declaration.rendererRevision }),
857
+ payload: structuredClone(payload),
858
+ save: declaration.save,
859
+ projection: declaration.strategy,
860
+ invalidation: declaration.invalidation,
861
+ toolCallId: input.toolCallId,
862
+ toolName: "artifact_emit",
863
+ callFingerprint,
864
+ payloadFingerprint: fingerprint,
865
+ status: "pending",
866
+ };
867
+ return {
868
+ ...record,
869
+ recordFingerprint: rawRecordFingerprint(record),
870
+ };
871
+ }
872
+ async #appendRaw(record) {
873
+ const directory = join(this.#worldRoot(record.worldId), "records");
874
+ await mkdir(directory, { recursive: true, mode: 0o700 });
875
+ await writeDurableJson(join(directory, `${String(record.sequence).padStart(12, "0")}.json`), record, true);
876
+ }
877
+ async #appendRecordEvent(record, event) {
878
+ await this.#appendCallEvent({
879
+ worldId: record.worldId,
880
+ parentHead: record.parentHead,
881
+ operationId: record.operationId,
882
+ playPresetId: record.playPresetId,
883
+ playPresetRevision: record.playPresetRevision,
884
+ playPresetScriptsEnabled: record.playPresetScriptsEnabled,
885
+ requestId: record.requestId,
886
+ requestAttempt: record.requestAttempt,
887
+ maxArtifactBytes: maxPayloadBytes,
888
+ declarations: [],
889
+ }, record.save, event);
890
+ }
891
+ async #appendCallEvent(context, save, event) {
892
+ if (save === "none") {
893
+ const events = this.#memoryEvents.get(context.operationId) ?? [];
894
+ if (!events.some((candidate) => sameEventIdentity(candidate, event))) {
895
+ events.push(event);
896
+ this.#memoryEvents.set(context.operationId, events);
897
+ }
898
+ return;
899
+ }
900
+ await this.#appendEventOnce(event);
901
+ }
902
+ async #appendEvent(event) {
903
+ const worldId = await this.#worldForOperation(event.operationId);
904
+ if (worldId === null) {
905
+ const memory = this.#memoryEvents.get(event.operationId) ?? [];
906
+ memory.push(event);
907
+ this.#memoryEvents.set(event.operationId, memory);
908
+ return;
909
+ }
910
+ const path = join(this.#worldRoot(worldId), "events", `${String(event.sequence).padStart(12, "0")}.json`);
911
+ await mkdir(join(this.#worldRoot(worldId), "events"), {
912
+ recursive: true,
913
+ mode: 0o700,
914
+ });
915
+ await writeDurableJson(path, event, true);
916
+ }
917
+ async #appendEventOnce(event) {
918
+ const worldId = await this.#worldForOperation(event.operationId);
919
+ if (worldId !== null) {
920
+ const existing = await this.#events(worldId);
921
+ if (existing.some((candidate) => sameEventIdentity(candidate, event)))
922
+ return;
923
+ }
924
+ else {
925
+ const existing = this.#memoryEvents.get(event.operationId) ?? [];
926
+ if (existing.some((candidate) => sameEventIdentity(candidate, event)))
927
+ return;
928
+ }
929
+ await this.#appendEvent(event);
930
+ }
931
+ async #effectiveRecords(worldId) {
932
+ const raw = [];
933
+ const memory = this.#memoryRecords;
934
+ const recordsDirectory = join(this.#worldRoot(worldId), "records");
935
+ try {
936
+ const names = (await readdir(recordsDirectory))
937
+ .filter((name) => name.endsWith(".json"))
938
+ .sort();
939
+ for (const name of names) {
940
+ const parsed = parseArtifactJson(await readFile(join(recordsDirectory, name), "utf8"), "artifact raw record");
941
+ assertRawRecord(parsed);
942
+ if (name !== artifactSequenceFileName(parsed.sequence))
943
+ throw new ArtifactStoreCorruptionError("Artifact raw-record file name does not match its sequence");
944
+ if (parsed.worldId !== worldId)
945
+ throw new ArtifactStoreCorruptionError("Artifact raw record does not match the world path");
946
+ raw.push(parsed);
947
+ }
948
+ }
949
+ catch (error) {
950
+ if (!isMissing(error))
951
+ throw error;
952
+ }
953
+ for (const records of memory.values())
954
+ for (const record of records)
955
+ if (record.worldId === worldId)
956
+ raw.push(record);
957
+ raw.sort((left, right) => left.sequence - right.sequence);
958
+ const recordIds = new Set();
959
+ const sequences = new Set();
960
+ for (const record of raw) {
961
+ if (recordIds.has(record.recordId))
962
+ throw new ArtifactStoreCorruptionError("Artifact raw-record ID is duplicated");
963
+ if (sequences.has(record.sequence))
964
+ throw new ArtifactStoreCorruptionError("Artifact world sequence is duplicated");
965
+ recordIds.add(record.recordId);
966
+ sequences.add(record.sequence);
967
+ }
968
+ const state = new Map();
969
+ for (const record of raw)
970
+ state.set(record.recordId, { status: "pending", head: null });
971
+ const operationStates = new Map();
972
+ for (const operation of await this.#operationsForWorld(worldId))
973
+ operationStates.set(operation.context.operationId, operation);
974
+ for (const record of raw) {
975
+ const operation = operationStates.get(record.operationId);
976
+ if (operation?.context.worldId !== record.worldId ||
977
+ operation.context.parentHead !== record.parentHead ||
978
+ operation.context.playPresetId !== record.playPresetId ||
979
+ operation.context.playPresetRevision !== record.playPresetRevision ||
980
+ operation.context.playPresetScriptsEnabled !==
981
+ record.playPresetScriptsEnabled)
982
+ throw new ArtifactStoreCorruptionError("Artifact raw record does not match the operation identity");
983
+ }
984
+ const events = await this.#events(worldId);
985
+ for (const event of events) {
986
+ if (sequences.has(event.sequence))
987
+ throw new ArtifactStoreCorruptionError("Artifact world sequence is duplicated");
988
+ sequences.add(event.sequence);
989
+ const eventOperation = operationStates.get(event.operationId);
990
+ if (eventOperation === undefined)
991
+ throw new ArtifactStoreCorruptionError("Artifact projection-event operation does not exist");
992
+ if (event.recordId !== undefined) {
993
+ const record = raw.find(({ recordId }) => recordId === event.recordId);
994
+ if (record?.operationId !== event.operationId)
995
+ throw new ArtifactStoreCorruptionError("Artifact projection event does not match the record identity");
996
+ }
997
+ if (event.kind === "activate" && event.recordId !== undefined) {
998
+ const current = state.get(event.recordId);
999
+ if (current?.status === "pending")
1000
+ current.status = "active";
1001
+ }
1002
+ else if (event.kind === "supersede" && event.recordId !== undefined) {
1003
+ const current = state.get(event.recordId);
1004
+ if (current !== undefined)
1005
+ current.status = "superseded";
1006
+ }
1007
+ else if (event.kind === "bind_head" && event.recordId !== undefined) {
1008
+ if ((eventOperation.status !== "core_committed" &&
1009
+ eventOperation.status !== "completed") ||
1010
+ eventOperation.head !== event.head)
1011
+ throw new ArtifactStoreCorruptionError("Artifact bind_head event does not match the operation core head");
1012
+ const current = state.get(event.recordId);
1013
+ if (current !== undefined)
1014
+ current.head = event.head ?? null;
1015
+ }
1016
+ else if (event.kind === "clear") {
1017
+ const initiator = operationStates.get(event.operationId);
1018
+ if (initiator === undefined ||
1019
+ initiator.artifactProjectionStatus === "superseded" ||
1020
+ (initiator.requestAttempts[event.requestId] !== undefined &&
1021
+ initiator.requestAttempts[event.requestId] !==
1022
+ event.requestAttempt))
1023
+ continue;
1024
+ for (const record of raw) {
1025
+ const current = state.get(record.recordId);
1026
+ if (current !== undefined &&
1027
+ current.status !== "superseded" &&
1028
+ current.status !== "cleared" &&
1029
+ record.sequence < event.sequence &&
1030
+ record.channel === event.channel &&
1031
+ record.output === event.output &&
1032
+ (event.key === undefined || record.key === event.key))
1033
+ current.status = "cleared";
1034
+ }
1035
+ }
1036
+ }
1037
+ return raw.map((record) => {
1038
+ const current = state.get(record.recordId);
1039
+ let status = current?.status ?? "failed";
1040
+ const operation = operationStates.get(record.operationId);
1041
+ const currentAttempt = operation?.requestAttempts[record.requestId];
1042
+ if (currentAttempt !== undefined &&
1043
+ currentAttempt !== record.requestAttempt &&
1044
+ status !== "failed")
1045
+ status = "superseded";
1046
+ if (operation?.artifactProjectionStatus === "superseded" &&
1047
+ status !== "failed")
1048
+ status = "superseded";
1049
+ return {
1050
+ record,
1051
+ status,
1052
+ head: current?.head ?? null,
1053
+ };
1054
+ });
1055
+ }
1056
+ async #events(worldId) {
1057
+ const result = [];
1058
+ const directory = join(this.#worldRoot(worldId), "events");
1059
+ try {
1060
+ const names = (await readdir(directory))
1061
+ .filter((name) => name.endsWith(".json"))
1062
+ .sort();
1063
+ for (const name of names) {
1064
+ const event = parseArtifactJson(await readFile(join(directory, name), "utf8"), "artifact projection event");
1065
+ assertEvent(event);
1066
+ if (name !== artifactSequenceFileName(event.sequence))
1067
+ throw new ArtifactStoreCorruptionError("Artifact projection-event file name does not match its sequence");
1068
+ result.push(event);
1069
+ }
1070
+ }
1071
+ catch (error) {
1072
+ if (!isMissing(error))
1073
+ throw error;
1074
+ }
1075
+ for (const [operationId, events] of this.#memoryEvents) {
1076
+ const operation = await this.#readOperation(operationId);
1077
+ if (operation?.context.worldId === worldId)
1078
+ result.push(...events);
1079
+ }
1080
+ return result.sort((left, right) => left.sequence - right.sequence);
1081
+ }
1082
+ async #recordsForOperation(operationId) {
1083
+ const operation = await this.#readOperation(operationId);
1084
+ if (operation === null)
1085
+ return this.#memoryRecords.get(operationId) ?? [];
1086
+ const records = await this.#effectiveRecords(operation.context.worldId);
1087
+ return records
1088
+ .filter(({ record }) => record.operationId === operationId)
1089
+ .map(({ record }) => record);
1090
+ }
1091
+ async #operationsForWorld(worldId) {
1092
+ const byId = new Map();
1093
+ const directory = join(this.#root, "operations");
1094
+ try {
1095
+ const names = (await readdir(directory))
1096
+ .filter((name) => name.endsWith(".json"))
1097
+ .sort();
1098
+ for (const name of names) {
1099
+ const operation = parseArtifactJson(await readFile(join(directory, name), "utf8"), "artifact operation");
1100
+ assertOperation(operation);
1101
+ if (name !== `${identityHash(operation.context.operationId)}.json`)
1102
+ throw new ArtifactStoreCorruptionError("Artifact operation does not match the file-path identity");
1103
+ if (byId.has(operation.context.operationId))
1104
+ throw new ArtifactStoreCorruptionError("Artifact operation ID is duplicated");
1105
+ if (operation.context.worldId === worldId)
1106
+ byId.set(operation.context.operationId, structuredClone(operation));
1107
+ }
1108
+ }
1109
+ catch (error) {
1110
+ if (!isMissing(error))
1111
+ throw error;
1112
+ }
1113
+ return [...byId.values()];
1114
+ }
1115
+ async #nextSequence(worldId) {
1116
+ let maximum = 0;
1117
+ const directories = [
1118
+ join(this.#worldRoot(worldId), "records"),
1119
+ join(this.#worldRoot(worldId), "events"),
1120
+ ];
1121
+ for (const directory of directories) {
1122
+ try {
1123
+ const names = (await readdir(directory)).filter((name) => name.endsWith(".json"));
1124
+ for (const name of names) {
1125
+ const value = Number.parseInt(name.slice(0, -5), 10);
1126
+ if (!Number.isSafeInteger(value) ||
1127
+ value < 1 ||
1128
+ name !== artifactSequenceFileName(value))
1129
+ throw new ArtifactStoreCorruptionError("Artifact sequence file name is invalid");
1130
+ maximum = Math.max(maximum, value);
1131
+ }
1132
+ }
1133
+ catch (error) {
1134
+ if (!isMissing(error))
1135
+ throw error;
1136
+ }
1137
+ }
1138
+ for (const records of this.#memoryRecords.values())
1139
+ for (const record of records)
1140
+ if (record.worldId === worldId)
1141
+ maximum = Math.max(maximum, record.sequence);
1142
+ for (const [operationId, events] of this.#memoryEvents) {
1143
+ const operation = await this.#readOperation(operationId);
1144
+ if (operation?.context.worldId !== worldId)
1145
+ continue;
1146
+ for (const event of events)
1147
+ maximum = Math.max(maximum, event.sequence);
1148
+ }
1149
+ return maximum + 1;
1150
+ }
1151
+ async #readOperation(operationId) {
1152
+ const path = join(this.#root, "operations", `${identityHash(operationId)}.json`);
1153
+ try {
1154
+ const operation = parseArtifactJson(await readFile(path, "utf8"), "artifact operation");
1155
+ assertOperation(operation);
1156
+ if (operation.context.operationId !== operationId)
1157
+ throw new ArtifactStoreCorruptionError("Artifact operation does not match the request identity");
1158
+ return operation;
1159
+ }
1160
+ catch (error) {
1161
+ if (isMissing(error))
1162
+ return null;
1163
+ throw error;
1164
+ }
1165
+ }
1166
+ async #writeOperation(operation) {
1167
+ assertOperation(operation);
1168
+ const directory = join(this.#root, "operations");
1169
+ await mkdir(directory, { recursive: true, mode: 0o700 });
1170
+ const path = join(directory, `${identityHash(operation.context.operationId)}.json`);
1171
+ await writeDurableJson(path, operation, false);
1172
+ }
1173
+ async #worldForOperation(operationId) {
1174
+ const operation = await this.#readOperation(operationId);
1175
+ return operation?.context.worldId ?? null;
1176
+ }
1177
+ async #withWorldMutation(worldId, action) {
1178
+ const active = this.#mutationContext.getStore();
1179
+ if (active?.has(worldId) === true)
1180
+ return action();
1181
+ return withArtifactWorldLock(join(this.#root, "locks", identityHash(worldId)), () => this.#mutationContext.run(new Set([...(active ?? []), worldId]), action));
1182
+ }
1183
+ #worldRoot(worldId) {
1184
+ return join(this.#root, "worlds", identityHash(worldId));
1185
+ }
1186
+ }
1187
+ const artifactWorldLockStaleMilliseconds = 30_000;
1188
+ const artifactWorldLockWaitMilliseconds = 30_000;
1189
+ async function withArtifactWorldLock(path, action) {
1190
+ const token = randomUUID();
1191
+ const processIdentity = await artifactLocalProcessIdentity();
1192
+ const parent = dirname(path);
1193
+ await mkdir(parent, { recursive: true, mode: 0o700 });
1194
+ const deadline = Date.now() + artifactWorldLockWaitMilliseconds;
1195
+ while (true) {
1196
+ try {
1197
+ await mkdir(path, { mode: 0o700 });
1198
+ break;
1199
+ }
1200
+ catch (error) {
1201
+ if (!isAlreadyExists(error))
1202
+ throw error;
1203
+ const owner = await readArtifactLockOwner(path);
1204
+ if (owner !== null && (await artifactLockOwnerIsAlive(owner, path))) {
1205
+ if (Date.now() >= deadline)
1206
+ throw new Error("artifact world mutation lock is busy", {
1207
+ cause: error,
1208
+ });
1209
+ await artifactLockDelay(5);
1210
+ continue;
1211
+ }
1212
+ const metadata = await stat(path).catch(() => null);
1213
+ if (owner !== null ||
1214
+ (metadata !== null &&
1215
+ Date.now() - metadata.mtimeMs > artifactWorldLockStaleMilliseconds))
1216
+ await retireArtifactWorldLock(path, owner, metadata, parent);
1217
+ if (Date.now() >= deadline)
1218
+ throw new Error("artifact world mutation lock is busy", {
1219
+ cause: error,
1220
+ });
1221
+ await artifactLockDelay(5);
1222
+ }
1223
+ }
1224
+ const ownerPath = join(path, "owner.json");
1225
+ const heartbeatPath = join(path, "heartbeat");
1226
+ let heartbeatHandle;
1227
+ let stopped = false;
1228
+ let timer;
1229
+ let heartbeat = Promise.resolve();
1230
+ const schedule = () => {
1231
+ const handle = heartbeatHandle;
1232
+ if (handle === undefined)
1233
+ throw new ArtifactStoreInvariantError("Artifact world-lock heartbeat is not initialized");
1234
+ timer = setTimeout(() => {
1235
+ const now = new Date();
1236
+ heartbeat = handle
1237
+ .utimes(now, now)
1238
+ .catch(() => undefined)
1239
+ .finally(() => {
1240
+ if (!stopped)
1241
+ schedule();
1242
+ });
1243
+ }, 5_000);
1244
+ };
1245
+ try {
1246
+ await writeDurableJson(ownerPath, { token, pid: process.pid, processIdentity }, true);
1247
+ if (process.env.NARRAEON_INTERNAL_TEST_FAIL_AT_ARTIFACT_LOCK_EDGE ===
1248
+ "after_owner" &&
1249
+ process.env.NODE_ENV === "test")
1250
+ throw new Error("simulated artifact lock setup failure");
1251
+ await writeDurableJson(heartbeatPath, { token }, true);
1252
+ heartbeatHandle = await open(heartbeatPath, "r+");
1253
+ schedule();
1254
+ return await action();
1255
+ }
1256
+ finally {
1257
+ stopped = true;
1258
+ if (timer !== undefined)
1259
+ clearTimeout(timer);
1260
+ await heartbeat;
1261
+ await heartbeatHandle?.close();
1262
+ await releaseArtifactWorldLock(path, token, parent);
1263
+ }
1264
+ }
1265
+ async function readArtifactLockOwner(path) {
1266
+ let value;
1267
+ try {
1268
+ value = JSON.parse(await readFile(join(path, "owner.json"), "utf8"));
1269
+ }
1270
+ catch (error) {
1271
+ if (isMissing(error))
1272
+ return null;
1273
+ throw error;
1274
+ }
1275
+ if (!isRecord(value) ||
1276
+ !hasExactKeys(value, ["token", "pid", "processIdentity"]) ||
1277
+ !isNonEmptyString(value.token) ||
1278
+ !Number.isSafeInteger(value.pid) ||
1279
+ Number(value.pid) < 1 ||
1280
+ !isNonEmptyString(value.processIdentity))
1281
+ throw new ArtifactStoreCorruptionError("Artifact world-mutation lock owner is corrupt");
1282
+ return value;
1283
+ }
1284
+ async function retireArtifactWorldLock(path, observedOwner, observedMetadata, locksRoot) {
1285
+ if (observedOwner === null && observedMetadata === null)
1286
+ return;
1287
+ const currentOwner = await readArtifactLockOwner(path);
1288
+ const currentMetadata = await stat(path).catch(() => null);
1289
+ const sameGeneration = observedOwner === null
1290
+ ? currentOwner === null &&
1291
+ observedMetadata !== null &&
1292
+ currentMetadata !== null &&
1293
+ observedMetadata.dev === currentMetadata.dev &&
1294
+ observedMetadata.ino === currentMetadata.ino
1295
+ : currentOwner?.token === observedOwner.token;
1296
+ if (!sameGeneration)
1297
+ return;
1298
+ if (currentOwner !== null &&
1299
+ (await artifactLockOwnerIsAlive(currentOwner, path)))
1300
+ return;
1301
+ if (currentOwner === null &&
1302
+ (currentMetadata === null ||
1303
+ Date.now() - currentMetadata.mtimeMs <=
1304
+ artifactWorldLockStaleMilliseconds))
1305
+ return;
1306
+ const generation = currentOwner?.token ??
1307
+ `inode-${currentMetadata?.dev ?? "missing"}-${currentMetadata?.ino ?? "missing"}`;
1308
+ const retired = `${path}.${identityHash(generation)}.${randomUUID()}.retired`;
1309
+ try {
1310
+ await rename(path, retired);
1311
+ await syncArtifactDirectory(locksRoot);
1312
+ await rm(retired, { recursive: true, force: true });
1313
+ }
1314
+ catch (error) {
1315
+ if (isMissing(error) || isAlreadyExists(error))
1316
+ return;
1317
+ throw error;
1318
+ }
1319
+ }
1320
+ async function releaseArtifactWorldLock(path, token, locksRoot) {
1321
+ const owner = await readArtifactLockOwner(path);
1322
+ if (owner?.token !== token)
1323
+ return;
1324
+ const retired = `${path}.${identityHash(token)}.${randomUUID()}.retired`;
1325
+ try {
1326
+ await rename(path, retired);
1327
+ await syncArtifactDirectory(locksRoot);
1328
+ await rm(retired, { recursive: true, force: true });
1329
+ }
1330
+ catch (error) {
1331
+ if (isMissing(error) || isAlreadyExists(error))
1332
+ return;
1333
+ throw error;
1334
+ }
1335
+ }
1336
+ let cachedArtifactProcessIdentity;
1337
+ function artifactLocalProcessIdentity() {
1338
+ cachedArtifactProcessIdentity ??= artifactProcessIdentity(process.pid).then((identity) => identity ?? `fallback:${process.pid}:${randomUUID()}`);
1339
+ return cachedArtifactProcessIdentity;
1340
+ }
1341
+ async function artifactLockOwnerIsAlive(owner, path) {
1342
+ const identity = await artifactProcessIdentity(owner.pid);
1343
+ if (identity === owner.processIdentity)
1344
+ return true;
1345
+ if (identity === null) {
1346
+ try {
1347
+ process.kill(owner.pid, 0);
1348
+ return true;
1349
+ }
1350
+ catch (error) {
1351
+ if (isRecord(error) && error.code === "ESRCH")
1352
+ return false;
1353
+ // Fall through to the heartbeat for non-Linux or inaccessible processes.
1354
+ }
1355
+ }
1356
+ const heartbeat = await stat(join(path, "heartbeat")).catch(() => null);
1357
+ return (heartbeat !== null &&
1358
+ Date.now() - heartbeat.mtimeMs <= artifactWorldLockStaleMilliseconds);
1359
+ }
1360
+ async function artifactProcessIdentity(pid) {
1361
+ try {
1362
+ const [bootId, namespace, processStat] = await Promise.all([
1363
+ readFile("/proc/sys/kernel/random/boot_id", "utf8"),
1364
+ readlink(`/proc/${pid}/ns/pid`),
1365
+ readFile(`/proc/${pid}/stat`, "utf8"),
1366
+ ]);
1367
+ const commandEnd = processStat.lastIndexOf(") ");
1368
+ if (commandEnd < 0)
1369
+ return null;
1370
+ const fields = processStat
1371
+ .slice(commandEnd + 2)
1372
+ .trim()
1373
+ .split(/\s+/u);
1374
+ const startTicks = fields[19];
1375
+ if (startTicks === undefined || !/^\d+$/u.test(startTicks))
1376
+ return null;
1377
+ return `linux:${bootId.trim()}:${namespace}:${startTicks}`;
1378
+ }
1379
+ catch {
1380
+ return null;
1381
+ }
1382
+ }
1383
+ async function writeDurableJson(path, value, exclusive) {
1384
+ const directory = dirname(path);
1385
+ await mkdir(directory, { recursive: true, mode: 0o700 });
1386
+ const target = exclusive ? path : `${path}.${randomUUID()}.tmp`;
1387
+ let handle;
1388
+ let created = false;
1389
+ try {
1390
+ handle = await open(target, "wx", 0o600);
1391
+ created = true;
1392
+ await handle.writeFile(JSON.stringify(value), "utf8");
1393
+ await handle.sync();
1394
+ await handle.close();
1395
+ handle = undefined;
1396
+ if (!exclusive)
1397
+ await rename(target, path);
1398
+ await syncArtifactDirectory(directory);
1399
+ }
1400
+ catch (error) {
1401
+ await handle?.close().catch(() => undefined);
1402
+ if (created)
1403
+ await rm(target, { force: true }).catch(() => undefined);
1404
+ throw error;
1405
+ }
1406
+ }
1407
+ function parseArtifactJson(bytes, label) {
1408
+ try {
1409
+ return JSON.parse(bytes);
1410
+ }
1411
+ catch (error) {
1412
+ throw new ArtifactStoreCorruptionError(`${label} JSON is corrupt`, error instanceof Error ? { cause: error } : undefined);
1413
+ }
1414
+ }
1415
+ async function syncArtifactDirectory(path) {
1416
+ const handle = await open(path, "r");
1417
+ try {
1418
+ await handle.sync();
1419
+ }
1420
+ finally {
1421
+ await handle.close();
1422
+ }
1423
+ }
1424
+ function artifactLockDelay(milliseconds) {
1425
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
1426
+ }
1427
+ export class InMemoryArtifactStore extends FileNativeArtifactStore {
1428
+ constructor() {
1429
+ super(join(tmpdir(), "narraeon-ephemeral-artifacts", randomUUID()));
1430
+ }
1431
+ }
1432
+ function findDeclaration(context, output) {
1433
+ return context.declarations.find(({ name }) => name === output) ?? null;
1434
+ }
1435
+ function normalizePayload(declaration, input) {
1436
+ if (declaration.contentType !== "application/json") {
1437
+ if (typeof input !== "string")
1438
+ return {
1439
+ ok: false,
1440
+ message: `${declaration.name} ${declaration.contentType} payload must be a string`,
1441
+ };
1442
+ return { ok: true, value: input };
1443
+ }
1444
+ if (!isJsonValue(input))
1445
+ return {
1446
+ ok: false,
1447
+ message: "JSON artifact payload must be a valid JSON value",
1448
+ };
1449
+ try {
1450
+ JSON.stringify(input);
1451
+ }
1452
+ catch {
1453
+ return { ok: false, message: "JSON artifact payload cannot be serialized" };
1454
+ }
1455
+ const contract = validatePlayPresetArtifactPayload(declaration.payloadContract, input);
1456
+ if (!contract.ok)
1457
+ return contract;
1458
+ return { ok: true, value: structuredClone(input) };
1459
+ }
1460
+ function isJsonValue(value) {
1461
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1462
+ return true;
1463
+ if (typeof value === "number")
1464
+ return Number.isFinite(value);
1465
+ if (Array.isArray(value))
1466
+ return value.every(isJsonValue);
1467
+ if (typeof value !== "object")
1468
+ return false;
1469
+ return Object.values(value).every(isJsonValue);
1470
+ }
1471
+ function byteLength(payload, contentType) {
1472
+ return Buffer.byteLength(payloadText(payload, contentType), "utf8");
1473
+ }
1474
+ function payloadText(payload, contentType) {
1475
+ if (contentType === "application/json")
1476
+ return JSON.stringify(payload);
1477
+ return typeof payload === "string" ? payload : JSON.stringify(payload);
1478
+ }
1479
+ function payloadFingerprint(output, payload) {
1480
+ return `sha256:${createHash("sha256").update(JSON.stringify({ output, payload })).digest("hex")}`;
1481
+ }
1482
+ function artifactCallFingerprint(toolName, context, parameters) {
1483
+ return `sha256:${createHash("sha256")
1484
+ .update(stableSerialize({
1485
+ toolName,
1486
+ operationId: context.operationId,
1487
+ worldId: context.worldId,
1488
+ parentHead: context.parentHead,
1489
+ playPresetId: context.playPresetId,
1490
+ playPresetRevision: context.playPresetRevision,
1491
+ playPresetScriptsEnabled: context.playPresetScriptsEnabled,
1492
+ requestId: context.requestId,
1493
+ requestAttempt: context.requestAttempt,
1494
+ parameters,
1495
+ }))
1496
+ .digest("hex")}`;
1497
+ }
1498
+ function stableSerialize(value) {
1499
+ if (value === null || typeof value !== "object")
1500
+ return JSON.stringify(value);
1501
+ if (Array.isArray(value))
1502
+ return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
1503
+ return `{${Object.keys(value)
1504
+ .sort()
1505
+ .map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
1506
+ .join(",")}}`;
1507
+ }
1508
+ function rawRecordFingerprint(record) {
1509
+ const content = { ...record };
1510
+ delete content.recordFingerprint;
1511
+ return `sha256:${createHash("sha256").update(JSON.stringify(content)).digest("hex")}`;
1512
+ }
1513
+ function sameEventIdentity(left, right) {
1514
+ return (left.kind === right.kind &&
1515
+ left.operationId === right.operationId &&
1516
+ left.recordId === right.recordId &&
1517
+ left.output === right.output &&
1518
+ left.channel === right.channel &&
1519
+ left.key === right.key &&
1520
+ left.head === right.head &&
1521
+ left.requestId === right.requestId &&
1522
+ left.requestAttempt === right.requestAttempt &&
1523
+ left.reason === right.reason &&
1524
+ left.toolCallId === right.toolCallId &&
1525
+ left.toolName === right.toolName &&
1526
+ left.callFingerprint === right.callFingerprint);
1527
+ }
1528
+ function artifactFailure(message) {
1529
+ return { ok: false, markdown: `# Runtime artifact rejected\n\n${message}` };
1530
+ }
1531
+ function isOpenExtension(operation) {
1532
+ return (operation.status === "core_committed" &&
1533
+ ["not_started", "running", "unknown", "recovery_required"].includes(operation.extensionStatus));
1534
+ }
1535
+ function isOpenProjectionOperation(operation) {
1536
+ return operation.status === "running" || isOpenExtension(operation);
1537
+ }
1538
+ function isTerminalExtensionStatus(status) {
1539
+ return [
1540
+ "completed",
1541
+ "failed",
1542
+ "cancelled",
1543
+ "budget_exceeded",
1544
+ "superseded",
1545
+ ].includes(status);
1546
+ }
1547
+ function isStaleOperation(operation) {
1548
+ return (operation === null ||
1549
+ operation.artifactProjectionStatus === "superseded" ||
1550
+ operation.status === "core_failed" ||
1551
+ operation.status === "completed" ||
1552
+ operation.extensionStatus === "superseded" ||
1553
+ operation.extensionStatus === "unknown" ||
1554
+ operation.extensionStatus === "recovery_required" ||
1555
+ operation.extensionStatus === "failed" ||
1556
+ operation.extensionStatus === "cancelled" ||
1557
+ operation.extensionStatus === "budget_exceeded");
1558
+ }
1559
+ function isActiveInteraction(operation) {
1560
+ return (operation !== undefined &&
1561
+ (operation.status === "running" ||
1562
+ (operation.status === "core_committed" &&
1563
+ operation.extensionStatus === "running")));
1564
+ }
1565
+ function identityHash(value) {
1566
+ return createHash("sha256").update(value, "utf8").digest("hex");
1567
+ }
1568
+ function artifactSequenceFileName(sequence) {
1569
+ return `${String(sequence).padStart(12, "0")}.json`;
1570
+ }
1571
+ function summary(operation) {
1572
+ return {
1573
+ operationId: operation.context.operationId,
1574
+ status: operation.extensionStatus,
1575
+ ...(operation.message === undefined ? {} : { message: operation.message }),
1576
+ completedRequests: [...operation.completedRequests],
1577
+ coreCommitted: operation.status === "core_materialization_pending" ||
1578
+ operation.status === "core_committed" ||
1579
+ operation.status === "completed",
1580
+ ...(operation.head === undefined ? {} : { head: operation.head }),
1581
+ };
1582
+ }
1583
+ function selectProjection(active) {
1584
+ const append = [];
1585
+ const replace = new Map();
1586
+ const upsert = new Map();
1587
+ for (const item of active.sort((left, right) => left.record.sequence - right.record.sequence)) {
1588
+ if (item.record.projection === "append" ||
1589
+ item.record.projection === "transient")
1590
+ append.push(item);
1591
+ else if (item.record.projection === "replace")
1592
+ replace.set(item.record.channel, item);
1593
+ else if (item.record.projection === "upsert")
1594
+ upsert.set(`${item.record.channel}\0${item.record.key ?? ""}`, item);
1595
+ }
1596
+ return [...append, ...replace.values(), ...upsert.values()].sort((left, right) => left.record.sequence - right.record.sequence);
1597
+ }
1598
+ function assertContext(context) {
1599
+ if (!isRecord(context) ||
1600
+ !hasExactKeys(context, [
1601
+ "worldId",
1602
+ "parentHead",
1603
+ "operationId",
1604
+ "playPresetId",
1605
+ "playPresetRevision",
1606
+ "playPresetScriptsEnabled",
1607
+ ]))
1608
+ throw new ArtifactStoreInvariantError("Artifact operation context has an invalid structure");
1609
+ for (const [key, value] of Object.entries({
1610
+ worldId: context.worldId,
1611
+ parentHead: context.parentHead,
1612
+ operationId: context.operationId,
1613
+ playPresetId: context.playPresetId,
1614
+ playPresetRevision: context.playPresetRevision,
1615
+ }))
1616
+ if (typeof value !== "string" || value.trim() === "")
1617
+ throw new ArtifactStoreInvariantError(`Artifact ${key} is invalid`);
1618
+ if (typeof context.playPresetScriptsEnabled !== "boolean")
1619
+ throw new ArtifactStoreInvariantError("Artifact playPresetScriptsEnabled is invalid");
1620
+ }
1621
+ function normalizeOperationContext(context) {
1622
+ return structuredClone(context);
1623
+ }
1624
+ function assertRequestContext(context) {
1625
+ assertContext({
1626
+ worldId: context.worldId,
1627
+ parentHead: context.parentHead,
1628
+ operationId: context.operationId,
1629
+ playPresetId: context.playPresetId,
1630
+ playPresetRevision: context.playPresetRevision,
1631
+ playPresetScriptsEnabled: context.playPresetScriptsEnabled,
1632
+ });
1633
+ if (context.requestId.trim() === "" ||
1634
+ !Number.isInteger(context.requestAttempt))
1635
+ throw new ArtifactStoreInvariantError("Artifact request attempt is invalid");
1636
+ if (context.requestAttempt < 1 || context.maxArtifactBytes < 0)
1637
+ throw new ArtifactStoreInvariantError("Artifact request-attempt budget is invalid");
1638
+ }
1639
+ function sameContext(left, right) {
1640
+ return (left.worldId === right.worldId &&
1641
+ left.parentHead === right.parentHead &&
1642
+ left.operationId === right.operationId &&
1643
+ left.playPresetId === right.playPresetId &&
1644
+ left.playPresetRevision === right.playPresetRevision &&
1645
+ left.playPresetScriptsEnabled === right.playPresetScriptsEnabled);
1646
+ }
1647
+ function assertRawRecord(record) {
1648
+ if (!isRecord(record) ||
1649
+ !hasExactKeys(record, [
1650
+ "schemaVersion",
1651
+ "recordId",
1652
+ "sequence",
1653
+ "worldId",
1654
+ "parentHead",
1655
+ "newHead",
1656
+ "operationId",
1657
+ "playPresetId",
1658
+ "playPresetRevision",
1659
+ "playPresetScriptsEnabled",
1660
+ "requestId",
1661
+ "requestAttempt",
1662
+ "output",
1663
+ "channel",
1664
+ "contentType",
1665
+ "payload",
1666
+ "save",
1667
+ "projection",
1668
+ "invalidation",
1669
+ "toolCallId",
1670
+ "toolName",
1671
+ "callFingerprint",
1672
+ "payloadFingerprint",
1673
+ "recordFingerprint",
1674
+ "status",
1675
+ ], ["key", "renderer", "rendererRevision"]) ||
1676
+ record.schemaVersion !== 1 ||
1677
+ !isNonEmptyString(record.recordId) ||
1678
+ !Number.isSafeInteger(record.sequence) ||
1679
+ record.sequence < 1 ||
1680
+ !isNonEmptyString(record.worldId) ||
1681
+ !isNonEmptyString(record.operationId) ||
1682
+ !isNonEmptyString(record.output) ||
1683
+ !isNonEmptyString(record.channel) ||
1684
+ !isNonEmptyString(record.parentHead) ||
1685
+ !isNonEmptyString(record.playPresetId) ||
1686
+ !isNonEmptyString(record.playPresetRevision) ||
1687
+ !isNonEmptyString(record.requestId) ||
1688
+ !Number.isSafeInteger(record.requestAttempt) ||
1689
+ record.requestAttempt < 1 ||
1690
+ record.newHead !== null ||
1691
+ typeof record.playPresetScriptsEnabled !== "boolean" ||
1692
+ !["text/plain", "text/markdown", "application/json", "text/html"].includes(record.contentType) ||
1693
+ !["none", "operation", "commit"].includes(record.save) ||
1694
+ !["append", "replace", "upsert", "transient", "hidden"].includes(record.projection) ||
1695
+ ![
1696
+ "new_operation",
1697
+ "head_change",
1698
+ "operation_end",
1699
+ "explicit_clear",
1700
+ "never",
1701
+ ].includes(record.invalidation) ||
1702
+ typeof record.toolCallId !== "string" ||
1703
+ record.toolName !== "artifact_emit" ||
1704
+ !isSha256(record.callFingerprint) ||
1705
+ !isSha256(record.payloadFingerprint) ||
1706
+ !isSha256(record.recordFingerprint) ||
1707
+ (record.renderer === undefined) !==
1708
+ (record.rendererRevision === undefined) ||
1709
+ (record.renderer !== undefined && !isNonEmptyString(record.renderer)) ||
1710
+ (record.rendererRevision !== undefined &&
1711
+ !isNonEmptyString(record.rendererRevision)) ||
1712
+ (record.projection === "upsert"
1713
+ ? !isNonEmptyString(record.key)
1714
+ : record.key !== undefined) ||
1715
+ record.status !== "pending")
1716
+ throw new ArtifactStoreCorruptionError("Artifact raw record is corrupt");
1717
+ if (!isJsonValue(record.payload))
1718
+ throw new ArtifactStoreCorruptionError("Artifact payload is not a JSON value");
1719
+ if (record.payloadFingerprint !==
1720
+ payloadFingerprint(record.output, record.payload))
1721
+ throw new ArtifactStoreCorruptionError("Artifact payload fingerprint does not match");
1722
+ if (record.recordFingerprint !== rawRecordFingerprint(record))
1723
+ throw new ArtifactStoreCorruptionError("Artifact raw-record fingerprint does not match");
1724
+ }
1725
+ function assertOperation(operation) {
1726
+ const context = operation.context;
1727
+ if (!isRecord(operation) ||
1728
+ !hasExactKeys(operation, [
1729
+ "schemaVersion",
1730
+ "context",
1731
+ "status",
1732
+ "extensionStatus",
1733
+ "completedRequests",
1734
+ "artifactProjectionStatus",
1735
+ "requestAttempts",
1736
+ ], ["message", "head"]) ||
1737
+ operation.schemaVersion !== 1 ||
1738
+ !isRecord(context) ||
1739
+ !hasExactKeys(context, [
1740
+ "worldId",
1741
+ "parentHead",
1742
+ "operationId",
1743
+ "playPresetId",
1744
+ "playPresetRevision",
1745
+ "playPresetScriptsEnabled",
1746
+ ]) ||
1747
+ !isNonEmptyString(context.worldId) ||
1748
+ !isNonEmptyString(context.parentHead) ||
1749
+ !isNonEmptyString(context.operationId) ||
1750
+ !isNonEmptyString(context.playPresetId) ||
1751
+ !isNonEmptyString(context.playPresetRevision) ||
1752
+ typeof context.playPresetScriptsEnabled !== "boolean" ||
1753
+ ![
1754
+ "running",
1755
+ "core_failed",
1756
+ "core_materialization_pending",
1757
+ "core_committed",
1758
+ "completed",
1759
+ ].includes(operation.status) ||
1760
+ ![
1761
+ "not_started",
1762
+ "running",
1763
+ "completed",
1764
+ "failed",
1765
+ "cancelled",
1766
+ "budget_exceeded",
1767
+ "unknown",
1768
+ "recovery_required",
1769
+ "superseded",
1770
+ ].includes(operation.extensionStatus) ||
1771
+ !Array.isArray(operation.completedRequests) ||
1772
+ !operation.completedRequests.every(isNonEmptyString) ||
1773
+ (operation.message !== undefined &&
1774
+ typeof operation.message !== "string") ||
1775
+ !validArtifactOperationState(operation) ||
1776
+ !["active", "superseded"].includes(operation.artifactProjectionStatus) ||
1777
+ !isRecord(operation.requestAttempts) ||
1778
+ !Object.entries(operation.requestAttempts).every(([request, attempt]) => request.trim() !== "" &&
1779
+ typeof attempt === "number" &&
1780
+ Number.isSafeInteger(attempt) &&
1781
+ attempt >= 1))
1782
+ throw new ArtifactStoreCorruptionError("Artifact operation state is corrupt");
1783
+ }
1784
+ function validArtifactOperationState(operation) {
1785
+ if (operation.status === "running")
1786
+ return (operation.extensionStatus === "not_started" &&
1787
+ operation.completedRequests.length === 0 &&
1788
+ operation.message === undefined &&
1789
+ operation.head === undefined);
1790
+ if (operation.status === "core_failed")
1791
+ return (operation.extensionStatus === "failed" &&
1792
+ operation.completedRequests.length === 0 &&
1793
+ operation.artifactProjectionStatus === "superseded" &&
1794
+ operation.head === undefined);
1795
+ if (operation.status === "core_materialization_pending")
1796
+ return (operation.extensionStatus === "recovery_required" &&
1797
+ operation.completedRequests.length === 0 &&
1798
+ operation.artifactProjectionStatus === "active" &&
1799
+ isNonEmptyString(operation.head) &&
1800
+ isNonEmptyString(operation.message));
1801
+ if (operation.status === "completed")
1802
+ return (operation.extensionStatus === "completed" &&
1803
+ isNonEmptyString(operation.head) &&
1804
+ operation.message === undefined);
1805
+ if (!isNonEmptyString(operation.head))
1806
+ return false;
1807
+ if (operation.extensionStatus === "completed")
1808
+ return false;
1809
+ if (operation.extensionStatus === "not_started" ||
1810
+ operation.extensionStatus === "running")
1811
+ return (operation.completedRequests.length === 0 &&
1812
+ operation.message === undefined);
1813
+ return (isNonEmptyString(operation.message) &&
1814
+ (operation.extensionStatus !== "superseded" ||
1815
+ operation.artifactProjectionStatus === "superseded"));
1816
+ }
1817
+ function assertEvent(event) {
1818
+ if (!isRecord(event) ||
1819
+ event.schemaVersion !== 1 ||
1820
+ !Number.isSafeInteger(event.sequence) ||
1821
+ event.sequence < 1 ||
1822
+ !isNonEmptyString(event.operationId))
1823
+ throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
1824
+ const requestPair = event.requestId === undefined && event.requestAttempt === undefined
1825
+ ? true
1826
+ : isNonEmptyString(event.requestId) &&
1827
+ Number.isSafeInteger(event.requestAttempt) &&
1828
+ Number(event.requestAttempt) >= 1;
1829
+ if (event.kind === "activate") {
1830
+ if (!hasExactKeys(event, ["schemaVersion", "kind", "sequence", "operationId", "recordId"], ["requestId", "requestAttempt"]) ||
1831
+ !isNonEmptyString(event.recordId) ||
1832
+ !requestPair)
1833
+ throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
1834
+ return;
1835
+ }
1836
+ if (event.kind === "bind_head") {
1837
+ if (!hasExactKeys(event, [
1838
+ "schemaVersion",
1839
+ "kind",
1840
+ "sequence",
1841
+ "operationId",
1842
+ "recordId",
1843
+ "head",
1844
+ ]) ||
1845
+ !isNonEmptyString(event.recordId) ||
1846
+ !isNonEmptyString(event.head))
1847
+ throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
1848
+ return;
1849
+ }
1850
+ if (event.kind === "supersede") {
1851
+ if (!hasExactKeys(event, [
1852
+ "schemaVersion",
1853
+ "kind",
1854
+ "sequence",
1855
+ "operationId",
1856
+ "recordId",
1857
+ "reason",
1858
+ ], ["requestId", "requestAttempt"]) ||
1859
+ !isNonEmptyString(event.recordId) ||
1860
+ !isNonEmptyString(event.reason) ||
1861
+ !requestPair)
1862
+ throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
1863
+ return;
1864
+ }
1865
+ if (event.kind === "clear" || event.kind === "ignored_call") {
1866
+ if (!hasExactKeys(event, [
1867
+ "schemaVersion",
1868
+ "kind",
1869
+ "sequence",
1870
+ "operationId",
1871
+ "output",
1872
+ "channel",
1873
+ "reason",
1874
+ "toolCallId",
1875
+ "toolName",
1876
+ "callFingerprint",
1877
+ "requestId",
1878
+ "requestAttempt",
1879
+ ], ["key"]) ||
1880
+ !isNonEmptyString(event.output) ||
1881
+ !isNonEmptyString(event.channel) ||
1882
+ !isNonEmptyString(event.reason) ||
1883
+ !isNonEmptyString(event.toolCallId) ||
1884
+ event.toolName !== "artifact_clear" ||
1885
+ !isSha256(event.callFingerprint) ||
1886
+ !isNonEmptyString(event.requestId) ||
1887
+ !Number.isSafeInteger(event.requestAttempt) ||
1888
+ Number(event.requestAttempt) < 1 ||
1889
+ (event.key !== undefined && !isNonEmptyString(event.key)))
1890
+ throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
1891
+ return;
1892
+ }
1893
+ throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
1894
+ }
1895
+ function isRecord(value) {
1896
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1897
+ }
1898
+ function hasExactKeys(value, required, optional = []) {
1899
+ const allowed = new Set([...required, ...optional]);
1900
+ return (required.every((key) => Object.hasOwn(value, key)) &&
1901
+ Object.keys(value).every((key) => allowed.has(key)));
1902
+ }
1903
+ function isNonEmptyString(value) {
1904
+ return typeof value === "string" && value.trim() !== "";
1905
+ }
1906
+ function isSha256(value) {
1907
+ return typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value);
1908
+ }
1909
+ function isMissing(error) {
1910
+ return isRecord(error) && error.code === "ENOENT";
1911
+ }
1912
+ function isAlreadyExists(error) {
1913
+ return isRecord(error) && error.code === "EEXIST";
1914
+ }