narraeon 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +11 -0
  2. package/README.zh-CN.md +8 -0
  3. package/dist/node/protocol/v1.js +0 -2
  4. package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
  5. package/dist/node/runtime/V1Runtime.js +1 -5
  6. package/dist/node/runtime/archive/WorldArchives.js +14 -46
  7. package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
  8. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
  9. package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
  10. package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
  11. package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
  12. package/dist/node/runtime/config/dataRootLock.js +108 -0
  13. package/dist/node/runtime/content/ContentWorkspace.js +0 -10
  14. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
  15. package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
  16. package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
  17. package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
  18. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
  20. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
  21. package/dist/node/runtime/play/PlayCallChain.js +338 -366
  22. package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
  23. package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
  24. package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
  25. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +3 -15
  26. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
  27. package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
  28. package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
  29. package/dist/node/runtime/world/WorldActivity.js +14 -22
  30. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
  31. package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
  32. package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
  33. package/dist/node/runtime/world/WorldQueue.js +46 -0
  34. package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
  35. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +0 -12
  36. package/dist/node/server/mobile.js +20 -0
  37. package/dist/node/server/startWebServer.js +15 -8
  38. package/dist/web/assets/{index-BgBL8G-A.js → index-7GzLSUpU.js} +2 -2
  39. package/dist/web/index.html +1 -1
  40. package/package.json +2 -1
  41. package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
@@ -3,201 +3,62 @@ import { isWorldExtensionControl, } from "../../protocol/worldExtensions.js";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
- import { mkdir, open, readdir, readFile, readlink, rename, rm, stat, } from "node:fs/promises";
7
- import { dirname, join, resolve } from "node:path";
6
+ import { readdir, readFile, rm } from "node:fs/promises";
7
+ import { join, resolve } from "node:path";
8
8
  import { tmpdir } from "node:os";
9
- import { validatePlayPresetArtifactPayload, isFrozenArtifactPresentation, } from "../play/FileNativePlayPresetStore.js";
10
- export class ArtifactStoreInvariantError extends Error {
11
- constructor(message, options) {
12
- super(message, options);
13
- this.name = "ArtifactStoreInvariantError";
14
- }
15
- }
16
- export class ArtifactStoreCorruptionError extends ArtifactStoreInvariantError {
17
- constructor(message, options) {
18
- super(message, options);
19
- this.name = "ArtifactStoreCorruptionError";
20
- }
21
- }
9
+ import { worldQueueFor } from "../world/WorldQueue.js";
10
+ import { validatePlayPresetArtifactPayload, } from "../play/FileNativePlayPresetStore.js";
11
+ import { ArtifactStoreCorruptionError, ArtifactStoreInvariantError, artifactSequenceFileName, assertOperation, assertRawRecord, hasExactKeys, identityHash, isJsonValue, isMissing, isRecord, parseArtifactJson, payloadFingerprint, rawRecordFingerprint, stableSerialize, validAttachment, headOrdinal, writeArtifactJson, } from "./ArtifactStoreFiles.js";
12
+ import { hasLegacyArtifactState, migrateLegacyArtifactStore, } from "./LegacyArtifactStoreMigration.js";
13
+ export { ArtifactStoreCorruptionError, ArtifactStoreInvariantError };
22
14
  const maxPayloadBytes = 4 * 1024 * 1024;
23
15
  /**
24
- * File-native artifact module. Raw records never change after publication;
25
- * projection events and operation state are the only mutable layers. The
26
- * public interface deliberately exposes no filesystem paths or provider
27
- * transcript and is the seam used by the play-call-chain coordinator and tests.
16
+ * File-native artifact module. Artifacts live in the world directory as
17
+ * `artifacts/records/<sequence>.json` and `artifacts/operations/<hash>.json`.
18
+ * A record carries its own status and head and is updated in place; there is
19
+ * no event log. Forks and archives copy the world directory and then rewrite
20
+ * identities with {@link forkArtifactClosure}.
28
21
  */
29
22
  export class FileNativeArtifactStore {
30
- #root;
31
- #extensionControls;
23
+ #dataRoot;
32
24
  #worldsRoot;
25
+ #extensionControls;
26
+ /** Records with `save: "none"` are never written to disk. */
33
27
  #memoryRecords = new Map();
34
- #memoryEvents = new Map();
35
28
  #mutationContext = new AsyncLocalStorage();
36
29
  constructor(dataRoot) {
37
- this.#root = join(resolve(dataRoot), "artifact-store");
38
- this.#extensionControls = new WorldExtensionRequests(join(resolve(dataRoot), "worlds-file-native"));
39
- this.#worldsRoot = join(resolve(dataRoot), "worlds-file-native");
30
+ this.#dataRoot = resolve(dataRoot);
31
+ this.#worldsRoot = join(this.#dataRoot, "worlds-file-native");
32
+ this.#extensionControls = new WorldExtensionRequests(this.#worldsRoot);
40
33
  }
41
- /** Control acceptance and artifact publication share one persistent lock.
42
- * Callers holding the Authority lock always acquire it before this lock. */
34
+ /** Control acceptance and artifact publication share the in-process world queue. */
43
35
  static withWorldControlsMutation(dataRoot, worldId, action) {
44
- return withArtifactWorldLock(artifactWorldLockPath(dataRoot, worldId), action);
45
- }
46
- /** The immutable closure publishes atomically with the derived world. */
47
- async stageFork(input) {
48
- await this.#withWorldMutation(input.sourceWorldId, async () => {
49
- const projection = await this.readActiveProjection(input.sourceWorldId, undefined, input.timeline);
50
- const retained = new Set(projection
51
- .filter((item) => item.save === "commit" && item.attachment !== undefined)
52
- .map((item) => item.recordId));
53
- const effective = (await this.#effectiveRecords(input.sourceWorldId)).filter(({ record }) => retained.has(record.recordId));
54
- const operations = [];
55
- const records = [];
56
- const events = [];
57
- let sequence = Math.max(0, ...effective.map(({ record }) => record.sequence));
58
- for (const { record, head } of effective) {
59
- const source = (await this.#readOperation(record.operationId));
60
- const operationId = `artifact-fork:${input.targetWorldId}:${identityHash(record.operationId)}`;
61
- if (!operations.some((operation) => operation.context.operationId === operationId)) {
62
- operations.push({
63
- ...source,
64
- context: {
65
- ...source.context,
66
- worldId: input.targetWorldId,
67
- operationId,
68
- },
69
- });
70
- }
71
- const copy = {
72
- ...record,
73
- worldId: input.targetWorldId,
74
- operationId,
75
- sequence: record.sequence,
76
- };
77
- copy.recordFingerprint = rawRecordFingerprint(copy);
78
- records.push(copy);
79
- events.push({
80
- schemaVersion: 1,
81
- kind: "activate",
82
- sequence: ++sequence,
83
- operationId,
84
- recordId: copy.recordId,
85
- });
86
- events.push({
87
- schemaVersion: 1,
88
- kind: "bind_head",
89
- sequence: ++sequence,
90
- operationId,
91
- recordId: copy.recordId,
92
- head: head,
93
- });
94
- }
95
- const snapshot = {
96
- schemaVersion: 1,
97
- worldId: input.targetWorldId,
98
- operations,
99
- records,
100
- events,
101
- };
102
- await mkdir(join(input.targetWorldRoot, "runtime"), { recursive: true });
103
- await writeDurableJson(join(input.targetWorldRoot, "runtime", "artifact-fork.json"), snapshot, true);
104
- });
36
+ return worldQueueFor(resolve(dataRoot)).run(worldId, action);
105
37
  }
106
- async #ensureForkRestored(worldId) {
107
- if (dirname(resolve(this.#worldsRoot, worldId)) !== this.#worldsRoot)
108
- return;
109
- try {
110
- await stat(join(this.#worldRoot(worldId), "fork-restored.json"));
111
- return;
112
- }
113
- catch (error) {
114
- if (!isMissing(error))
115
- throw error;
116
- }
117
- try {
118
- await stat(join(this.#worldsRoot, worldId, "runtime", "artifact-fork.json"));
119
- }
120
- catch (error) {
121
- if (isMissing(error))
122
- return;
123
- throw error;
124
- }
125
- await this.#withWorldMutation(worldId, () => Promise.resolve());
126
- }
127
- async #restoreFork(worldId) {
128
- if (dirname(resolve(this.#worldsRoot, worldId)) !== this.#worldsRoot)
129
- return;
130
- const marker = join(this.#worldRoot(worldId), "fork-restored.json");
131
- try {
132
- await readFile(marker);
133
- return;
134
- }
135
- catch (error) {
136
- if (!isMissing(error))
137
- throw error;
138
- }
139
- let snapshot;
140
- try {
141
- snapshot = parseArtifactJson(await readFile(join(this.#worldsRoot, worldId, "runtime", "artifact-fork.json"), "utf8"), "artifact fork");
142
- }
143
- catch (error) {
144
- if (isMissing(error))
38
+ async finishRequest(context, failed) {
39
+ await this.#withWorldMutation(context.worldId, async () => {
40
+ const operation = await this.#readOperation(context.worldId, context.operationId);
41
+ if (operation?.requestAttempts[context.requestId] !== context.requestAttempt)
145
42
  return;
146
- throw error;
147
- }
148
- if (!isRecord(snapshot) ||
149
- !hasExactKeys(snapshot, [
150
- "schemaVersion",
151
- "worldId",
152
- "operations",
153
- "records",
154
- "events",
155
- ]) ||
156
- snapshot.schemaVersion !== 1 ||
157
- snapshot.worldId !== worldId ||
158
- !Array.isArray(snapshot.operations) ||
159
- !Array.isArray(snapshot.records) ||
160
- !Array.isArray(snapshot.events))
161
- throw new ArtifactStoreCorruptionError("Artifact fork snapshot is invalid");
162
- for (const operation of snapshot.operations) {
163
- assertOperation(operation);
164
- if (operation.context.worldId !== worldId)
165
- throw new ArtifactStoreCorruptionError("Artifact fork world mismatch");
166
- }
167
- for (const record of snapshot.records) {
168
- assertRawRecord(record);
169
- if (record.worldId !== worldId ||
170
- !snapshot.operations.some((operation) => operation.context.operationId === record.operationId))
171
- throw new ArtifactStoreCorruptionError("Artifact fork record mismatch");
172
- }
173
- for (const event of snapshot.events) {
174
- assertEvent(event);
175
- if (!snapshot.records.some((record) => record.recordId === event.recordId &&
176
- record.operationId === event.operationId))
177
- throw new ArtifactStoreCorruptionError("Artifact fork event mismatch");
178
- }
179
- // No mutation can start until this idempotent restore finishes under the world lock.
180
- for (const operation of snapshot.operations)
181
- await this.#writeOperation(operation);
182
- for (const record of snapshot.records)
183
- await writeDurableJson(join(this.#worldRoot(worldId), "records", artifactSequenceFileName(record.sequence)), record, false);
184
- for (const event of snapshot.events)
185
- await writeDurableJson(join(this.#worldRoot(worldId), "events", artifactSequenceFileName(event.sequence)), event, false);
186
- await mkdir(this.#worldRoot(worldId), { recursive: true });
187
- await writeDurableJson(marker, { schemaVersion: 1 }, false);
43
+ await this.#writeOperation({
44
+ ...operation,
45
+ requests: (operation.requests ?? []).map((item) => item.requestId === context.requestId
46
+ ? { ...item, status: failed ? "failed" : "completed" }
47
+ : item),
48
+ });
49
+ });
188
50
  }
189
51
  async beginOperation(context) {
190
52
  return this.#withWorldMutation(context.worldId, () => this.#beginOperation(context));
191
53
  }
192
54
  async #beginOperation(context) {
193
55
  assertContext(context);
194
- const existing = await this.#readOperation(context.operationId);
56
+ const existing = await this.#readOperation(context.worldId, context.operationId);
195
57
  if (existing !== null && !sameContext(existing.context, context))
196
58
  throw new ArtifactStoreInvariantError("Artifact operation is bound to a different frozen payload");
197
59
  // A direct orchestrator/store caller must not rely on a later UI read to
198
- // invalidate artifacts bound to the parent head. This call is safe on a
199
- // retry because projection events are appended idempotently below.
200
- await this.reconcileHead(context.worldId, context.parentHead, context.operationId);
60
+ // invalidate artifacts bound to the parent head.
61
+ await this.#reconcileHead(context.worldId, context.parentHead, context.operationId);
201
62
  const priorOperations = await this.#operationsForWorld(context.worldId);
202
63
  if (existing === null)
203
64
  await this.#writeOperation({
@@ -209,31 +70,24 @@ export class FileNativeArtifactStore {
209
70
  artifactProjectionStatus: "active",
210
71
  requestAttempts: {},
211
72
  });
212
- // Operation state is indexed independently of raw records. This closes the
213
- // no-artifact gap: a running/unknown extension is superseded even when it
214
- // has not emitted anything yet. Its execution status is retained unless it
215
- // is an actually open extension, while projection invalidation is tracked
216
- // separately for late raw results.
217
- const prior = await this.#effectiveRecords(context.worldId);
73
+ // An open extension is superseded even when it has not emitted anything
74
+ // yet; its execution status is kept unless it is actually open.
75
+ const records = await this.#records(context.worldId);
218
76
  for (const priorOperation of priorOperations) {
219
77
  if (priorOperation.context.operationId === context.operationId)
220
78
  continue;
221
79
  const openExtension = isOpenExtension(priorOperation);
222
80
  const operationAlreadySuperseded = priorOperation.artifactProjectionStatus === "superseded";
223
- const priorRecords = prior.filter(({ record, status }) => record.operationId === priorOperation.context.operationId &&
224
- status === "active");
225
- const invalidatedRecords = priorRecords.filter(({ record }) => openExtension ||
226
- operationAlreadySuperseded ||
227
- record.invalidation === "new_operation");
228
- for (const item of invalidatedRecords)
229
- await this.#appendEventOnce({
230
- schemaVersion: 1,
231
- kind: "supersede",
232
- operationId: item.record.operationId,
233
- recordId: item.record.recordId,
234
- reason: "new_operation",
235
- sequence: await this.#nextSequence(context.worldId),
236
- });
81
+ for (const record of records) {
82
+ if (record.operationId !== priorOperation.context.operationId ||
83
+ record.status !== "active")
84
+ continue;
85
+ if (!(openExtension ||
86
+ operationAlreadySuperseded ||
87
+ record.invalidation === "new_operation"))
88
+ continue;
89
+ await this.#putRecord({ ...record, status: "superseded" });
90
+ }
237
91
  if (openExtension) {
238
92
  const next = {
239
93
  ...priorOperation,
@@ -241,8 +95,7 @@ export class FileNativeArtifactStore {
241
95
  extensionStatus: "superseded",
242
96
  message: "This extension was superseded by a newer operation",
243
97
  };
244
- if (JSON.stringify(next) !== JSON.stringify(priorOperation))
245
- await this.#writeOperation(next);
98
+ await this.#writeOperation(next);
246
99
  }
247
100
  }
248
101
  }
@@ -251,7 +104,7 @@ export class FileNativeArtifactStore {
251
104
  }
252
105
  async #beginRequestAttempt(context) {
253
106
  assertRequestContext(context);
254
- const operation = await this.#readOperation(context.operationId);
107
+ const operation = await this.#readOperation(context.worldId, context.operationId);
255
108
  if (operation === null)
256
109
  throw new ArtifactStoreInvariantError("Artifact request attempt is missing an operation checkpoint");
257
110
  if (!sameContext(operation.context, context))
@@ -267,7 +120,7 @@ export class FileNativeArtifactStore {
267
120
  ? []
268
121
  : [context.frozenResources.mount])),
269
122
  ];
270
- operation.requests = [
123
+ const requests = [
271
124
  ...(operation.requests ?? []).filter((item) => item.requestId !== context.requestId),
272
125
  {
273
126
  requestId: context.requestId,
@@ -279,48 +132,63 @@ export class FileNativeArtifactStore {
279
132
  const current = operation.requestAttempts[context.requestId];
280
133
  if (current !== undefined && context.requestAttempt < current)
281
134
  return;
282
- // Persist the monotonic marker before projection events. If a process dies
283
- // between these writes, cold rebuild still treats the old attempt as
284
- // stale, and a retry below completes the missing supersede events.
285
- if (current === undefined || context.requestAttempt > current) {
135
+ if (current === undefined || context.requestAttempt > current)
286
136
  await this.#writeOperation({
287
137
  ...operation,
138
+ requests,
288
139
  requestAttempts: {
289
140
  ...operation.requestAttempts,
290
141
  [context.requestId]: context.requestAttempt,
291
142
  },
292
143
  });
144
+ // A clear made by an earlier attempt of this request is rolled back: that
145
+ // attempt did not finish, and its retry decides again.
146
+ const records = await this.#records(context.worldId);
147
+ for (const record of records) {
148
+ if (record.status === "cleared" &&
149
+ record.clearedBy?.operationId === context.operationId &&
150
+ record.clearedBy.requestId === context.requestId &&
151
+ record.clearedBy.requestAttempt < context.requestAttempt) {
152
+ await this.#putRecord({
153
+ ...record,
154
+ status: record.clearedBy.priorStatus,
155
+ });
156
+ }
293
157
  }
294
- const records = await this.#recordsForOperation(context.operationId);
295
158
  for (const record of records) {
296
- if (record.requestId !== context.requestId ||
297
- record.requestAttempt >= context.requestAttempt)
159
+ if (record.operationId !== context.operationId ||
160
+ record.requestId !== context.requestId ||
161
+ record.requestAttempt >= context.requestAttempt ||
162
+ record.status === "superseded")
298
163
  continue;
299
- const event = {
300
- schemaVersion: 1,
301
- kind: "supersede",
302
- operationId: context.operationId,
303
- recordId: record.recordId,
304
- requestId: record.requestId,
305
- requestAttempt: record.requestAttempt,
306
- reason: "request_attempt_superseded",
307
- sequence: await this.#nextSequence(context.worldId),
308
- };
309
- await this.#appendRecordEvent(record, event);
164
+ await this.#putRecord({ ...record, status: "superseded" });
310
165
  }
311
166
  }
312
- async finishRequest(context, failed) {
313
- await this.#withWorldMutation(context.worldId, async () => {
314
- const operation = await this.#readOperation(context.operationId);
315
- if (operation?.requestAttempts[context.requestId] !== context.requestAttempt)
316
- return;
317
- await this.#writeOperation({
318
- ...operation,
319
- requests: (operation.requests ?? []).map((item) => item.requestId === context.requestId
320
- ? { ...item, status: failed ? "failed" : "completed" }
321
- : item),
322
- });
323
- });
167
+ /**
168
+ * Remove the operations and records whose head is after restoredHead. A
169
+ * rewind discards those rounds, and their replies, attachments and extension
170
+ * results must not survive on disk (ADR-0045). Each artifact is one file, so
171
+ * the discard deletes files.
172
+ */
173
+ async discardAfterHead(worldId, restoredHead) {
174
+ return this.#withWorldMutation(worldId, () => this.#discardAfterHead(worldId, restoredHead));
175
+ }
176
+ async #discardAfterHead(worldId, restoredHead) {
177
+ const limit = headOrdinal(restoredHead);
178
+ const operations = await this.#operationsForWorld(worldId);
179
+ const discardedOperations = new Set(operations
180
+ .filter((operation) => headOrdinal(operation.head ?? operation.context.parentHead) > limit)
181
+ .map(({ context }) => context.operationId));
182
+ for (const record of await this.#records(worldId)) {
183
+ if (!discardedOperations.has(record.operationId) &&
184
+ headOrdinal(record.head ?? record.parentHead) <= limit)
185
+ continue;
186
+ await this.#removeRecord(record);
187
+ }
188
+ for (const operationId of discardedOperations) {
189
+ await rm(this.#operationPath(worldId, operationId), { force: true });
190
+ this.#memoryRecords.delete(operationId);
191
+ }
324
192
  }
325
193
  async reconcileHead(worldId, head, exceptOperationId) {
326
194
  return this.#withWorldMutation(worldId, () => this.#reconcileHead(worldId, head, exceptOperationId));
@@ -329,26 +197,18 @@ export class FileNativeArtifactStore {
329
197
  if (worldId.trim() === "" || head.trim() === "")
330
198
  throw new ArtifactStoreInvariantError("Artifact head-reconciliation arguments are invalid");
331
199
  const operations = await this.#operationsForWorld(worldId);
332
- const effective = await this.#effectiveRecords(worldId);
200
+ const records = await this.#records(worldId);
333
201
  const affected = new Set();
334
- for (const item of effective) {
335
- const operationId = item.record.operationId;
336
- if (operationId === exceptOperationId || item.status !== "active")
202
+ for (const record of records) {
203
+ if (record.operationId === exceptOperationId ||
204
+ record.status !== "active")
337
205
  continue;
338
- if (item.record.invalidation !== "head_change")
206
+ if (record.invalidation !== "head_change")
339
207
  continue;
340
- const boundHead = item.head ?? item.record.parentHead;
341
- if (boundHead === head)
208
+ if ((record.head ?? record.parentHead) === head)
342
209
  continue;
343
- await this.#appendEventOnce({
344
- schemaVersion: 1,
345
- kind: "supersede",
346
- operationId,
347
- recordId: item.record.recordId,
348
- reason: "head_change",
349
- sequence: await this.#nextSequence(worldId),
350
- });
351
- affected.add(operationId);
210
+ await this.#putRecord({ ...record, status: "superseded" });
211
+ affected.add(record.operationId);
352
212
  }
353
213
  for (const operation of operations) {
354
214
  const operationId = operation.context.operationId;
@@ -357,10 +217,10 @@ export class FileNativeArtifactStore {
357
217
  const boundHead = operation.head ?? operation.context.parentHead;
358
218
  if (boundHead === head)
359
219
  continue;
360
- const hasActiveHeadBoundRecord = effective.some(({ record, status, head: recordHead }) => record.operationId === operationId &&
361
- status === "active" &&
220
+ const hasActiveHeadBoundRecord = records.some((record) => record.operationId === operationId &&
221
+ record.status === "active" &&
362
222
  record.invalidation === "head_change" &&
363
- (recordHead ?? record.parentHead) !== head);
223
+ (record.head ?? record.parentHead) !== head);
364
224
  if (!hasActiveHeadBoundRecord && !isOpenProjectionOperation(operation))
365
225
  continue;
366
226
  affected.add(operationId);
@@ -397,7 +257,7 @@ export class FileNativeArtifactStore {
397
257
  return artifactFailure(payload.message);
398
258
  if (input.toolCallId.trim() === "")
399
259
  return artifactFailure("Tool-call ID cannot be empty");
400
- const operation = await this.#readOperation(input.context.operationId);
260
+ const operation = await this.#readOperation(input.context.worldId, input.context.operationId);
401
261
  if (operation === null)
402
262
  throw new ArtifactStoreInvariantError("Artifact emit is missing an operation checkpoint");
403
263
  if (!sameContext(operation.context, input.context))
@@ -407,12 +267,13 @@ export class FileNativeArtifactStore {
407
267
  throw new ArtifactStoreInvariantError("Artifact emit request attempt is not registered");
408
268
  const fingerprint = payloadFingerprint(input.output, payload.value);
409
269
  const callFingerprint = artifactCallFingerprint("artifact_emit", input.context, { output: declaration.name, payload: payload.value });
410
- const prior = await this.#recordsForOperation(input.context.operationId);
270
+ const prior = await this.#recordsForOperation(input.context.worldId, input.context.operationId);
271
+ // A tool-call ID is an identity only within one operation. A cold retry
272
+ // re-calls the model and gets new IDs (ADR-0044), so no wider ledger exists.
411
273
  const previousByCall = prior.find((record) => record.toolCallId === input.toolCallId);
412
274
  if (previousByCall !== undefined) {
413
275
  if (previousByCall.callFingerprint !== callFingerprint)
414
276
  return artifactFailure("The same artifact tool-call ID is bound to different arguments");
415
- await this.#repairRecordPublication(previousByCall, operation, currentAttempt);
416
277
  return {
417
278
  ok: true,
418
279
  idempotent: true,
@@ -420,10 +281,6 @@ export class FileNativeArtifactStore {
420
281
  markdown: `# Runtime artifact already exists\n\noutput=${input.output}`,
421
282
  };
422
283
  }
423
- const previousEvent = (await this.#events(input.context.worldId)).find((event) => event.operationId === input.context.operationId &&
424
- event.toolCallId === input.toolCallId);
425
- if (previousEvent !== undefined)
426
- return artifactFailure("The same artifact tool-call ID is bound to a different tool call");
427
284
  const staleAttempt = currentAttempt !== input.context.requestAttempt;
428
285
  const sameOutput = prior.filter((record) => record.requestId === input.context.requestId &&
429
286
  record.requestAttempt === input.context.requestAttempt &&
@@ -439,135 +296,38 @@ export class FileNativeArtifactStore {
439
296
  .reduce((total, record) => total + byteLength(record.payload, record.contentType), 0);
440
297
  if (totalBytes + payloadBytes > input.context.maxArtifactBytes)
441
298
  return artifactFailure("Follow-up request artifacts exceed maxArtifactBytes");
442
- const record = await this.#newRecord(input, declaration, payload.value, fingerprint, callFingerprint);
299
+ const sequence = await this.#nextSequence(input.context.worldId);
300
+ const base = newRecord(input, declaration, payload.value, {
301
+ sequence,
302
+ fingerprint,
303
+ callFingerprint,
304
+ });
443
305
  const stale = isStaleOperation(operation) ||
444
306
  staleAttempt ||
445
- !(await this.#extensionControls.visible(record.worldId, record));
446
- if (declaration.save === "none") {
447
- const records = this.#memoryRecords.get(input.context.operationId) ?? [];
448
- records.push(record);
449
- this.#memoryRecords.set(input.context.operationId, records);
450
- await this.#appendRecordEvent(record, {
451
- schemaVersion: 1,
452
- kind: stale ? "supersede" : "activate",
453
- sequence: await this.#nextSequence(input.context.worldId),
454
- operationId: input.context.operationId,
455
- recordId: record.recordId,
456
- requestId: record.requestId,
457
- requestAttempt: record.requestAttempt,
458
- ...(stale ? { reason: "late_superseded_operation_result" } : {}),
459
- });
460
- if (!stale &&
461
- operation?.status === "core_committed" &&
462
- operation.head !== undefined) {
463
- await this.#appendRecordEvent(record, {
464
- schemaVersion: 1,
465
- kind: "bind_head",
466
- sequence: await this.#nextSequence(input.context.worldId),
467
- operationId: input.context.operationId,
468
- recordId: record.recordId,
469
- head: operation.head,
470
- });
471
- }
472
- }
473
- else {
474
- await this.#appendRaw(record);
475
- if (stale)
476
- await this.#appendRecordEvent(record, {
477
- schemaVersion: 1,
478
- kind: "supersede",
479
- sequence: await this.#nextSequence(input.context.worldId),
480
- operationId: input.context.operationId,
481
- recordId: record.recordId,
482
- requestId: record.requestId,
483
- requestAttempt: record.requestAttempt,
484
- reason: "late_superseded_operation_result",
485
- });
486
- else if (operation?.status === "core_committed" &&
487
- operation.head !== undefined) {
488
- await this.#appendRecordEvent(record, {
489
- schemaVersion: 1,
490
- kind: "bind_head",
491
- sequence: await this.#nextSequence(input.context.worldId),
492
- operationId: input.context.operationId,
493
- recordId: record.recordId,
494
- head: operation.head,
495
- });
496
- await this.#appendRecordEvent(record, {
497
- schemaVersion: 1,
498
- kind: "activate",
499
- sequence: await this.#nextSequence(input.context.worldId),
500
- operationId: input.context.operationId,
501
- recordId: record.recordId,
502
- });
503
- }
504
- else if (declaration.save === "operation")
505
- await this.#appendRecordEvent(record, {
506
- schemaVersion: 1,
507
- kind: "activate",
508
- sequence: await this.#nextSequence(input.context.worldId),
509
- operationId: input.context.operationId,
510
- recordId: record.recordId,
511
- });
512
- }
307
+ !(await this.#extensionControls.visible(base.worldId, base));
308
+ const committed = !stale &&
309
+ operation.status === "core_committed" &&
310
+ operation.head !== undefined;
311
+ // A stale result is kept as superseded. A live result is active when its
312
+ // core committed or when it does not wait for the core; a commit-saved
313
+ // result waits as pending until markCoreCommitted binds it.
314
+ const status = stale
315
+ ? "superseded"
316
+ : committed || declaration.save !== "commit"
317
+ ? "active"
318
+ : "pending";
319
+ const record = {
320
+ ...base,
321
+ status,
322
+ head: committed ? (operation.head ?? null) : null,
323
+ };
324
+ await this.#putRecord(record);
513
325
  return {
514
326
  ok: true,
515
327
  recordId: record.recordId,
516
328
  markdown: `# Runtime artifact accepted\n\noutput=${input.output}`,
517
329
  };
518
330
  }
519
- async #repairRecordPublication(record, operation, currentAttempt) {
520
- const events = await this.#events(record.worldId);
521
- const hasEvent = (kind, head) => events.some((event) => event.kind === kind &&
522
- event.operationId === record.operationId &&
523
- event.recordId === record.recordId &&
524
- (head === undefined || event.head === head));
525
- const stale = isStaleOperation(operation) ||
526
- currentAttempt !== record.requestAttempt ||
527
- !(await this.#extensionControls.visible(record.worldId, record));
528
- if (stale) {
529
- if (!hasEvent("supersede"))
530
- await this.#appendRecordEvent(record, {
531
- schemaVersion: 1,
532
- kind: "supersede",
533
- sequence: await this.#nextSequence(record.worldId),
534
- operationId: record.operationId,
535
- recordId: record.recordId,
536
- requestId: record.requestId,
537
- requestAttempt: record.requestAttempt,
538
- reason: "late_superseded_operation_result",
539
- });
540
- return;
541
- }
542
- if (operation.status === "core_committed" && operation.head !== undefined) {
543
- if (!hasEvent("bind_head", operation.head))
544
- await this.#appendRecordEvent(record, {
545
- schemaVersion: 1,
546
- kind: "bind_head",
547
- sequence: await this.#nextSequence(record.worldId),
548
- operationId: record.operationId,
549
- recordId: record.recordId,
550
- head: operation.head,
551
- });
552
- if (!hasEvent("activate"))
553
- await this.#appendRecordEvent(record, {
554
- schemaVersion: 1,
555
- kind: "activate",
556
- sequence: await this.#nextSequence(record.worldId),
557
- operationId: record.operationId,
558
- recordId: record.recordId,
559
- });
560
- return;
561
- }
562
- if (record.save === "operation" && !hasEvent("activate"))
563
- await this.#appendRecordEvent(record, {
564
- schemaVersion: 1,
565
- kind: "activate",
566
- sequence: await this.#nextSequence(record.worldId),
567
- operationId: record.operationId,
568
- recordId: record.recordId,
569
- });
570
- }
571
331
  async clear(input) {
572
332
  return this.#withWorldMutation(input.context.worldId, () => this.#clear(input));
573
333
  }
@@ -578,7 +338,7 @@ export class FileNativeArtifactStore {
578
338
  return artifactFailure(`Undeclared artifact output: ${input.output}`);
579
339
  if (input.toolCallId.trim() === "")
580
340
  return artifactFailure("Tool-call ID cannot be empty");
581
- const operation = await this.#readOperation(input.context.operationId);
341
+ const operation = await this.#readOperation(input.context.worldId, input.context.operationId);
582
342
  if (operation === null)
583
343
  throw new ArtifactStoreInvariantError("Artifact clear is missing an operation checkpoint");
584
344
  if (!sameContext(operation.context, input.context))
@@ -586,68 +346,37 @@ export class FileNativeArtifactStore {
586
346
  const currentAttempt = operation.requestAttempts[input.context.requestId];
587
347
  if (currentAttempt === undefined)
588
348
  throw new ArtifactStoreInvariantError("Artifact clear request attempt is not registered");
589
- const clearFingerprint = artifactCallFingerprint("artifact_clear", input.context, { output: declaration.name });
590
- const records = await this.#recordsForOperation(input.context.operationId);
591
- const prior = records.find((record) => record.toolCallId === input.toolCallId);
592
- if (prior !== undefined)
349
+ const prior = await this.#recordsForOperation(input.context.worldId, input.context.operationId);
350
+ if (prior.some((record) => record.toolCallId === input.toolCallId))
593
351
  return artifactFailure("The same artifact tool-call ID is bound to a different tool call");
594
- const priorCall = (await this.#events(input.context.worldId)).find((event) => event.operationId === input.context.operationId &&
595
- event.toolCallId === input.toolCallId);
596
- if (priorCall !== undefined) {
597
- if (priorCall.callFingerprint !== clearFingerprint)
598
- return artifactFailure("The same artifact tool-call ID is bound to different arguments");
599
- return {
600
- ok: true,
601
- idempotent: true,
602
- markdown: `# Runtime artifact clear confirmed\n\noutput=${input.output}`,
603
- };
604
- }
605
352
  const staleAttempt = currentAttempt !== input.context.requestAttempt;
606
353
  if (isStaleOperation(operation) ||
607
354
  staleAttempt ||
608
- !(await this.#extensionControls.visible(input.context.worldId, input.context))) {
609
- const event = {
610
- schemaVersion: 1,
611
- kind: "ignored_call",
612
- operationId: input.context.operationId,
613
- output: declaration.name,
614
- channel: declaration.channel,
615
- ...(declaration.key === undefined ? {} : { key: declaration.key }),
616
- reason: "stale_clear_ignored",
617
- sequence: await this.#nextSequence(input.context.worldId),
618
- toolCallId: input.toolCallId,
619
- toolName: "artifact_clear",
620
- callFingerprint: clearFingerprint,
621
- requestId: input.context.requestId,
622
- requestAttempt: input.context.requestAttempt,
623
- };
624
- // An ignored stale call is a durable identity ledger even for save:none:
625
- // no raw payload is persisted, but a cold retry must distinguish the
626
- // exact same call from a changed request/attempt/parameter.
627
- await this.#appendEventOnce(event);
355
+ !(await this.#extensionControls.visible(input.context.worldId, input.context)))
628
356
  return {
629
357
  ok: true,
630
358
  markdown: `# Runtime artifact clear ignored\n\noutput=${input.output}`,
631
359
  };
360
+ // Projection clears are durable even for save:none records. Only the raw
361
+ // payload is ephemeral, so a cold restart cannot resurrect an old panel.
362
+ for (const record of await this.#records(input.context.worldId)) {
363
+ if (record.status === "superseded" ||
364
+ record.status === "cleared" ||
365
+ record.channel !== declaration.channel ||
366
+ record.output !== declaration.name ||
367
+ (declaration.key !== undefined && record.key !== declaration.key))
368
+ continue;
369
+ await this.#putRecord({
370
+ ...record,
371
+ status: "cleared",
372
+ clearedBy: {
373
+ operationId: input.context.operationId,
374
+ requestId: input.context.requestId,
375
+ requestAttempt: input.context.requestAttempt,
376
+ priorStatus: record.status,
377
+ },
378
+ });
632
379
  }
633
- const event = {
634
- schemaVersion: 1,
635
- kind: "clear",
636
- operationId: input.context.operationId,
637
- output: declaration.name,
638
- channel: declaration.channel,
639
- ...(declaration.key === undefined ? {} : { key: declaration.key }),
640
- reason: "explicit_clear",
641
- sequence: await this.#nextSequence(input.context.worldId),
642
- toolCallId: input.toolCallId,
643
- toolName: "artifact_clear",
644
- callFingerprint: clearFingerprint,
645
- requestId: input.context.requestId,
646
- requestAttempt: input.context.requestAttempt,
647
- };
648
- // Projection clears are durable even for save:none. Only the raw payload
649
- // is ephemeral; otherwise a cold restart would resurrect an old panel.
650
- await this.#appendEventOnce(event);
651
380
  return {
652
381
  ok: true,
653
382
  markdown: `# Runtime artifact clear confirmed\n\noutput=${input.output}`,
@@ -660,7 +389,7 @@ export class FileNativeArtifactStore {
660
389
  assertContext(context);
661
390
  if (head.trim() === "")
662
391
  throw new ArtifactStoreInvariantError("Core commit head is invalid");
663
- const operation = await this.#readOperation(context.operationId);
392
+ const operation = await this.#readOperation(context.worldId, context.operationId);
664
393
  if (operation === null)
665
394
  throw new ArtifactStoreInvariantError("Core commit is missing an artifact-operation checkpoint");
666
395
  if (!sameContext(operation.context, context))
@@ -675,112 +404,50 @@ export class FileNativeArtifactStore {
675
404
  if (operation.status === "core_committed") {
676
405
  if (operation.head !== head)
677
406
  throw new ArtifactStoreInvariantError("Committed artifact operation has a conflicting core head");
678
- await this.#activateCommittedRecords(operation, context, head);
407
+ await this.#activateCommittedRecords(operation, head);
679
408
  return;
680
409
  }
681
- const recoveredFromMaterializationPending = operation.status === "core_materialization_pending";
682
- if (recoveredFromMaterializationPending && operation.head !== head)
683
- throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization has a conflicting core head");
684
- await this.reconcileHead(context.worldId, head, context.operationId);
410
+ await this.#reconcileHead(context.worldId, head, context.operationId);
685
411
  const committed = {
686
412
  ...operation,
687
413
  status: "core_committed",
688
- extensionStatus: recoveredFromMaterializationPending
689
- ? "not_started"
690
- : operation.extensionStatus,
691
- ...(recoveredFromMaterializationPending ? { completedRequests: [] } : {}),
692
414
  head,
693
415
  };
694
- if (recoveredFromMaterializationPending)
695
- delete committed.message;
696
- // Persist the receipt/head before projection events. A crash after this
416
+ // The head is persisted before records bind to it. A crash after this
697
417
  // write is repaired by the idempotent same-head path above.
698
418
  await this.#writeOperation(committed);
699
- await this.#activateCommittedRecords(committed, context, head);
700
- }
701
- async markCoreMaterializationPending(context, head, message) {
702
- return this.#withWorldMutation(context.worldId, () => this.#markCoreMaterializationPending(context, head, message));
703
- }
704
- async #markCoreMaterializationPending(context, head, message) {
705
- const operation = await this.#readOperation(context.operationId);
706
- if (operation === null)
707
- throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization does not exist");
708
- if (!sameContext(operation.context, context))
709
- throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization does not match the frozen operation");
710
- if (operation.status === "core_failed")
711
- throw new ArtifactStoreInvariantError("An artifact operation whose core failed cannot await materialization");
712
- if (operation.status === "core_committed" ||
713
- operation.status === "completed")
714
- return summary(operation);
715
- if (operation.status === "core_materialization_pending" &&
716
- operation.head !== head)
717
- throw new ArtifactStoreInvariantError("Artifact operation awaiting materialization has a conflicting core head");
718
- const pending = {
719
- ...operation,
720
- status: "core_materialization_pending",
721
- extensionStatus: "recovery_required",
722
- message,
723
- completedRequests: [],
724
- head,
725
- };
726
- await this.#writeOperation(pending);
727
- return summary(pending);
419
+ await this.#activateCommittedRecords(committed, head);
728
420
  }
729
- async #activateCommittedRecords(operation, context, head) {
421
+ async #activateCommittedRecords(operation, head) {
730
422
  if (operation.artifactProjectionStatus === "superseded")
731
423
  return;
732
- const effective = await this.#effectiveRecords(context.worldId);
733
- for (const item of effective) {
734
- const { record, status, head: boundHead } = item;
735
- if (record.operationId !== context.operationId)
736
- continue;
737
- if (status === "superseded" ||
738
- status === "cleared" ||
739
- status === "failed")
424
+ const records = await this.#recordsForOperation(operation.context.worldId, operation.context.operationId);
425
+ for (const record of records) {
426
+ if (record.status === "superseded" || record.status === "cleared")
740
427
  continue;
741
- if (boundHead !== null && boundHead !== head)
428
+ if (record.head !== null && record.head !== head)
742
429
  throw new ArtifactStoreInvariantError("Artifact record is bound to a different core head");
743
- if (boundHead !== head)
744
- await this.#appendRecordEvent(record, {
745
- schemaVersion: 1,
746
- kind: "bind_head",
747
- operationId: context.operationId,
748
- recordId: record.recordId,
749
- head,
750
- sequence: await this.#nextSequence(context.worldId),
751
- });
752
- if (status === "pending")
753
- await this.#appendRecordEvent(record, {
754
- schemaVersion: 1,
755
- kind: "activate",
756
- operationId: context.operationId,
757
- recordId: record.recordId,
758
- sequence: await this.#nextSequence(context.worldId),
759
- });
430
+ if (record.head === head && record.status !== "pending")
431
+ continue;
432
+ await this.#putRecord({
433
+ ...record,
434
+ head,
435
+ status: record.status === "pending" ? "active" : record.status,
436
+ });
760
437
  }
761
438
  }
762
- async markCoreFailed(operationId, message) {
763
- const worldId = await this.#worldForOperation(operationId);
764
- if (worldId === null)
765
- return;
766
- return this.#withWorldMutation(worldId, () => this.#markCoreFailed(operationId, message));
439
+ async markCoreFailed(worldId, operationId, message) {
440
+ return this.#withWorldMutation(worldId, () => this.#markCoreFailed(worldId, operationId, message));
767
441
  }
768
- async #markCoreFailed(operationId, message) {
769
- const operation = await this.#readOperation(operationId);
442
+ async #markCoreFailed(worldId, operationId, message) {
443
+ const operation = await this.#readOperation(worldId, operationId);
770
444
  if (operation === null)
771
445
  return;
772
446
  if (operation.status !== "running")
773
447
  return;
774
- const records = await this.#recordsForOperation(operationId);
775
- for (const record of records)
776
- await this.#appendEventOnce({
777
- schemaVersion: 1,
778
- kind: "supersede",
779
- operationId,
780
- recordId: record.recordId,
781
- reason: "core_failed",
782
- sequence: await this.#nextSequence(operation.context.worldId),
783
- });
448
+ for (const record of await this.#recordsForOperation(worldId, operationId))
449
+ if (record.status !== "superseded")
450
+ await this.#putRecord({ ...record, status: "superseded" });
784
451
  await this.#writeOperation({
785
452
  ...operation,
786
453
  status: "core_failed",
@@ -793,7 +460,7 @@ export class FileNativeArtifactStore {
793
460
  return this.#withWorldMutation(context.worldId, () => this.#beginExtension(context));
794
461
  }
795
462
  async #beginExtension(context) {
796
- const operation = await this.#readOperation(context.operationId);
463
+ const operation = await this.#readOperation(context.worldId, context.operationId);
797
464
  if (operation === null)
798
465
  throw new ArtifactStoreInvariantError("Artifact extension is missing an operation checkpoint");
799
466
  if (!sameContext(operation.context, context))
@@ -805,21 +472,13 @@ export class FileNativeArtifactStore {
805
472
  return;
806
473
  await this.#writeOperation({ ...operation, extensionStatus: "running" });
807
474
  }
808
- async completeExtension(operationId, requests) {
809
- const worldId = await this.#worldForOperation(operationId);
810
- if (worldId === null)
811
- return this.#completeExtension(operationId, requests);
812
- return this.#withWorldMutation(worldId, () => this.#completeExtension(operationId, requests));
475
+ async completeExtension(worldId, operationId, requests) {
476
+ return this.#withWorldMutation(worldId, () => this.#completeExtension(worldId, operationId, requests));
813
477
  }
814
- async #completeExtension(operationId, requests) {
815
- const operation = await this.#readOperation(operationId);
478
+ async #completeExtension(worldId, operationId, requests) {
479
+ const operation = await this.#readOperation(worldId, operationId);
816
480
  if (operation === null)
817
- return {
818
- operationId,
819
- status: "recovery_required",
820
- completedRequests: [],
821
- coreCommitted: false,
822
- };
481
+ return recoveryRequired(operationId, [], undefined);
823
482
  if (operation.status === "running")
824
483
  return summary(operation);
825
484
  if (isTerminalExtensionStatus(operation.extensionStatus) ||
@@ -828,17 +487,7 @@ export class FileNativeArtifactStore {
828
487
  return summary(operation);
829
488
  if (operation.status !== "core_committed")
830
489
  throw new ArtifactStoreInvariantError("Artifact extension cannot complete before the core is committed");
831
- const records = await this.#recordsForOperation(operationId);
832
- for (const record of records)
833
- if (record.save === "none" || record.invalidation === "operation_end")
834
- await this.#appendRecordEvent(record, {
835
- schemaVersion: 1,
836
- kind: "supersede",
837
- operationId,
838
- recordId: record.recordId,
839
- reason: "operation_end",
840
- sequence: await this.#nextSequence(operation.context.worldId),
841
- });
490
+ await this.#endOperationRecords(worldId, operationId);
842
491
  const next = {
843
492
  ...operation,
844
493
  status: "completed",
@@ -849,39 +498,20 @@ export class FileNativeArtifactStore {
849
498
  await this.#writeOperation(next);
850
499
  return summary(next);
851
500
  }
852
- async failExtension(operationId, status, message, requests = []) {
853
- const worldId = await this.#worldForOperation(operationId);
854
- if (worldId === null)
855
- return this.#failExtension(operationId, status, message, requests);
856
- return this.#withWorldMutation(worldId, () => this.#failExtension(operationId, status, message, requests));
501
+ async failExtension(worldId, operationId, status, message, requests = []) {
502
+ return this.#withWorldMutation(worldId, () => this.#failExtension(worldId, operationId, status, message, requests));
857
503
  }
858
- async #failExtension(operationId, status, message, requests = []) {
859
- const operation = await this.#readOperation(operationId);
504
+ async #failExtension(worldId, operationId, status, message, requests) {
505
+ const operation = await this.#readOperation(worldId, operationId);
860
506
  if (operation === null)
861
- return {
862
- operationId,
863
- status: "recovery_required",
864
- completedRequests: requests,
865
- message,
866
- coreCommitted: false,
867
- };
507
+ return recoveryRequired(operationId, requests, message);
868
508
  if (isTerminalExtensionStatus(operation.extensionStatus) ||
869
509
  operation.extensionStatus === "unknown" ||
870
510
  operation.extensionStatus === "recovery_required")
871
511
  return summary(operation);
872
512
  if (operation.status !== "core_committed")
873
513
  throw new ArtifactStoreInvariantError("Artifact extension cannot fail before the core is committed");
874
- const records = await this.#recordsForOperation(operationId);
875
- for (const record of records)
876
- if (record.save === "none" || record.invalidation === "operation_end")
877
- await this.#appendRecordEvent(record, {
878
- schemaVersion: 1,
879
- kind: "supersede",
880
- operationId,
881
- recordId: record.recordId,
882
- reason: "extension_failed",
883
- sequence: await this.#nextSequence(operation.context.worldId),
884
- });
514
+ await this.#endOperationRecords(worldId, operationId);
885
515
  const next = {
886
516
  ...operation,
887
517
  extensionStatus: status,
@@ -891,12 +521,20 @@ export class FileNativeArtifactStore {
891
521
  await this.#writeOperation(next);
892
522
  return summary(next);
893
523
  }
894
- async readExtension(operationId) {
895
- const operation = await this.#readOperation(operationId);
524
+ /** Save-none and operation-end records do not survive the extension that produced them. */
525
+ async #endOperationRecords(worldId, operationId) {
526
+ for (const record of await this.#recordsForOperation(worldId, operationId))
527
+ if ((record.save === "none" || record.invalidation === "operation_end") &&
528
+ record.status !== "superseded")
529
+ await this.#putRecord({ ...record, status: "superseded" });
530
+ }
531
+ async readExtension(worldId, operationId) {
532
+ await this.#ensureReadable(worldId);
533
+ const operation = await this.#readOperation(worldId, operationId);
896
534
  return operation === null ? null : summary(operation);
897
535
  }
898
536
  async readExtensionSummaries(worldId) {
899
- await this.#ensureForkRestored(worldId);
537
+ await this.#ensureReadable(worldId);
900
538
  return (await this.#operationsForWorld(worldId))
901
539
  .filter((operation) => operation.status !== "core_failed" &&
902
540
  !(operation.status === "completed" &&
@@ -905,51 +543,43 @@ export class FileNativeArtifactStore {
905
543
  .sort((left, right) => left.context.operationId.localeCompare(right.context.operationId))
906
544
  .map(summary);
907
545
  }
908
- async resumeExtension(operationId) {
909
- const worldId = await this.#worldForOperation(operationId);
910
- if (worldId === null)
911
- return this.#resumeExtension(operationId);
912
- return this.#withWorldMutation(worldId, () => this.#resumeExtension(operationId));
913
- }
914
- async #resumeExtension(operationId) {
915
- const operation = await this.#readOperation(operationId);
916
- if (operation === null)
917
- return {
918
- operationId,
919
- status: "recovery_required",
920
- completedRequests: [],
921
- message: "No recoverable extension checkpoint was found",
922
- coreCommitted: false,
546
+ async resumeExtension(worldId, operationId) {
547
+ return this.#withWorldMutation(worldId, async () => {
548
+ const operation = await this.#readOperation(worldId, operationId);
549
+ if (operation === null)
550
+ return {
551
+ operationId,
552
+ status: "recovery_required",
553
+ completedRequests: [],
554
+ message: "No recoverable extension checkpoint was found",
555
+ coreCommitted: false,
556
+ };
557
+ if (operation.status === "running")
558
+ return summary(operation);
559
+ if (isTerminalExtensionStatus(operation.extensionStatus) ||
560
+ operation.extensionStatus === "recovery_required")
561
+ return summary(operation);
562
+ const next = {
563
+ ...operation,
564
+ extensionStatus: "recovery_required",
565
+ message: "Extension dispatch state cannot be replayed safely; the original request attempt requires explicit manual recovery",
923
566
  };
924
- if (operation.status === "running")
925
- return summary(operation);
926
- if (isTerminalExtensionStatus(operation.extensionStatus) ||
927
- operation.extensionStatus === "recovery_required")
928
- return summary(operation);
929
- const next = {
930
- ...operation,
931
- extensionStatus: "recovery_required",
932
- message: "Extension dispatch state cannot be replayed safely; the original request attempt requires explicit manual recovery",
933
- };
934
- await this.#writeOperation(next);
935
- return summary(next);
567
+ await this.#writeOperation(next);
568
+ return summary(next);
569
+ });
936
570
  }
937
571
  async readActiveProjection(worldId, channel, timeline) {
938
- await this.#ensureForkRestored(worldId);
939
- const effective = await this.#effectiveRecords(worldId);
940
- const operationStates = new Map();
941
- for (const item of effective)
942
- if (!operationStates.has(item.record.operationId)) {
943
- const operation = await this.#readOperation(item.record.operationId);
944
- if (operation !== null)
945
- operationStates.set(item.record.operationId, operation);
946
- }
947
- const active = effective.filter(({ record, status, head }) => {
948
- if (status !== "active" || record.projection === "hidden")
572
+ await this.#ensureReadable(worldId);
573
+ const operations = new Map((await this.#operationsForWorld(worldId)).map((operation) => [
574
+ operation.context.operationId,
575
+ operation,
576
+ ]));
577
+ const active = (await this.#records(worldId)).filter((record) => {
578
+ if (record.status !== "active" || record.projection === "hidden")
949
579
  return false;
950
580
  if (channel !== undefined && record.channel !== channel)
951
581
  return false;
952
- const operation = operationStates.get(record.operationId);
582
+ const operation = operations.get(record.operationId);
953
583
  if (operation === undefined ||
954
584
  operation.artifactProjectionStatus === "superseded")
955
585
  return false;
@@ -961,22 +591,21 @@ export class FileNativeArtifactStore {
961
591
  return false;
962
592
  if (record.projection === "transient" && !isActiveInteraction(operation))
963
593
  return false;
964
- if (record.save === "operation" && operation?.status === "completed")
594
+ if (record.save === "operation" && operation.status === "completed")
965
595
  return false;
966
- if (record.save === "commit" && head === null)
596
+ if (record.save === "commit" && record.head === null)
967
597
  return false;
968
598
  return true;
969
599
  });
970
- const visible = await Promise.all(active.map(async (item) => (await this.#extensionControls.visible(worldId, item.record))
971
- ? item
600
+ const visible = await Promise.all(active.map(async (record) => (await this.#extensionControls.visible(worldId, record))
601
+ ? record
972
602
  : null));
973
603
  const selected = selectProjection(visible.filter((item) => item !== null));
974
- return selected.map(({ record, head }) => ({
975
- ...(operationStates.get(record.operationId)?.context.attachment ===
976
- undefined
604
+ return selected.map((record) => ({
605
+ ...(operations.get(record.operationId)?.context.attachment === undefined
977
606
  ? {}
978
607
  : {
979
- attachment: structuredClone(operationStates.get(record.operationId).context.attachment),
608
+ attachment: structuredClone(operations.get(record.operationId).context.attachment),
980
609
  }),
981
610
  recordId: record.recordId,
982
611
  worldId: record.worldId,
@@ -1004,20 +633,19 @@ export class FileNativeArtifactStore {
1004
633
  projection: record.projection,
1005
634
  save: record.save,
1006
635
  sequence: record.sequence,
1007
- head: head ?? record.parentHead,
636
+ head: record.head ?? record.parentHead,
1008
637
  }));
1009
638
  }
1010
639
  async readDebug(worldId, operationId) {
1011
- await this.#ensureForkRestored(worldId);
1012
- const effective = await this.#effectiveRecords(worldId);
640
+ await this.#ensureReadable(worldId);
1013
641
  const operations = new Map((await this.#operationsForWorld(worldId)).map((operation) => [
1014
642
  operation.context.operationId,
1015
643
  operation,
1016
644
  ]));
1017
- return effective
1018
- .filter(({ record }) => operationId === undefined || record.operationId === operationId)
1019
- .sort((left, right) => left.record.sequence - right.record.sequence)
1020
- .map(({ record, status, head }) => ({
645
+ return (await this.#records(worldId))
646
+ .filter((record) => operationId === undefined || record.operationId === operationId)
647
+ .sort((left, right) => left.sequence - right.sequence)
648
+ .map((record) => ({
1021
649
  ...(operations.get(record.operationId)?.context.attachment === undefined
1022
650
  ? {}
1023
651
  : {
@@ -1031,7 +659,7 @@ export class FileNativeArtifactStore {
1031
659
  playPresetRevision: record.playPresetRevision,
1032
660
  playPresetScriptsEnabled: record.playPresetScriptsEnabled,
1033
661
  parentHead: record.parentHead,
1034
- newHead: head,
662
+ newHead: record.head,
1035
663
  requestId: record.requestId,
1036
664
  requestAttempt: record.requestAttempt,
1037
665
  output: record.output,
@@ -1053,155 +681,24 @@ export class FileNativeArtifactStore {
1053
681
  : { frozenPresentation: structuredClone(record.frozenPresentation) }),
1054
682
  payload: structuredClone(record.payload),
1055
683
  payloadFingerprint: record.payloadFingerprint,
1056
- status,
684
+ status: record.status,
1057
685
  }));
1058
686
  }
1059
- async #newRecord(input, declaration, payload, fingerprint, callFingerprint) {
1060
- const sequence = await this.#nextSequence(input.context.worldId);
1061
- const recordId = `artifact:${sequence}:${fingerprint.slice(7, 23)}`;
1062
- const record = {
1063
- schemaVersion: 1,
1064
- recordId,
1065
- sequence,
1066
- worldId: input.context.worldId,
1067
- parentHead: input.context.parentHead,
1068
- newHead: null,
1069
- operationId: input.context.operationId,
1070
- playPresetId: input.context.playPresetId,
1071
- playPresetRevision: input.context.playPresetRevision,
1072
- playPresetScriptsEnabled: input.context.playPresetScriptsEnabled,
1073
- requestId: input.context.requestId,
1074
- requestAttempt: input.context.requestAttempt,
1075
- output: declaration.name,
1076
- channel: declaration.channel,
1077
- ...(declaration.key === undefined ? {} : { key: declaration.key }),
1078
- contentType: declaration.contentType,
1079
- ...(declaration.renderer === undefined
1080
- ? {}
1081
- : { renderer: declaration.renderer }),
1082
- ...(declaration.rendererRevision === undefined
1083
- ? {}
1084
- : { rendererRevision: declaration.rendererRevision }),
1085
- ...(input.context.extensionControl === undefined
1086
- ? {}
1087
- : {
1088
- extensionControl: structuredClone(input.context.extensionControl),
1089
- }),
1090
- ...(input.context.frozenPresentations?.[declaration.name] !== undefined
1091
- ? {
1092
- frozenPresentation: structuredClone(input.context.frozenPresentations[declaration.name]),
1093
- }
1094
- : input.context.frozenResources === undefined
1095
- ? {}
1096
- : {
1097
- frozenPresentation: {
1098
- ...structuredClone(input.context.frozenResources),
1099
- declaration: structuredClone(declaration),
1100
- },
1101
- }),
1102
- payload: structuredClone(payload),
1103
- save: declaration.save,
1104
- projection: declaration.strategy,
1105
- invalidation: declaration.invalidation,
1106
- toolCallId: input.toolCallId,
1107
- toolName: "artifact_emit",
1108
- callFingerprint,
1109
- payloadFingerprint: fingerprint,
1110
- status: "pending",
1111
- };
1112
- return {
1113
- ...record,
1114
- recordFingerprint: rawRecordFingerprint(record),
1115
- };
1116
- }
1117
- async #appendRaw(record) {
1118
- const directory = join(this.#worldRoot(record.worldId), "records");
1119
- await mkdir(directory, { recursive: true, mode: 0o700 });
1120
- await writeDurableJson(join(directory, `${String(record.sequence).padStart(12, "0")}.json`), record, true);
1121
- }
1122
- async #appendRecordEvent(record, event) {
1123
- await this.#appendCallEvent({
1124
- worldId: record.worldId,
1125
- parentHead: record.parentHead,
1126
- operationId: record.operationId,
1127
- playPresetId: record.playPresetId,
1128
- playPresetRevision: record.playPresetRevision,
1129
- playPresetScriptsEnabled: record.playPresetScriptsEnabled,
1130
- requestId: record.requestId,
1131
- requestAttempt: record.requestAttempt,
1132
- maxArtifactBytes: maxPayloadBytes,
1133
- declarations: [],
1134
- }, record.save, event);
1135
- }
1136
- async #appendCallEvent(context, save, event) {
1137
- if (save === "none") {
1138
- const events = this.#memoryEvents.get(context.operationId) ?? [];
1139
- if (!events.some((candidate) => sameEventIdentity(candidate, event))) {
1140
- events.push(event);
1141
- this.#memoryEvents.set(context.operationId, events);
1142
- }
1143
- return;
1144
- }
1145
- await this.#appendEventOnce(event);
1146
- }
1147
- async #appendEvent(event) {
1148
- const worldId = await this.#worldForOperation(event.operationId);
1149
- if (worldId === null) {
1150
- const memory = this.#memoryEvents.get(event.operationId) ?? [];
1151
- memory.push(event);
1152
- this.#memoryEvents.set(event.operationId, memory);
1153
- return;
1154
- }
1155
- const path = join(this.#worldRoot(worldId), "events", `${String(event.sequence).padStart(12, "0")}.json`);
1156
- await mkdir(join(this.#worldRoot(worldId), "events"), {
1157
- recursive: true,
1158
- mode: 0o700,
1159
- });
1160
- await writeDurableJson(path, event, true);
1161
- }
1162
- async #appendEventOnce(event) {
1163
- const worldId = await this.#worldForOperation(event.operationId);
1164
- if (worldId !== null) {
1165
- const existing = await this.#events(worldId);
1166
- if (existing.some((candidate) => sameEventIdentity(candidate, event)))
1167
- return;
1168
- }
1169
- else {
1170
- const existing = this.#memoryEvents.get(event.operationId) ?? [];
1171
- if (existing.some((candidate) => sameEventIdentity(candidate, event)))
1172
- return;
1173
- }
1174
- await this.#appendEvent(event);
1175
- }
1176
- async #effectiveRecords(worldId) {
1177
- const raw = [];
1178
- const memory = this.#memoryRecords;
1179
- const recordsDirectory = join(this.#worldRoot(worldId), "records");
1180
- try {
1181
- const names = (await readdir(recordsDirectory))
1182
- .filter((name) => name.endsWith(".json"))
1183
- .sort();
1184
- for (const name of names) {
1185
- const parsed = parseArtifactJson(await readFile(join(recordsDirectory, name), "utf8"), "artifact raw record");
1186
- assertRawRecord(parsed);
1187
- if (name !== artifactSequenceFileName(parsed.sequence))
1188
- throw new ArtifactStoreCorruptionError("Artifact raw-record file name does not match its sequence");
1189
- if (parsed.worldId !== worldId)
1190
- throw new ArtifactStoreCorruptionError("Artifact raw record does not match the world path");
1191
- raw.push(parsed);
1192
- }
1193
- }
1194
- catch (error) {
1195
- if (!isMissing(error))
1196
- throw error;
1197
- }
1198
- for (const records of memory.values())
1199
- for (const record of records)
1200
- if (record.worldId === worldId)
1201
- raw.push(record);
687
+ /** Records and operations of a world, read from its directory; memory-only records are included. */
688
+ async #records(worldId) {
689
+ const raw = [
690
+ ...(await readRecordFiles(this.#artifactsRoot(worldId))),
691
+ ...[...this.#memoryRecords.values()]
692
+ .flat()
693
+ .filter((record) => record.worldId === worldId),
694
+ ];
1202
695
  raw.sort((left, right) => left.sequence - right.sequence);
1203
696
  const recordIds = new Set();
1204
697
  const sequences = new Set();
698
+ const operations = new Map((await this.#operationsForWorld(worldId)).map((operation) => [
699
+ operation.context.operationId,
700
+ operation,
701
+ ]));
1205
702
  for (const record of raw) {
1206
703
  if (recordIds.has(record.recordId))
1207
704
  throw new ArtifactStoreCorruptionError("Artifact raw-record ID is duplicated");
@@ -1209,15 +706,7 @@ export class FileNativeArtifactStore {
1209
706
  throw new ArtifactStoreCorruptionError("Artifact world sequence is duplicated");
1210
707
  recordIds.add(record.recordId);
1211
708
  sequences.add(record.sequence);
1212
- }
1213
- const state = new Map();
1214
- for (const record of raw)
1215
- state.set(record.recordId, { status: "pending", head: null });
1216
- const operationStates = new Map();
1217
- for (const operation of await this.#operationsForWorld(worldId))
1218
- operationStates.set(operation.context.operationId, operation);
1219
- for (const record of raw) {
1220
- const operation = operationStates.get(record.operationId);
709
+ const operation = operations.get(record.operationId);
1221
710
  if (operation?.context.worldId !== record.worldId ||
1222
711
  operation.context.parentHead !== record.parentHead ||
1223
712
  operation.context.playPresetId !== record.playPresetId ||
@@ -1226,177 +715,72 @@ export class FileNativeArtifactStore {
1226
715
  record.playPresetScriptsEnabled)
1227
716
  throw new ArtifactStoreCorruptionError("Artifact raw record does not match the operation identity");
1228
717
  }
1229
- const events = await this.#events(worldId);
1230
- for (const event of events) {
1231
- if (sequences.has(event.sequence))
1232
- throw new ArtifactStoreCorruptionError("Artifact world sequence is duplicated");
1233
- sequences.add(event.sequence);
1234
- const eventOperation = operationStates.get(event.operationId);
1235
- if (eventOperation === undefined)
1236
- throw new ArtifactStoreCorruptionError("Artifact projection-event operation does not exist");
1237
- if (event.recordId !== undefined) {
1238
- const record = raw.find(({ recordId }) => recordId === event.recordId);
1239
- if (record?.operationId !== event.operationId)
1240
- throw new ArtifactStoreCorruptionError("Artifact projection event does not match the record identity");
1241
- }
1242
- if (event.kind === "activate" && event.recordId !== undefined) {
1243
- const current = state.get(event.recordId);
1244
- if (current?.status === "pending")
1245
- current.status = "active";
1246
- }
1247
- else if (event.kind === "supersede" && event.recordId !== undefined) {
1248
- const current = state.get(event.recordId);
1249
- if (current !== undefined)
1250
- current.status = "superseded";
1251
- }
1252
- else if (event.kind === "bind_head" && event.recordId !== undefined) {
1253
- if ((eventOperation.status !== "core_committed" &&
1254
- eventOperation.status !== "completed") ||
1255
- eventOperation.head !== event.head)
1256
- throw new ArtifactStoreCorruptionError("Artifact bind_head event does not match the operation core head");
1257
- const current = state.get(event.recordId);
1258
- if (current !== undefined)
1259
- current.head = event.head ?? null;
1260
- }
1261
- else if (event.kind === "clear") {
1262
- const initiator = operationStates.get(event.operationId);
1263
- if (initiator === undefined ||
1264
- initiator.artifactProjectionStatus === "superseded" ||
1265
- (initiator.requestAttempts[event.requestId] !== undefined &&
1266
- initiator.requestAttempts[event.requestId] !==
1267
- event.requestAttempt))
1268
- continue;
1269
- for (const record of raw) {
1270
- const current = state.get(record.recordId);
1271
- if (current !== undefined &&
1272
- current.status !== "superseded" &&
1273
- current.status !== "cleared" &&
1274
- record.sequence < event.sequence &&
1275
- record.channel === event.channel &&
1276
- record.output === event.output &&
1277
- (event.key === undefined || record.key === event.key))
1278
- current.status = "cleared";
1279
- }
1280
- }
1281
- }
1282
- return raw.map((record) => {
1283
- const current = state.get(record.recordId);
1284
- let status = current?.status ?? "failed";
1285
- const operation = operationStates.get(record.operationId);
1286
- const currentAttempt = operation?.requestAttempts[record.requestId];
1287
- if (currentAttempt !== undefined &&
1288
- currentAttempt !== record.requestAttempt &&
1289
- status !== "failed")
1290
- status = "superseded";
1291
- if (operation?.artifactProjectionStatus === "superseded" &&
1292
- status !== "failed")
1293
- status = "superseded";
1294
- return {
1295
- record,
1296
- status,
1297
- head: current?.head ?? null,
1298
- };
1299
- });
718
+ return raw;
1300
719
  }
1301
- async #events(worldId) {
1302
- const result = [];
1303
- const directory = join(this.#worldRoot(worldId), "events");
1304
- try {
1305
- const names = (await readdir(directory))
1306
- .filter((name) => name.endsWith(".json"))
1307
- .sort();
1308
- for (const name of names) {
1309
- const event = parseArtifactJson(await readFile(join(directory, name), "utf8"), "artifact projection event");
1310
- assertEvent(event);
1311
- if (name !== artifactSequenceFileName(event.sequence))
1312
- throw new ArtifactStoreCorruptionError("Artifact projection-event file name does not match its sequence");
1313
- result.push(event);
1314
- }
1315
- }
1316
- catch (error) {
1317
- if (!isMissing(error))
1318
- throw error;
1319
- }
1320
- for (const [operationId, events] of this.#memoryEvents) {
1321
- const operation = await this.#readOperation(operationId);
1322
- if (operation?.context.worldId === worldId)
1323
- result.push(...events);
720
+ async #recordsForOperation(worldId, operationId) {
721
+ return (await this.#records(worldId)).filter((record) => record.operationId === operationId);
722
+ }
723
+ /** Writes or replaces one record; save:none records stay in memory only. */
724
+ async #putRecord(input) {
725
+ // Only a cleared record keeps the clear that made it so.
726
+ const record = input.status === "cleared" ? input : withoutClearedBy(input);
727
+ const finalized = {
728
+ ...record,
729
+ recordFingerprint: rawRecordFingerprint(record),
730
+ };
731
+ assertRawRecord(finalized);
732
+ if (record.save === "none") {
733
+ const records = this.#memoryRecords.get(record.operationId) ?? [];
734
+ const index = records.findIndex((candidate) => candidate.recordId === record.recordId);
735
+ if (index < 0)
736
+ records.push(finalized);
737
+ else
738
+ records[index] = finalized;
739
+ this.#memoryRecords.set(record.operationId, records);
740
+ return;
1324
741
  }
1325
- return result.sort((left, right) => left.sequence - right.sequence);
742
+ await writeArtifactJson(join(this.#artifactsRoot(record.worldId), "records", artifactSequenceFileName(record.sequence)), finalized);
1326
743
  }
1327
- async #recordsForOperation(operationId) {
1328
- const operation = await this.#readOperation(operationId);
1329
- if (operation === null)
1330
- return this.#memoryRecords.get(operationId) ?? [];
1331
- const records = await this.#effectiveRecords(operation.context.worldId);
1332
- return records
1333
- .filter(({ record }) => record.operationId === operationId)
1334
- .map(({ record }) => record);
744
+ async #removeRecord(record) {
745
+ if (record.save === "none") {
746
+ const records = (this.#memoryRecords.get(record.operationId) ?? []).filter((candidate) => candidate.recordId !== record.recordId);
747
+ this.#memoryRecords.set(record.operationId, records);
748
+ return;
749
+ }
750
+ await rm(join(this.#artifactsRoot(record.worldId), "records", artifactSequenceFileName(record.sequence)), { force: true });
1335
751
  }
1336
752
  async #operationsForWorld(worldId) {
1337
- const byId = new Map();
1338
- const directory = join(this.#root, "operations");
753
+ return [
754
+ ...(await readOperationFiles(this.#artifactsRoot(worldId))).values(),
755
+ ].filter((operation) => operation.context.worldId === worldId);
756
+ }
757
+ async #nextSequence(worldId) {
758
+ let maximum = 0;
1339
759
  try {
1340
- const names = (await readdir(directory))
1341
- .filter((name) => name.endsWith(".json"))
1342
- .sort();
1343
- for (const name of names) {
1344
- const operation = parseArtifactJson(await readFile(join(directory, name), "utf8"), "artifact operation");
1345
- assertOperation(operation);
1346
- if (name !== `${identityHash(operation.context.operationId)}.json`)
1347
- throw new ArtifactStoreCorruptionError("Artifact operation does not match the file-path identity");
1348
- if (byId.has(operation.context.operationId))
1349
- throw new ArtifactStoreCorruptionError("Artifact operation ID is duplicated");
1350
- if (operation.context.worldId === worldId)
1351
- byId.set(operation.context.operationId, structuredClone(operation));
760
+ for (const name of await readdir(join(this.#artifactsRoot(worldId), "records"))) {
761
+ if (!name.endsWith(".json"))
762
+ continue;
763
+ const value = Number.parseInt(name.slice(0, -5), 10);
764
+ if (!Number.isSafeInteger(value) ||
765
+ value < 1 ||
766
+ name !== artifactSequenceFileName(value))
767
+ throw new ArtifactStoreCorruptionError("Artifact sequence file name is invalid");
768
+ maximum = Math.max(maximum, value);
1352
769
  }
1353
770
  }
1354
771
  catch (error) {
1355
772
  if (!isMissing(error))
1356
773
  throw error;
1357
774
  }
1358
- return [...byId.values()];
1359
- }
1360
- async #nextSequence(worldId) {
1361
- let maximum = 0;
1362
- const directories = [
1363
- join(this.#worldRoot(worldId), "records"),
1364
- join(this.#worldRoot(worldId), "events"),
1365
- ];
1366
- for (const directory of directories) {
1367
- try {
1368
- const names = (await readdir(directory)).filter((name) => name.endsWith(".json"));
1369
- for (const name of names) {
1370
- const value = Number.parseInt(name.slice(0, -5), 10);
1371
- if (!Number.isSafeInteger(value) ||
1372
- value < 1 ||
1373
- name !== artifactSequenceFileName(value))
1374
- throw new ArtifactStoreCorruptionError("Artifact sequence file name is invalid");
1375
- maximum = Math.max(maximum, value);
1376
- }
1377
- }
1378
- catch (error) {
1379
- if (!isMissing(error))
1380
- throw error;
1381
- }
1382
- }
1383
775
  for (const records of this.#memoryRecords.values())
1384
776
  for (const record of records)
1385
777
  if (record.worldId === worldId)
1386
778
  maximum = Math.max(maximum, record.sequence);
1387
- for (const [operationId, events] of this.#memoryEvents) {
1388
- const operation = await this.#readOperation(operationId);
1389
- if (operation?.context.worldId !== worldId)
1390
- continue;
1391
- for (const event of events)
1392
- maximum = Math.max(maximum, event.sequence);
1393
- }
1394
779
  return maximum + 1;
1395
780
  }
1396
- async #readOperation(operationId) {
1397
- const path = join(this.#root, "operations", `${identityHash(operationId)}.json`);
781
+ async #readOperation(worldId, operationId) {
1398
782
  try {
1399
- const operation = parseArtifactJson(await readFile(path, "utf8"), "artifact operation");
783
+ const operation = parseArtifactJson(await readFile(this.#operationPath(worldId, operationId), "utf8"), "artifact operation");
1400
784
  assertOperation(operation);
1401
785
  if (operation.context.operationId !== operationId)
1402
786
  throw new ArtifactStoreCorruptionError("Artifact operation does not match the request identity");
@@ -1410,276 +794,208 @@ export class FileNativeArtifactStore {
1410
794
  }
1411
795
  async #writeOperation(operation) {
1412
796
  assertOperation(operation);
1413
- const directory = join(this.#root, "operations");
1414
- await mkdir(directory, { recursive: true, mode: 0o700 });
1415
- const path = join(directory, `${identityHash(operation.context.operationId)}.json`);
1416
- await writeDurableJson(path, operation, false);
797
+ await writeArtifactJson(this.#operationPath(operation.context.worldId, operation.context.operationId), operation);
1417
798
  }
1418
- async #worldForOperation(operationId) {
1419
- const operation = await this.#readOperation(operationId);
1420
- return operation?.context.worldId ?? null;
799
+ /** Reads of a world whose artifacts are still in the legacy layout migrate it first. */
800
+ async #ensureReadable(worldId) {
801
+ if (await hasLegacyArtifactState(this.#dataRoot, worldId))
802
+ await this.#withWorldMutation(worldId, () => Promise.resolve());
1421
803
  }
1422
804
  async #withWorldMutation(worldId, action) {
1423
805
  const active = this.#mutationContext.getStore();
1424
806
  if (active?.has(worldId) === true)
1425
807
  return action();
1426
- return withArtifactWorldLock(artifactWorldLockPath(dirname(this.#root), worldId), () => this.#mutationContext.run(new Set([...(active ?? []), worldId]), async () => {
1427
- await this.#restoreFork(worldId);
808
+ return worldQueueFor(this.#dataRoot).run(worldId, () => this.#mutationContext.run(new Set([...(active ?? []), worldId]), async () => {
809
+ await migrateLegacyArtifactStore(this.#dataRoot, worldId);
1428
810
  return action();
1429
811
  }));
1430
812
  }
1431
- #worldRoot(worldId) {
1432
- return join(this.#root, "worlds", identityHash(worldId));
813
+ #artifactsRoot(worldId) {
814
+ assertWorldDirectoryId(worldId);
815
+ return join(this.#worldsRoot, worldId, "artifacts");
1433
816
  }
1434
- }
1435
- function artifactWorldLockPath(dataRoot, worldId) {
1436
- return join(resolve(dataRoot), "runtime-locks", "artifacts", identityHash(worldId));
1437
- }
1438
- const artifactWorldLockStaleMilliseconds = 30_000;
1439
- const artifactWorldLockWaitMilliseconds = 30_000;
1440
- async function withArtifactWorldLock(path, action) {
1441
- const token = randomUUID();
1442
- const processIdentity = await artifactLocalProcessIdentity();
1443
- const parent = dirname(path);
1444
- await mkdir(parent, { recursive: true, mode: 0o700 });
1445
- const deadline = Date.now() + artifactWorldLockWaitMilliseconds;
1446
- while (true) {
1447
- try {
1448
- await mkdir(path, { mode: 0o700 });
1449
- break;
1450
- }
1451
- catch (error) {
1452
- if (!isAlreadyExists(error))
1453
- throw error;
1454
- const owner = await readArtifactLockOwner(path);
1455
- if (owner !== null && (await artifactLockOwnerIsAlive(owner, path))) {
1456
- if (Date.now() >= deadline)
1457
- throw new Error("artifact world mutation lock is busy", {
1458
- cause: error,
1459
- });
1460
- await artifactLockDelay(5);
1461
- continue;
1462
- }
1463
- const metadata = await stat(path).catch(() => null);
1464
- if (owner !== null ||
1465
- (metadata !== null &&
1466
- Date.now() - metadata.mtimeMs > artifactWorldLockStaleMilliseconds))
1467
- await retireArtifactWorldLock(path, owner, metadata, parent);
1468
- if (Date.now() >= deadline)
1469
- throw new Error("artifact world mutation lock is busy", {
1470
- cause: error,
1471
- });
1472
- await artifactLockDelay(5);
1473
- }
817
+ #operationPath(worldId, operationId) {
818
+ return join(this.#artifactsRoot(worldId), "operations", `${identityHash(operationId)}.json`);
1474
819
  }
1475
- const ownerPath = join(path, "owner.json");
1476
- const heartbeatPath = join(path, "heartbeat");
1477
- let heartbeatHandle;
1478
- let stopped = false;
1479
- let timer;
1480
- let heartbeat = Promise.resolve();
1481
- const schedule = () => {
1482
- const handle = heartbeatHandle;
1483
- if (handle === undefined)
1484
- throw new ArtifactStoreInvariantError("Artifact world-lock heartbeat is not initialized");
1485
- timer = setTimeout(() => {
1486
- const now = new Date();
1487
- heartbeat = handle
1488
- .utimes(now, now)
1489
- .catch(() => undefined)
1490
- .finally(() => {
1491
- if (!stopped)
1492
- schedule();
820
+ }
821
+ /**
822
+ * Rewrites a copied world directory into a fork target: records and operations
823
+ * are kept only when they are committed, active, attached to a reply and bound
824
+ * at or before the restored head. Everything else is deleted file by file, and
825
+ * the survivors take the target identity. Runs on an unpublished staging copy.
826
+ */
827
+ export async function forkArtifactClosure(worldRoot, input) {
828
+ const artifactsRoot = join(worldRoot, "artifacts");
829
+ const limit = headOrdinal(input.restoredHead);
830
+ const records = await readRecordFiles(artifactsRoot);
831
+ const operations = await readOperationFiles(artifactsRoot);
832
+ for (const record of records)
833
+ if (record.worldId !== input.sourceWorldId)
834
+ throw new ArtifactStoreCorruptionError("Artifact fork record belongs to another world");
835
+ // A fork keeps what the source's projection would show at that head: the
836
+ // committed, attached, selected records. Superseded or unselected records
837
+ // stay behind with the source.
838
+ const candidates = records.filter((record) => {
839
+ const operation = operations.get(record.operationId);
840
+ return (record.save === "commit" &&
841
+ record.status === "active" &&
842
+ record.projection !== "hidden" &&
843
+ (record.projection !== "transient" || isActiveInteraction(operation)) &&
844
+ operation !== undefined &&
845
+ operation.artifactProjectionStatus !== "superseded" &&
846
+ operation.context.attachment !== undefined &&
847
+ headOrdinal(record.head ?? record.parentHead) <= limit);
848
+ });
849
+ const retained = selectProjection(candidates);
850
+ const retainedRecordIds = new Set(retained.map((record) => record.recordId));
851
+ for (const record of records)
852
+ if (!retainedRecordIds.has(record.recordId))
853
+ await rm(join(artifactsRoot, "records", artifactSequenceFileName(record.sequence)), { force: true });
854
+ const retainedOperations = new Set(retained.map((record) => record.operationId));
855
+ for (const operationId of operations.keys())
856
+ if (!retainedOperations.has(operationId))
857
+ await rm(join(artifactsRoot, "operations", `${identityHash(operationId)}.json`), {
858
+ force: true,
1493
859
  });
1494
- }, 5_000);
1495
- };
1496
- try {
1497
- await writeDurableJson(ownerPath, { token, pid: process.pid, processIdentity }, true);
1498
- if (process.env.NARRAEON_INTERNAL_TEST_FAIL_AT_ARTIFACT_LOCK_EDGE ===
1499
- "after_owner" &&
1500
- process.env.NODE_ENV === "test")
1501
- throw new Error("simulated artifact lock setup failure");
1502
- await writeDurableJson(heartbeatPath, { token }, true);
1503
- heartbeatHandle = await open(heartbeatPath, "r+");
1504
- schedule();
1505
- return await action();
860
+ for (const record of retained) {
861
+ const next = {
862
+ ...record,
863
+ worldId: input.targetWorldId,
864
+ operationId: forkOperationId(input.targetWorldId, record.operationId),
865
+ };
866
+ await writeArtifactJson(join(artifactsRoot, "records", artifactSequenceFileName(record.sequence)), { ...next, recordFingerprint: rawRecordFingerprint(next) });
1506
867
  }
1507
- finally {
1508
- stopped = true;
1509
- if (timer !== undefined)
1510
- clearTimeout(timer);
1511
- await heartbeat;
1512
- await heartbeatHandle?.close();
1513
- await releaseArtifactWorldLock(path, token, parent);
868
+ for (const operationId of retainedOperations) {
869
+ const operation = operations.get(operationId);
870
+ const nextId = forkOperationId(input.targetWorldId, operationId);
871
+ const next = {
872
+ ...operation,
873
+ context: {
874
+ ...operation.context,
875
+ worldId: input.targetWorldId,
876
+ operationId: nextId,
877
+ },
878
+ };
879
+ await writeArtifactJson(join(artifactsRoot, "operations", `${identityHash(nextId)}.json`), next);
880
+ await rm(join(artifactsRoot, "operations", `${identityHash(operationId)}.json`), {
881
+ force: true,
882
+ });
1514
883
  }
1515
884
  }
1516
- async function readArtifactLockOwner(path) {
1517
- let value;
1518
- try {
1519
- value = JSON.parse(await readFile(join(path, "owner.json"), "utf8"));
1520
- }
1521
- catch (error) {
1522
- if (isMissing(error))
1523
- return null;
1524
- throw error;
1525
- }
1526
- if (!isRecord(value) ||
1527
- !hasExactKeys(value, ["token", "pid", "processIdentity"]) ||
1528
- !isNonEmptyString(value.token) ||
1529
- !Number.isSafeInteger(value.pid) ||
1530
- Number(value.pid) < 1 ||
1531
- !isNonEmptyString(value.processIdentity))
1532
- throw new ArtifactStoreCorruptionError("Artifact world-mutation lock owner is corrupt");
1533
- return value;
885
+ function withoutClearedBy(record) {
886
+ const copy = { ...record };
887
+ delete copy.clearedBy;
888
+ return copy;
1534
889
  }
1535
- async function retireArtifactWorldLock(path, observedOwner, observedMetadata, locksRoot) {
1536
- if (observedOwner === null && observedMetadata === null)
1537
- return;
1538
- const currentOwner = await readArtifactLockOwner(path);
1539
- const currentMetadata = await stat(path).catch(() => null);
1540
- const sameGeneration = observedOwner === null
1541
- ? currentOwner === null &&
1542
- observedMetadata !== null &&
1543
- currentMetadata !== null &&
1544
- observedMetadata.dev === currentMetadata.dev &&
1545
- observedMetadata.ino === currentMetadata.ino
1546
- : currentOwner?.token === observedOwner.token;
1547
- if (!sameGeneration)
1548
- return;
1549
- if (currentOwner !== null &&
1550
- (await artifactLockOwnerIsAlive(currentOwner, path)))
1551
- return;
1552
- if (currentOwner === null &&
1553
- (currentMetadata === null ||
1554
- Date.now() - currentMetadata.mtimeMs <=
1555
- artifactWorldLockStaleMilliseconds))
1556
- return;
1557
- const generation = currentOwner?.token ??
1558
- `inode-${currentMetadata?.dev ?? "missing"}-${currentMetadata?.ino ?? "missing"}`;
1559
- const retired = `${path}.${identityHash(generation)}.${randomUUID()}.retired`;
1560
- try {
1561
- await rename(path, retired);
1562
- await syncArtifactDirectory(locksRoot);
1563
- await rm(retired, { recursive: true, force: true });
1564
- }
1565
- catch (error) {
1566
- if (isMissing(error) || isAlreadyExists(error))
1567
- return;
1568
- throw error;
1569
- }
890
+ function forkOperationId(targetWorldId, operationId) {
891
+ return `artifact-fork:${targetWorldId}:${identityHash(operationId)}`;
1570
892
  }
1571
- async function releaseArtifactWorldLock(path, token, locksRoot) {
1572
- const owner = await readArtifactLockOwner(path);
1573
- if (owner?.token !== token)
1574
- return;
1575
- const retired = `${path}.${identityHash(token)}.${randomUUID()}.retired`;
893
+ async function readRecordFiles(artifactsRoot) {
894
+ const directory = join(artifactsRoot, "records");
895
+ let names;
1576
896
  try {
1577
- await rename(path, retired);
1578
- await syncArtifactDirectory(locksRoot);
1579
- await rm(retired, { recursive: true, force: true });
897
+ names = (await readdir(directory)).filter((name) => name.endsWith(".json"));
1580
898
  }
1581
899
  catch (error) {
1582
- if (isMissing(error) || isAlreadyExists(error))
1583
- return;
900
+ if (isMissing(error))
901
+ return [];
1584
902
  throw error;
1585
903
  }
1586
- }
1587
- let cachedArtifactProcessIdentity;
1588
- function artifactLocalProcessIdentity() {
1589
- cachedArtifactProcessIdentity ??= artifactProcessIdentity(process.pid).then((identity) => identity ?? `fallback:${process.pid}:${randomUUID()}`);
1590
- return cachedArtifactProcessIdentity;
1591
- }
1592
- async function artifactLockOwnerIsAlive(owner, path) {
1593
- const identity = await artifactProcessIdentity(owner.pid);
1594
- if (identity === owner.processIdentity)
1595
- return true;
1596
- if (identity === null) {
1597
- try {
1598
- process.kill(owner.pid, 0);
1599
- return true;
1600
- }
1601
- catch (error) {
1602
- if (isRecord(error) && error.code === "ESRCH")
1603
- return false;
1604
- // Fall through to the heartbeat for non-Linux or inaccessible processes.
1605
- }
1606
- }
1607
- const heartbeat = await stat(join(path, "heartbeat")).catch(() => null);
1608
- return (heartbeat !== null &&
1609
- Date.now() - heartbeat.mtimeMs <= artifactWorldLockStaleMilliseconds);
1610
- }
1611
- async function artifactProcessIdentity(pid) {
1612
- try {
1613
- const [bootId, namespace, processStat] = await Promise.all([
1614
- readFile("/proc/sys/kernel/random/boot_id", "utf8"),
1615
- readlink(`/proc/${pid}/ns/pid`),
1616
- readFile(`/proc/${pid}/stat`, "utf8"),
1617
- ]);
1618
- const commandEnd = processStat.lastIndexOf(") ");
1619
- if (commandEnd < 0)
1620
- return null;
1621
- const fields = processStat
1622
- .slice(commandEnd + 2)
1623
- .trim()
1624
- .split(/\s+/u);
1625
- const startTicks = fields[19];
1626
- if (startTicks === undefined || !/^\d+$/u.test(startTicks))
1627
- return null;
1628
- return `linux:${bootId.trim()}:${namespace}:${startTicks}`;
1629
- }
1630
- catch {
1631
- return null;
904
+ const records = [];
905
+ for (const name of names.sort()) {
906
+ const parsed = parseArtifactJson(await readFile(join(directory, name), "utf8"), "artifact raw record");
907
+ assertRawRecord(parsed);
908
+ if (name !== artifactSequenceFileName(parsed.sequence))
909
+ throw new ArtifactStoreCorruptionError("Artifact raw-record file name does not match its sequence");
910
+ records.push(parsed);
1632
911
  }
912
+ return records.sort((left, right) => left.sequence - right.sequence);
1633
913
  }
1634
- async function writeDurableJson(path, value, exclusive) {
1635
- const directory = dirname(path);
1636
- await mkdir(directory, { recursive: true, mode: 0o700 });
1637
- const target = exclusive ? path : `${path}.${randomUUID()}.tmp`;
1638
- let handle;
1639
- let created = false;
914
+ async function readOperationFiles(artifactsRoot) {
915
+ const directory = join(artifactsRoot, "operations");
916
+ const byId = new Map();
917
+ let names;
1640
918
  try {
1641
- handle = await open(target, "wx", 0o600);
1642
- created = true;
1643
- await handle.writeFile(JSON.stringify(value), "utf8");
1644
- await handle.sync();
1645
- await handle.close();
1646
- handle = undefined;
1647
- if (!exclusive)
1648
- await rename(target, path);
1649
- await syncArtifactDirectory(directory);
919
+ names = (await readdir(directory)).filter((name) => name.endsWith(".json"));
1650
920
  }
1651
921
  catch (error) {
1652
- await handle?.close().catch(() => undefined);
1653
- if (created)
1654
- await rm(target, { force: true }).catch(() => undefined);
922
+ if (isMissing(error))
923
+ return byId;
1655
924
  throw error;
1656
925
  }
1657
- }
1658
- function parseArtifactJson(bytes, label) {
1659
- try {
1660
- return JSON.parse(bytes);
1661
- }
1662
- catch (error) {
1663
- throw new ArtifactStoreCorruptionError(`${label} JSON is corrupt`, error instanceof Error ? { cause: error } : undefined);
1664
- }
1665
- }
1666
- async function syncArtifactDirectory(path) {
1667
- const handle = await open(path, "r");
1668
- try {
1669
- await handle.sync();
1670
- }
1671
- finally {
1672
- await handle.close();
926
+ for (const name of names.sort()) {
927
+ const operation = parseArtifactJson(await readFile(join(directory, name), "utf8"), "artifact operation");
928
+ assertOperation(operation);
929
+ if (name !== `${identityHash(operation.context.operationId)}.json`)
930
+ throw new ArtifactStoreCorruptionError("Artifact operation does not match the file-path identity");
931
+ byId.set(operation.context.operationId, operation);
1673
932
  }
933
+ return byId;
1674
934
  }
1675
- function artifactLockDelay(milliseconds) {
1676
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
935
+ function assertWorldDirectoryId(worldId) {
936
+ if (worldId === "" ||
937
+ worldId === "." ||
938
+ worldId === ".." ||
939
+ worldId.includes("/") ||
940
+ worldId.includes("\\"))
941
+ throw new ArtifactStoreInvariantError("Artifact world ID is not a path");
1677
942
  }
1678
943
  export class InMemoryArtifactStore extends FileNativeArtifactStore {
1679
944
  constructor() {
1680
945
  super(join(tmpdir(), "narraeon-ephemeral-artifacts", randomUUID()));
1681
946
  }
1682
947
  }
948
+ function newRecord(input, declaration, payload, identity) {
949
+ const record = {
950
+ schemaVersion: 1,
951
+ recordId: `artifact:${identity.sequence}:${identity.fingerprint.slice(7, 23)}`,
952
+ sequence: identity.sequence,
953
+ worldId: input.context.worldId,
954
+ parentHead: input.context.parentHead,
955
+ operationId: input.context.operationId,
956
+ playPresetId: input.context.playPresetId,
957
+ playPresetRevision: input.context.playPresetRevision,
958
+ playPresetScriptsEnabled: input.context.playPresetScriptsEnabled,
959
+ requestId: input.context.requestId,
960
+ requestAttempt: input.context.requestAttempt,
961
+ output: declaration.name,
962
+ channel: declaration.channel,
963
+ ...(declaration.key === undefined ? {} : { key: declaration.key }),
964
+ contentType: declaration.contentType,
965
+ ...(declaration.renderer === undefined
966
+ ? {}
967
+ : { renderer: declaration.renderer }),
968
+ ...(declaration.rendererRevision === undefined
969
+ ? {}
970
+ : { rendererRevision: declaration.rendererRevision }),
971
+ ...(input.context.extensionControl === undefined
972
+ ? {}
973
+ : {
974
+ extensionControl: structuredClone(input.context.extensionControl),
975
+ }),
976
+ ...(input.context.frozenPresentations?.[declaration.name] !== undefined
977
+ ? {
978
+ frozenPresentation: structuredClone(input.context.frozenPresentations[declaration.name]),
979
+ }
980
+ : input.context.frozenResources === undefined
981
+ ? {}
982
+ : {
983
+ frozenPresentation: {
984
+ ...structuredClone(input.context.frozenResources),
985
+ declaration: structuredClone(declaration),
986
+ },
987
+ }),
988
+ payload: structuredClone(payload),
989
+ save: declaration.save,
990
+ projection: declaration.strategy,
991
+ invalidation: declaration.invalidation,
992
+ toolCallId: input.toolCallId,
993
+ toolName: "artifact_emit",
994
+ callFingerprint: identity.callFingerprint,
995
+ payloadFingerprint: identity.fingerprint,
996
+ };
997
+ return record;
998
+ }
1683
999
  function findDeclaration(context, output) {
1684
1000
  return context.declarations.find(({ name }) => name === output) ?? null;
1685
1001
  }
@@ -1708,17 +1024,6 @@ function normalizePayload(declaration, input) {
1708
1024
  return contract;
1709
1025
  return { ok: true, value: structuredClone(input) };
1710
1026
  }
1711
- function isJsonValue(value) {
1712
- if (value === null || typeof value === "string" || typeof value === "boolean")
1713
- return true;
1714
- if (typeof value === "number")
1715
- return Number.isFinite(value);
1716
- if (Array.isArray(value))
1717
- return value.every(isJsonValue);
1718
- if (typeof value !== "object")
1719
- return false;
1720
- return Object.values(value).every(isJsonValue);
1721
- }
1722
1027
  function byteLength(payload, contentType) {
1723
1028
  return Buffer.byteLength(payloadText(payload, contentType), "utf8");
1724
1029
  }
@@ -1727,9 +1032,6 @@ function payloadText(payload, contentType) {
1727
1032
  return JSON.stringify(payload);
1728
1033
  return typeof payload === "string" ? payload : JSON.stringify(payload);
1729
1034
  }
1730
- function payloadFingerprint(output, payload) {
1731
- return `sha256:${createHash("sha256").update(JSON.stringify({ output, payload })).digest("hex")}`;
1732
- }
1733
1035
  function artifactCallFingerprint(toolName, context, parameters) {
1734
1036
  return `sha256:${createHash("sha256")
1735
1037
  .update(stableSerialize({
@@ -1749,39 +1051,18 @@ function artifactCallFingerprint(toolName, context, parameters) {
1749
1051
  }))
1750
1052
  .digest("hex")}`;
1751
1053
  }
1752
- function stableSerialize(value) {
1753
- if (value === null || typeof value !== "object")
1754
- return JSON.stringify(value);
1755
- if (Array.isArray(value))
1756
- return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
1757
- return `{${Object.keys(value)
1758
- .sort()
1759
- .map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
1760
- .join(",")}}`;
1761
- }
1762
- function rawRecordFingerprint(record) {
1763
- const content = { ...record };
1764
- delete content.recordFingerprint;
1765
- return `sha256:${createHash("sha256").update(JSON.stringify(content)).digest("hex")}`;
1766
- }
1767
- function sameEventIdentity(left, right) {
1768
- return (left.kind === right.kind &&
1769
- left.operationId === right.operationId &&
1770
- left.recordId === right.recordId &&
1771
- left.output === right.output &&
1772
- left.channel === right.channel &&
1773
- left.key === right.key &&
1774
- left.head === right.head &&
1775
- left.requestId === right.requestId &&
1776
- left.requestAttempt === right.requestAttempt &&
1777
- left.reason === right.reason &&
1778
- left.toolCallId === right.toolCallId &&
1779
- left.toolName === right.toolName &&
1780
- left.callFingerprint === right.callFingerprint);
1781
- }
1782
1054
  function artifactFailure(message) {
1783
1055
  return { ok: false, markdown: `# Runtime artifact rejected\n\n${message}` };
1784
1056
  }
1057
+ function recoveryRequired(operationId, completedRequests, message) {
1058
+ return {
1059
+ operationId,
1060
+ status: "recovery_required",
1061
+ completedRequests,
1062
+ ...(message === undefined ? {} : { message }),
1063
+ coreCommitted: false,
1064
+ };
1065
+ }
1785
1066
  function isOpenExtension(operation) {
1786
1067
  return (operation.status === "core_committed" &&
1787
1068
  ["not_started", "running", "unknown", "recovery_required"].includes(operation.extensionStatus));
@@ -1816,12 +1097,6 @@ function isActiveInteraction(operation) {
1816
1097
  (operation.status === "core_committed" &&
1817
1098
  operation.extensionStatus === "running")));
1818
1099
  }
1819
- function identityHash(value) {
1820
- return createHash("sha256").update(value, "utf8").digest("hex");
1821
- }
1822
- function artifactSequenceFileName(sequence) {
1823
- return `${String(sequence).padStart(12, "0")}.json`;
1824
- }
1825
1100
  function summary(operation) {
1826
1101
  return {
1827
1102
  ...(operation.context.attachment === undefined
@@ -1834,9 +1109,7 @@ function summary(operation) {
1834
1109
  status: operation.extensionStatus,
1835
1110
  ...(operation.message === undefined ? {} : { message: operation.message }),
1836
1111
  completedRequests: [...operation.completedRequests],
1837
- coreCommitted: operation.status === "core_materialization_pending" ||
1838
- operation.status === "core_committed" ||
1839
- operation.status === "completed",
1112
+ coreCommitted: operation.status === "core_committed" || operation.status === "completed",
1840
1113
  ...(operation.head === undefined ? {} : { head: operation.head }),
1841
1114
  };
1842
1115
  }
@@ -1844,16 +1117,15 @@ function selectProjection(active) {
1844
1117
  const append = [];
1845
1118
  const replace = new Map();
1846
1119
  const upsert = new Map();
1847
- for (const item of active.sort((left, right) => left.record.sequence - right.record.sequence)) {
1848
- if (item.record.projection === "append" ||
1849
- item.record.projection === "transient")
1850
- append.push(item);
1851
- else if (item.record.projection === "replace")
1852
- replace.set(item.record.channel, item);
1853
- else if (item.record.projection === "upsert")
1854
- upsert.set(`${item.record.channel}\0${item.record.key ?? ""}`, item);
1855
- }
1856
- return [...append, ...replace.values(), ...upsert.values()].sort((left, right) => left.record.sequence - right.record.sequence);
1120
+ for (const record of [...active].sort((left, right) => left.sequence - right.sequence)) {
1121
+ if (record.projection === "append" || record.projection === "transient")
1122
+ append.push(record);
1123
+ else if (record.projection === "replace")
1124
+ replace.set(record.channel, record);
1125
+ else if (record.projection === "upsert")
1126
+ upsert.set(`${record.channel}\0${record.key ?? ""}`, record);
1127
+ }
1128
+ return [...append, ...replace.values(), ...upsert.values()].sort((left, right) => left.sequence - right.sequence);
1857
1129
  }
1858
1130
  function assertContext(context) {
1859
1131
  if (!isRecord(context) ||
@@ -1914,315 +1186,3 @@ function sameContext(left, right) {
1914
1186
  left.playPresetRevision === right.playPresetRevision &&
1915
1187
  left.playPresetScriptsEnabled === right.playPresetScriptsEnabled);
1916
1188
  }
1917
- function assertRawRecord(record) {
1918
- if (!isRecord(record) ||
1919
- !hasExactKeys(record, [
1920
- "schemaVersion",
1921
- "recordId",
1922
- "sequence",
1923
- "worldId",
1924
- "parentHead",
1925
- "newHead",
1926
- "operationId",
1927
- "playPresetId",
1928
- "playPresetRevision",
1929
- "playPresetScriptsEnabled",
1930
- "requestId",
1931
- "requestAttempt",
1932
- "output",
1933
- "channel",
1934
- "contentType",
1935
- "payload",
1936
- "save",
1937
- "projection",
1938
- "invalidation",
1939
- "toolCallId",
1940
- "toolName",
1941
- "callFingerprint",
1942
- "payloadFingerprint",
1943
- "recordFingerprint",
1944
- "status",
1945
- ], [
1946
- "key",
1947
- "renderer",
1948
- "rendererRevision",
1949
- "frozenPresentation",
1950
- "extensionControl",
1951
- ]) ||
1952
- (record.extensionControl !== undefined &&
1953
- !isWorldExtensionControl(record.extensionControl)) ||
1954
- (record.frozenPresentation !== undefined &&
1955
- !isFrozenArtifactPresentation(record.frozenPresentation)) ||
1956
- record.schemaVersion !== 1 ||
1957
- !isNonEmptyString(record.recordId) ||
1958
- !Number.isSafeInteger(record.sequence) ||
1959
- record.sequence < 1 ||
1960
- !isNonEmptyString(record.worldId) ||
1961
- !isNonEmptyString(record.operationId) ||
1962
- !isNonEmptyString(record.output) ||
1963
- !isNonEmptyString(record.channel) ||
1964
- !isNonEmptyString(record.parentHead) ||
1965
- !isNonEmptyString(record.playPresetId) ||
1966
- !isNonEmptyString(record.playPresetRevision) ||
1967
- !isNonEmptyString(record.requestId) ||
1968
- !Number.isSafeInteger(record.requestAttempt) ||
1969
- record.requestAttempt < 1 ||
1970
- record.newHead !== null ||
1971
- typeof record.playPresetScriptsEnabled !== "boolean" ||
1972
- !["text/plain", "text/markdown", "application/json", "text/html"].includes(record.contentType) ||
1973
- !["none", "operation", "commit"].includes(record.save) ||
1974
- !["append", "replace", "upsert", "transient", "hidden"].includes(record.projection) ||
1975
- ![
1976
- "new_operation",
1977
- "head_change",
1978
- "operation_end",
1979
- "explicit_clear",
1980
- "never",
1981
- ].includes(record.invalidation) ||
1982
- typeof record.toolCallId !== "string" ||
1983
- record.toolName !== "artifact_emit" ||
1984
- !isSha256(record.callFingerprint) ||
1985
- !isSha256(record.payloadFingerprint) ||
1986
- !isSha256(record.recordFingerprint) ||
1987
- (record.renderer === undefined) !==
1988
- (record.rendererRevision === undefined) ||
1989
- (record.renderer !== undefined && !isNonEmptyString(record.renderer)) ||
1990
- (record.rendererRevision !== undefined &&
1991
- !isNonEmptyString(record.rendererRevision)) ||
1992
- (record.projection === "upsert"
1993
- ? !isNonEmptyString(record.key)
1994
- : record.key !== undefined) ||
1995
- record.status !== "pending")
1996
- throw new ArtifactStoreCorruptionError("Artifact raw record is corrupt");
1997
- if (!isJsonValue(record.payload))
1998
- throw new ArtifactStoreCorruptionError("Artifact payload is not a JSON value");
1999
- if (record.payloadFingerprint !==
2000
- payloadFingerprint(record.output, record.payload))
2001
- throw new ArtifactStoreCorruptionError("Artifact payload fingerprint does not match");
2002
- if (record.recordFingerprint !== rawRecordFingerprint(record))
2003
- throw new ArtifactStoreCorruptionError("Artifact raw-record fingerprint does not match");
2004
- }
2005
- function assertOperation(operation) {
2006
- const context = operation.context;
2007
- if (!isRecord(operation) ||
2008
- !hasExactKeys(operation, [
2009
- "schemaVersion",
2010
- "context",
2011
- "status",
2012
- "extensionStatus",
2013
- "completedRequests",
2014
- "artifactProjectionStatus",
2015
- "requestAttempts",
2016
- ], ["message", "head", "requests"]) ||
2017
- (operation.requests !== undefined &&
2018
- (!Array.isArray(operation.requests) ||
2019
- !operation.requests.every((item) => isRecord(item) &&
2020
- hasExactKeys(item, [
2021
- "requestId",
2022
- "displayName",
2023
- "mounts",
2024
- "status",
2025
- ]) &&
2026
- isNonEmptyString(item.requestId) &&
2027
- isNonEmptyString(item.displayName) &&
2028
- Array.isArray(item.mounts) &&
2029
- item.mounts.every((mount) => typeof mount === "string" &&
2030
- [
2031
- "story",
2032
- "sidebar",
2033
- "composer_above",
2034
- "composer_below",
2035
- "overlay",
2036
- "debug",
2037
- ].includes(mount)) &&
2038
- ["running", "completed", "failed"].includes(item.status)))) ||
2039
- operation.schemaVersion !== 1 ||
2040
- !isRecord(context) ||
2041
- !hasExactKeys(context, [
2042
- "worldId",
2043
- "parentHead",
2044
- "operationId",
2045
- "playPresetId",
2046
- "playPresetRevision",
2047
- "playPresetScriptsEnabled",
2048
- ], ["attachment"]) ||
2049
- (context.attachment !== undefined &&
2050
- !validAttachment(context.attachment)) ||
2051
- !isNonEmptyString(context.worldId) ||
2052
- !isNonEmptyString(context.parentHead) ||
2053
- !isNonEmptyString(context.operationId) ||
2054
- !isNonEmptyString(context.playPresetId) ||
2055
- !isNonEmptyString(context.playPresetRevision) ||
2056
- typeof context.playPresetScriptsEnabled !== "boolean" ||
2057
- ![
2058
- "running",
2059
- "core_failed",
2060
- "core_materialization_pending",
2061
- "core_committed",
2062
- "completed",
2063
- ].includes(operation.status) ||
2064
- ![
2065
- "not_started",
2066
- "running",
2067
- "completed",
2068
- "failed",
2069
- "cancelled",
2070
- "budget_exceeded",
2071
- "unknown",
2072
- "recovery_required",
2073
- "superseded",
2074
- ].includes(operation.extensionStatus) ||
2075
- !Array.isArray(operation.completedRequests) ||
2076
- !operation.completedRequests.every(isNonEmptyString) ||
2077
- (operation.message !== undefined &&
2078
- typeof operation.message !== "string") ||
2079
- !validArtifactOperationState(operation) ||
2080
- !["active", "superseded"].includes(operation.artifactProjectionStatus) ||
2081
- !isRecord(operation.requestAttempts) ||
2082
- !Object.entries(operation.requestAttempts).every(([request, attempt]) => request.trim() !== "" &&
2083
- typeof attempt === "number" &&
2084
- Number.isSafeInteger(attempt) &&
2085
- attempt >= 1))
2086
- throw new ArtifactStoreCorruptionError("Artifact operation state is corrupt");
2087
- }
2088
- function validArtifactOperationState(operation) {
2089
- if (operation.status === "running")
2090
- return (operation.extensionStatus === "not_started" &&
2091
- operation.completedRequests.length === 0 &&
2092
- operation.message === undefined &&
2093
- operation.head === undefined);
2094
- if (operation.status === "core_failed")
2095
- return (operation.extensionStatus === "failed" &&
2096
- operation.completedRequests.length === 0 &&
2097
- operation.artifactProjectionStatus === "superseded" &&
2098
- operation.head === undefined);
2099
- if (operation.status === "core_materialization_pending")
2100
- return (operation.extensionStatus === "recovery_required" &&
2101
- operation.completedRequests.length === 0 &&
2102
- operation.artifactProjectionStatus === "active" &&
2103
- isNonEmptyString(operation.head) &&
2104
- isNonEmptyString(operation.message));
2105
- if (operation.status === "completed")
2106
- return (operation.extensionStatus === "completed" &&
2107
- isNonEmptyString(operation.head) &&
2108
- operation.message === undefined);
2109
- if (!isNonEmptyString(operation.head))
2110
- return false;
2111
- if (operation.extensionStatus === "completed")
2112
- return false;
2113
- if (operation.extensionStatus === "not_started" ||
2114
- operation.extensionStatus === "running")
2115
- return (operation.completedRequests.length === 0 &&
2116
- operation.message === undefined);
2117
- return (isNonEmptyString(operation.message) &&
2118
- (operation.extensionStatus !== "superseded" ||
2119
- operation.artifactProjectionStatus === "superseded"));
2120
- }
2121
- function assertEvent(event) {
2122
- if (!isRecord(event) ||
2123
- event.schemaVersion !== 1 ||
2124
- !Number.isSafeInteger(event.sequence) ||
2125
- event.sequence < 1 ||
2126
- !isNonEmptyString(event.operationId))
2127
- throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
2128
- const requestPair = event.requestId === undefined && event.requestAttempt === undefined
2129
- ? true
2130
- : isNonEmptyString(event.requestId) &&
2131
- Number.isSafeInteger(event.requestAttempt) &&
2132
- Number(event.requestAttempt) >= 1;
2133
- if (event.kind === "activate") {
2134
- if (!hasExactKeys(event, ["schemaVersion", "kind", "sequence", "operationId", "recordId"], ["requestId", "requestAttempt"]) ||
2135
- !isNonEmptyString(event.recordId) ||
2136
- !requestPair)
2137
- throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
2138
- return;
2139
- }
2140
- if (event.kind === "bind_head") {
2141
- if (!hasExactKeys(event, [
2142
- "schemaVersion",
2143
- "kind",
2144
- "sequence",
2145
- "operationId",
2146
- "recordId",
2147
- "head",
2148
- ]) ||
2149
- !isNonEmptyString(event.recordId) ||
2150
- !isNonEmptyString(event.head))
2151
- throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
2152
- return;
2153
- }
2154
- if (event.kind === "supersede") {
2155
- if (!hasExactKeys(event, [
2156
- "schemaVersion",
2157
- "kind",
2158
- "sequence",
2159
- "operationId",
2160
- "recordId",
2161
- "reason",
2162
- ], ["requestId", "requestAttempt"]) ||
2163
- !isNonEmptyString(event.recordId) ||
2164
- !isNonEmptyString(event.reason) ||
2165
- !requestPair)
2166
- throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
2167
- return;
2168
- }
2169
- if (event.kind === "clear" || event.kind === "ignored_call") {
2170
- if (!hasExactKeys(event, [
2171
- "schemaVersion",
2172
- "kind",
2173
- "sequence",
2174
- "operationId",
2175
- "output",
2176
- "channel",
2177
- "reason",
2178
- "toolCallId",
2179
- "toolName",
2180
- "callFingerprint",
2181
- "requestId",
2182
- "requestAttempt",
2183
- ], ["key"]) ||
2184
- !isNonEmptyString(event.output) ||
2185
- !isNonEmptyString(event.channel) ||
2186
- !isNonEmptyString(event.reason) ||
2187
- !isNonEmptyString(event.toolCallId) ||
2188
- event.toolName !== "artifact_clear" ||
2189
- !isSha256(event.callFingerprint) ||
2190
- !isNonEmptyString(event.requestId) ||
2191
- !Number.isSafeInteger(event.requestAttempt) ||
2192
- Number(event.requestAttempt) < 1 ||
2193
- (event.key !== undefined && !isNonEmptyString(event.key)))
2194
- throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
2195
- return;
2196
- }
2197
- throw new ArtifactStoreCorruptionError("Artifact projection event is corrupt");
2198
- }
2199
- function isRecord(value) {
2200
- return typeof value === "object" && value !== null && !Array.isArray(value);
2201
- }
2202
- function hasExactKeys(value, required, optional = []) {
2203
- const allowed = new Set([...required, ...optional]);
2204
- return (required.every((key) => Object.hasOwn(value, key)) &&
2205
- Object.keys(value).every((key) => allowed.has(key)));
2206
- }
2207
- function isNonEmptyString(value) {
2208
- return typeof value === "string" && value.trim() !== "";
2209
- }
2210
- function isSha256(value) {
2211
- return typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value);
2212
- }
2213
- function isMissing(error) {
2214
- return isRecord(error) && error.code === "ENOENT";
2215
- }
2216
- function isAlreadyExists(error) {
2217
- return isRecord(error) && error.code === "EEXIST";
2218
- }
2219
- function validAttachment(value) {
2220
- return (isRecord(value) &&
2221
- hasExactKeys(value, ["contextId", "eventId", "runId", "head"]) &&
2222
- isNonEmptyString(value.head) &&
2223
- isNonEmptyString(value.contextId) &&
2224
- isNonEmptyString(value.runId) &&
2225
- typeof value.eventId === "number" &&
2226
- Number.isSafeInteger(value.eventId) &&
2227
- value.eventId > 0);
2228
- }