narraeon 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +188 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +447 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-BXfelCi9.js +1207 -0
  70. package/dist/web/assets/index-BYnhUxlk.css +1 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D5VqDvjY.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,5 +1,7 @@
1
- import { createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
1
+ import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
2
+ import { createHash } from "node:crypto";
2
3
  import { stringify } from "yaml";
4
+ import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
3
5
  import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
4
6
  import {} from "../world/WorldDocumentStore.js";
5
7
  import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
@@ -18,20 +20,54 @@ class PlayDocumentToolFailure extends Error {
18
20
  */
19
21
  export class FileNativePlayDocuments {
20
22
  #candidate;
23
+ #committedSnapshot;
21
24
  #reads;
25
+ #declaredDirectories = [];
26
+ #maintenanceWarning;
27
+ get maintenanceWarning() {
28
+ return this.#maintenanceWarning;
29
+ }
30
+ bindMaintenanceUnavailable(reason, locale) {
31
+ this.#maintenance = {};
32
+ this.#maintenanceLocale = locale;
33
+ this.#maintenanceWarning = reason;
34
+ }
35
+ #maintenanceLocale = "en";
36
+ #maintenance = {};
37
+ bindMaintenance(facts, locale = "en") {
38
+ this.#maintenanceWarning = undefined;
39
+ this.#maintenance = structuredClone(facts);
40
+ this.#maintenanceLocale = locale;
41
+ }
22
42
  constructor(files) {
23
43
  this.#candidate = openPlayCandidate({ ...files });
44
+ this.#committedSnapshot = this.#candidate.snapshot;
24
45
  this.#reads = {
25
46
  snapshotId: this.#candidate.snapshot.id,
26
47
  documents: new Map(),
27
- pendingReads: new Map(),
28
48
  };
29
49
  }
30
50
  get snapshot() {
31
51
  return this.#candidate.snapshot;
32
52
  }
33
- bindBootstrap(bootstrap) {
34
- this.#reads = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
53
+ get committedSnapshot() {
54
+ return this.#committedSnapshot;
55
+ }
56
+ bindBootstrap(bootstrap, previousAuthorization) {
57
+ const injected = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
58
+ this.#reads = injected;
59
+ // Refreshing a prefix retains the native conversation. Its exact reads
60
+ // remain valid only on the identical state tree, including our own writes.
61
+ if (previousAuthorization?.stateFingerprint ===
62
+ fingerprintStateFiles(this.#candidate.files)) {
63
+ this.restoreAuthorizationCheckpoint(previousAuthorization);
64
+ for (const [ref, locators] of injected.documents) {
65
+ for (const locator of locators ?? [null])
66
+ authorizeRead(this.#reads, this.#candidate.snapshot.id, ref, locator);
67
+ }
68
+ }
69
+ // Creation capabilities come from this prefix, not past directory slots.
70
+ this.#declaredDirectories = declaredStateDirectories(bootstrap);
35
71
  }
36
72
  authorizationCheckpoint() {
37
73
  return {
@@ -75,62 +111,33 @@ export class FileNativePlayDocuments {
75
111
  this.#reads = {
76
112
  snapshotId: this.#candidate.snapshot.id,
77
113
  documents,
78
- // Provider cursors are deliberately snapshot-local. A partial read has
79
- // granted no write authority yet, so it restarts after cold recovery.
80
- pendingReads: new Map(),
81
114
  };
82
115
  }
83
- contextToolBudgetRequest(call, history) {
84
- const preview = previewContextToolResult(this.#candidate.snapshot, history, call);
85
- if (preview === null)
86
- return null;
87
- const retryParameter = call.name === "context_read" ? "max_bytes" : "limit";
88
- return {
89
- requestedResultBytes: Buffer.byteLength(preview.markdown, "utf8"),
90
- previewMarkdown: preview.markdown,
91
- retryParameter,
92
- retryMinimum: call.name === "context_read" ? 4 : 1,
93
- retryMaximum: contextToolRetryMaximum(call),
94
- restartWithoutCursor: record(call.arguments) && typeof call.arguments.cursor === "string",
95
- };
96
- }
97
- contextToolRetryPreview(call, history, retryValue) {
98
- const argumentsWithoutCursor = record(call.arguments)
99
- ? Object.fromEntries(Object.entries(call.arguments).filter(([key]) => key !== "cursor"))
100
- : {};
101
- const retryCall = {
102
- ...structuredClone(call),
103
- id: `${call.id}-budget-retry`,
104
- arguments: {
105
- ...argumentsWithoutCursor,
106
- [call.name === "context_read" ? "maxBytes" : "limit"]: retryValue,
107
- },
108
- };
109
- const preview = previewContextToolResult(this.#candidate.snapshot, history, retryCall);
110
- if (preview === null)
111
- throw new TypeError("context tool retry preview requires a read tool");
112
- return { call: retryCall, markdown: preview.markdown };
113
- }
114
116
  execute(call, history) {
115
117
  if (call.name === "context_search")
116
118
  return executeContextSearch(this.#candidate.snapshot, history, call.arguments);
117
119
  if (call.name === "state_list")
118
- return executeStateList(this.#candidate.snapshot, history, call.arguments);
120
+ return executeStateList(this.#candidate.snapshot, history, call.arguments, this.#declaredDirectories);
119
121
  if (call.name === "history_list")
120
122
  return executeHistoryList(this.#candidate.snapshot, history, call.arguments);
121
123
  // Frozen contexts created before runtime-tools-v5 retain this exact name
122
124
  // and argument shape. New contexts never advertise it.
123
125
  if (call.name === "context_list")
124
- return executeContextList(this.#candidate.snapshot, history, call.arguments);
126
+ return executeContextList(this.#candidate.snapshot, history, call.arguments, this.#declaredDirectories);
125
127
  if (call.name === "context_read") {
126
128
  const result = executeContextRead(this.#candidate.snapshot, history, call.arguments);
127
129
  authorizeToolRead(this.#reads, this.#candidate.snapshot, result);
130
+ const ref = result.readAuthorization?.shortRef;
131
+ if (ref !== undefined && result.markdown !== undefined)
132
+ result.markdown += `\n\n${this.#maintenanceWarning === undefined ? renderDocumentWritePosition(ref, this.#maintenance[ref], this.#maintenanceLocale) : `${this.#maintenanceLocale === "zh-CN" ? "最近写入位置暂时不可用" : "Last committed write unavailable"}: ${this.#maintenanceWarning}`}`;
128
133
  return result;
129
134
  }
130
135
  if (call.name === "world_patch")
131
136
  return applyPlayCandidatePatch(this.#candidate, this.#reads, call.arguments);
132
137
  if (call.name === "world_create")
133
- return applyPlayCandidateCreate(this.#candidate, this.#reads, call.arguments);
138
+ return applyPlayCandidateCreate(this.#candidate, this.#reads, call.arguments, this.#declaredDirectories);
139
+ if (call.name === "world_retire")
140
+ return applyPlayCandidateRetirement(this.#candidate, this.#reads, call.arguments);
134
141
  return toolFailure(`The current document context does not accept ${call.name}`);
135
142
  }
136
143
  stateChanges() {
@@ -142,15 +149,14 @@ export class FileNativePlayDocuments {
142
149
  * this new committed baseline instead of replaying the old diff.
143
150
  */
144
151
  acceptCommittedState() {
152
+ this.#committedSnapshot = this.#candidate.snapshot;
145
153
  this.#candidate.changes.clear();
146
- this.#candidate.suppliedBytes = 0;
147
154
  }
148
155
  }
149
156
  function bootstrapAuthorizations(bootstrap, snapshot) {
150
157
  const result = {
151
158
  snapshotId: snapshot.id,
152
159
  documents: new Map(),
153
- pendingReads: new Map(),
154
160
  };
155
161
  for (const { status, complete, readAuthorization } of bootstrap.coverage) {
156
162
  if (status !== "resolved" || !complete || readAuthorization === undefined)
@@ -159,7 +165,6 @@ function bootstrapAuthorizations(bootstrap, snapshot) {
159
165
  const resolved = snapshot.query({
160
166
  kind: "read_document",
161
167
  document: { shortRef: readAuthorization.shortRef },
162
- maxBytes: 4,
163
168
  });
164
169
  if (resolved.kind === "read_document" && resolved.ok)
165
170
  authorizeRead(result, snapshot.id, resolved.document.shortRef, null);
@@ -176,6 +181,16 @@ function bootstrapAuthorizations(bootstrap, snapshot) {
176
181
  }
177
182
  return result;
178
183
  }
184
+ function declaredStateDirectories(bootstrap) {
185
+ // Catalog coverage is the durable compiler projection shared by current
186
+ // state_list contexts and legacy context_list contexts.
187
+ return [
188
+ ...new Set(bootstrap.coverage
189
+ .filter(({ slot }) => slot === "catalog")
190
+ .map(({ source }) => source)
191
+ .filter(validStateDirectory)),
192
+ ].sort((left, right) => left.localeCompare(right));
193
+ }
179
194
  function authorizeRead(authorizations, snapshotId, ref, locator) {
180
195
  if (authorizations.snapshotId !== snapshotId)
181
196
  return;
@@ -192,31 +207,7 @@ function authorizeToolRead(authorizations, snapshot, result) {
192
207
  if (authorization?.snapshotId !== snapshot.id ||
193
208
  authorizations.snapshotId !== snapshot.id)
194
209
  return;
195
- const { page } = authorization;
196
- let progress = authorizations.pendingReads.get(authorization.readKey);
197
- if (page.start === 0) {
198
- progress = {
199
- shortRef: authorization.shortRef,
200
- locator: authorization.locator,
201
- nextOffset: page.end,
202
- totalBytes: page.total,
203
- };
204
- authorizations.pendingReads.set(authorization.readKey, progress);
205
- }
206
- else {
207
- if (progress === undefined)
208
- return;
209
- if (progress.shortRef !== authorization.shortRef ||
210
- !sameLocator(progress.locator, authorization.locator) ||
211
- progress.nextOffset !== page.start ||
212
- progress.totalBytes !== page.total)
213
- return;
214
- progress.nextOffset = page.end;
215
- }
216
- if (progress.nextOffset !== progress.totalBytes)
217
- return;
218
- authorizeRead(authorizations, snapshot.id, progress.shortRef, progress.locator);
219
- authorizations.pendingReads.delete(authorization.readKey);
210
+ authorizeRead(authorizations, snapshot.id, authorization.shortRef, authorization.locator);
220
211
  }
221
212
  function sameLocator(left, right) {
222
213
  if (left === null || right === null)
@@ -231,16 +222,12 @@ function parseYamlHandleSegment(segment) {
231
222
  return /^(?:0|[1-9][0-9]*)$/u.test(segment) ? Number(segment) : segment;
232
223
  }
233
224
  function openPlayCandidate(files) {
234
- return {
235
- ...beginWorldStateRevision(files),
236
- suppliedBytes: 0,
237
- };
225
+ return beginWorldStateRevision(files);
238
226
  }
239
227
  function documentDescriptorByRef(snapshot, ref) {
240
228
  const result = snapshot.query({
241
229
  kind: "read_document",
242
230
  document: { shortRef: ref },
243
- maxBytes: 4,
244
231
  });
245
232
  return result.kind === "read_document" && result.ok ? result.document : null;
246
233
  }
@@ -255,17 +242,11 @@ function applyPlayCandidatePatch(candidate, reads, args) {
255
242
  const target = documentDescriptorByRef(candidate.snapshot, ref);
256
243
  if (target === null)
257
244
  return toolFailure(`Target @${ref} does not exist.`);
258
- const retainedAuthorization = reads.documents.get(ref);
245
+ const retainedAuthorization = authorizationsAfterEdits(reads.documents.get(ref), args.edits);
259
246
  if (reads.snapshotId !== candidate.snapshot.id ||
260
- retainedAuthorization === undefined ||
261
- !editsAreAuthorized(retainedAuthorization, args.edits))
247
+ retainedAuthorization === undefined)
262
248
  return toolFailure(`Read @${ref} exactly, or receive the complete document in bootstrap, before writing it.`);
263
249
  try {
264
- const suppliedBytes = Buffer.byteLength(JSON.stringify(args.edits), "utf8");
265
- if (suppliedBytes > 64 * 1024)
266
- throw new PlayDocumentToolFailure("A single patch body exceeds 64 KiB");
267
- if (candidate.suppliedBytes + suppliedBytes > 256 * 1024)
268
- throw new PlayDocumentToolFailure("New body content for this model operation exceeds 256 KiB in total");
269
250
  assertToolReferenceHandles(args.edits);
270
251
  const revised = candidate.snapshot.revise({
271
252
  commands: [
@@ -282,13 +263,19 @@ function applyPlayCandidatePatch(candidate, reads, args) {
282
263
  const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes);
283
264
  const previousSnapshot = candidate.snapshot;
284
265
  acceptWorldStateRevision(candidate, revised);
285
- candidate.suppliedBytes += suppliedBytes;
286
266
  carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
287
267
  if (retainedAuthorization === null)
288
268
  authorizeRead(reads, candidate.snapshot.id, ref, null);
289
269
  else
290
- for (const locator of retainedAuthorization)
291
- authorizeRead(reads, candidate.snapshot.id, ref, locator);
270
+ for (const locator of retainedAuthorization) {
271
+ const selected = candidate.snapshot.query({
272
+ kind: "select_node",
273
+ document: { shortRef: ref },
274
+ locator,
275
+ });
276
+ if (selected.kind === "select_node" && selected.ok)
277
+ authorizeRead(reads, candidate.snapshot.id, ref, selected.node.locator);
278
+ }
292
279
  return {
293
280
  ok: true,
294
281
  markdown: receipt,
@@ -301,26 +288,53 @@ function applyPlayCandidatePatch(candidate, reads, args) {
301
288
  throw error;
302
289
  }
303
290
  }
304
- function editsAreAuthorized(authorization, edits) {
305
- if (authorization === null)
306
- return true;
307
- if (authorization === undefined)
308
- return false;
309
- return edits.every((edit) => {
291
+ function authorizationsAfterEdits(authorization, edits) {
292
+ if (authorization === null || authorization === undefined)
293
+ return authorization;
294
+ for (const edit of edits) {
310
295
  if (!record(edit) || edit.op === "set_metadata" || !record(edit.locator))
311
- return false;
296
+ return undefined;
312
297
  const requested = Array.isArray(edit.locator.yaml)
313
298
  ? { codec: "yaml", path: edit.locator.yaml }
314
299
  : Array.isArray(edit.locator.markdown)
315
300
  ? { codec: "markdown", path: edit.locator.markdown }
316
301
  : null;
317
- return (requested !== null &&
318
- authorization.some((allowed) => {
302
+ if (requested === null ||
303
+ !authorization.some((allowed) => {
319
304
  const path = "yaml" in allowed ? allowed.yaml : allowed.markdown;
320
305
  return (requested.codec in allowed &&
321
306
  path.every((segment, index) => requested.path[index] === segment));
322
- }));
323
- });
307
+ }))
308
+ return undefined;
309
+ // A removed/replaced subtree or shifted array address is no longer proof
310
+ // about its old descendants. Check each edit before moving to the next so
311
+ // a batch cannot use a deleted array item to overwrite its unread successor.
312
+ authorization = authorization.filter((allowed) => {
313
+ if (!(requested.codec in allowed))
314
+ return true;
315
+ const path = "yaml" in allowed ? allowed.yaml : allowed.markdown;
316
+ const withinEdit = requested.path.every((segment, index) => path[index] === segment);
317
+ if (withinEdit &&
318
+ ["remove", "remove_section", "rename_section"].includes(String(edit.op)))
319
+ return false;
320
+ if (withinEdit &&
321
+ path.length > requested.path.length &&
322
+ ["replace", "replace_section"].includes(String(edit.op)))
323
+ return false;
324
+ if (edit.op === "remove" && requested.codec === "yaml") {
325
+ const index = requested.path.length - 1;
326
+ const removed = requested.path[index];
327
+ const current = path[index];
328
+ if (typeof removed === "number" &&
329
+ typeof current === "number" &&
330
+ current >= removed &&
331
+ requested.path.slice(0, -1).every((segment, i) => path[i] === segment))
332
+ return false;
333
+ }
334
+ return true;
335
+ });
336
+ }
337
+ return authorization;
324
338
  }
325
339
  function formatToolRevisionReceipt(shortRef, changes) {
326
340
  if (changes.length > 1)
@@ -329,6 +343,43 @@ function formatToolRevisionReceipt(shortRef, changes) {
329
343
  ? `@${shortRef} pending write`
330
344
  : `@${shortRef} unchanged`;
331
345
  }
346
+ function applyPlayCandidateRetirement(candidate, reads, args) {
347
+ if (!record(args) ||
348
+ !hasOnlyToolKeys(args, ["target", "retired"]) ||
349
+ typeof args.target !== "string" ||
350
+ typeof args.retired !== "boolean")
351
+ return toolFailure("world_retire requires exactly a target and boolean retired value.");
352
+ const ref = args.target.replace(/^@/u, "");
353
+ const target = documentDescriptorByRef(candidate.snapshot, ref);
354
+ if (target === null)
355
+ return toolFailure(`Target @${ref} does not exist.`);
356
+ if (reads.snapshotId !== candidate.snapshot.id ||
357
+ reads.documents.get(ref) !== null)
358
+ return toolFailure(`Read @${ref} exactly, or receive the complete document in bootstrap, before retiring or restoring it.`);
359
+ const revised = candidate.snapshot.revise({
360
+ commands: [
361
+ {
362
+ kind: "retire",
363
+ document: { documentId: target.documentId },
364
+ retired: args.retired,
365
+ },
366
+ ],
367
+ });
368
+ if (!revised.ok || revised.snapshotStatus !== "usable")
369
+ return toolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
370
+ const changed = revised.changes.length > 0;
371
+ const previousSnapshot = candidate.snapshot;
372
+ acceptWorldStateRevision(candidate, revised);
373
+ carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
374
+ authorizeRead(reads, candidate.snapshot.id, ref, null);
375
+ return {
376
+ ok: true,
377
+ markdown: changed
378
+ ? `@${target.shortRef} pending ${args.retired ? "retirement" : "restoration"}`
379
+ : `@${target.shortRef} unchanged`,
380
+ candidateWrite: { shortRef: target.shortRef, changed },
381
+ };
382
+ }
332
383
  function nextAvailableStatePath(candidate, parentDirectory, refHint, codec) {
333
384
  const directory = parentDirectory === "" ? "" : `${parentDirectory}/`;
334
385
  const extension = codec === "yaml" ? "yaml" : "md";
@@ -363,7 +414,7 @@ function assertToolReferenceHandles(value, seen = new WeakSet()) {
363
414
  assertToolReferenceHandles(item, seen);
364
415
  seen.delete(value);
365
416
  }
366
- function applyPlayCandidateCreate(candidate, authorizations, args) {
417
+ function applyPlayCandidateCreate(candidate, authorizations, args, declaredDirectories) {
367
418
  try {
368
419
  const parentDirectory = record(args) && typeof args.parent === "string"
369
420
  ? parseStateDirectoryHandle(args.parent)
@@ -380,11 +431,8 @@ function applyPlayCandidateCreate(candidate, authorizations, args) {
380
431
  !args.aliases.every((alias) => typeof alias === "string") ||
381
432
  typeof args.body !== "string")
382
433
  throw new PlayDocumentToolFailure("world_create arguments are invalid");
383
- const suppliedBytes = Buffer.byteLength(args.body, "utf8");
384
- if (suppliedBytes > 64 * 1024)
385
- throw new PlayDocumentToolFailure("The world_create body exceeds 64 KiB");
386
- if (candidate.suppliedBytes + suppliedBytes > 256 * 1024)
387
- throw new PlayDocumentToolFailure("New body content for this model operation exceeds 256 KiB in total");
434
+ if (!knownStateDirectory(candidate.snapshot, parentDirectory, declaredDirectories))
435
+ throw new PlayDocumentToolFailure("world_create parent is not a Runtime-known state directory; call state_list and use one of its directory handles");
388
436
  const path = nextAvailableStatePath(candidate, parentDirectory, args.refHint, args.codec);
389
437
  const command = args.codec === "yaml"
390
438
  ? {
@@ -423,7 +471,6 @@ function applyPlayCandidateCreate(candidate, authorizations, args) {
423
471
  acceptWorldStateRevision(candidate, revised);
424
472
  carryWriteAuthorizations(previousSnapshot, candidate, authorizations, revised.changes);
425
473
  authorizeRead(authorizations, candidate.snapshot.id, created.shortRef, null);
426
- candidate.suppliedBytes += suppliedBytes;
427
474
  return {
428
475
  ok: true,
429
476
  markdown: `@${created.shortRef} pending write`,
@@ -453,9 +500,6 @@ function carryWriteAuthorizations(previousSnapshot, candidate, authorizations, c
453
500
  authorizations.snapshotId = candidate.snapshot.id;
454
501
  for (const ref of stale)
455
502
  authorizations.documents.delete(ref);
456
- for (const [readKey, progress] of authorizations.pendingReads)
457
- if (stale.has(progress.shortRef))
458
- authorizations.pendingReads.delete(readKey);
459
503
  }
460
504
  function playCandidateStateChanges(candidate) {
461
505
  return fileNativeStateChanges(candidate);
@@ -464,7 +508,6 @@ function documentById(snapshot, id) {
464
508
  const result = snapshot.query({
465
509
  kind: "read_document",
466
510
  document: { documentId: id },
467
- maxBytes: 4,
468
511
  });
469
512
  return result.kind === "read_document" && result.ok ? result.document : null;
470
513
  }
@@ -475,36 +518,6 @@ function toolFailure(message, failureKind = "protocol") {
475
518
  failureKind,
476
519
  };
477
520
  }
478
- function previewContextToolResult(snapshot, history, call) {
479
- if (call.name === "state_list")
480
- return executeStateList(snapshot, history, call.arguments);
481
- if (call.name === "history_list")
482
- return executeHistoryList(snapshot, history, call.arguments);
483
- if (call.name === "context_list")
484
- return executeContextList(snapshot, history, call.arguments);
485
- if (call.name === "context_search")
486
- return executeContextSearch(snapshot, history, call.arguments);
487
- if (call.name === "context_read")
488
- return executeContextRead(snapshot, history, call.arguments);
489
- return null;
490
- }
491
- function contextToolRetryMaximum(call) {
492
- if (!record(call.arguments))
493
- return call.name === "context_read" ? 8_192 : 1;
494
- if (call.name === "context_read")
495
- return Number.isInteger(call.arguments.maxBytes) &&
496
- Number(call.arguments.maxBytes) >= 4 &&
497
- Number(call.arguments.maxBytes) <= 8_192
498
- ? Number(call.arguments.maxBytes)
499
- : 8_192;
500
- const defaultLimit = call.name === "context_search" ? 10 : 20;
501
- const maximumLimit = call.name === "context_search" ? 50 : 100;
502
- return Number.isInteger(call.arguments.limit) &&
503
- Number(call.arguments.limit) >= 1 &&
504
- Number(call.arguments.limit) <= maximumLimit
505
- ? Number(call.arguments.limit)
506
- : defaultLimit;
507
- }
508
521
  function executeContextSearch(snapshot, history, args) {
509
522
  if (!record(args) ||
510
523
  !hasOnlyToolKeys(args, [
@@ -650,7 +663,26 @@ function parseStateDirectoryHandle(handle) {
650
663
  return null;
651
664
  }
652
665
  }
653
- function executeStateList(snapshot, history, args) {
666
+ function validStateDirectory(directory) {
667
+ if (directory.length === 0)
668
+ return false;
669
+ try {
670
+ return (parseStateDirectoryHandle(stateDirectoryHandle(directory)) === directory);
671
+ }
672
+ catch {
673
+ return false;
674
+ }
675
+ }
676
+ function knownStateDirectory(snapshot, directory, declaredDirectories) {
677
+ if (directory === "")
678
+ return true;
679
+ const relativePrefix = `${directory}/`;
680
+ if (declaredDirectories.some((declared) => declared === directory || declared.startsWith(relativePrefix)))
681
+ return true;
682
+ const logicalPrefix = `${snapshot.logicalRoot}/${relativePrefix}`;
683
+ return snapshot.files.some(({ path }) => path.startsWith(logicalPrefix));
684
+ }
685
+ function executeStateList(snapshot, history, args, declaredDirectories) {
654
686
  if (!record(args) ||
655
687
  !hasOnlyToolKeys(args, ["parent", "cursor", "limit"]) ||
656
688
  typeof args.parent !== "string" ||
@@ -661,7 +693,7 @@ function executeStateList(snapshot, history, args) {
661
693
  ok: false,
662
694
  markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime and accepts only cursor and limit in addition; use history_list for committed history.",
663
695
  };
664
- return executeContextList(snapshot, history, { ...args, source: "state" });
696
+ return executeContextList(snapshot, history, { ...args, source: "state" }, declaredDirectories);
665
697
  }
666
698
  function executeHistoryList(snapshot, history, args) {
667
699
  if (!record(args) ||
@@ -675,7 +707,7 @@ function executeHistoryList(snapshot, history, args) {
675
707
  };
676
708
  return executeContextList(snapshot, history, { ...args, source: "history" });
677
709
  }
678
- function executeContextList(snapshot, history, args) {
710
+ function executeContextList(snapshot, history, args, declaredDirectories = []) {
679
711
  if (!record(args) || (args.source !== "state" && args.source !== "history"))
680
712
  return {
681
713
  ok: false,
@@ -709,9 +741,16 @@ function executeContextList(snapshot, history, args) {
709
741
  const directory = parseStateDirectoryHandle(parent);
710
742
  if (directory === null)
711
743
  return unexpectedStateQueryResult();
744
+ if (!knownStateDirectory(snapshot, directory, declaredDirectories))
745
+ return {
746
+ ok: false,
747
+ markdown: "# Runtime argument error\n\nThe parent is not a Runtime-known state directory; call state_list and descend only through its returned directory handles.",
748
+ };
712
749
  const result = snapshot.query({
713
750
  kind: "catalog",
714
751
  directory,
752
+ includeRetired: true,
753
+ declaredDirectories,
715
754
  limit,
716
755
  ...(typeof args.cursor === "string" || args.cursor === null
717
756
  ? { cursor: args.cursor }
@@ -728,7 +767,7 @@ function executeContextList(snapshot, history, args) {
728
767
  }
729
768
  if (entry.document === undefined)
730
769
  return "- Damaged document (currently not addressable)";
731
- return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.status === "damaged" ? " · needs repair" : ""}`;
770
+ return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
732
771
  });
733
772
  return {
734
773
  ok: true,
@@ -761,24 +800,22 @@ function executeContextList(snapshot, history, args) {
761
800
  }
762
801
  function executeContextRead(snapshot, history, args) {
763
802
  if (!record(args) ||
803
+ // Frozen contexts created before complete exact reads still advertise
804
+ // cursor/maxBytes. Accept and ignore those obsolete arguments so an
805
+ // existing Provider continuation can cross the upgrade boundary.
764
806
  !hasOnlyToolKeys(args, ["ref", "cursor", "maxBytes"]) ||
765
- typeof args.ref !== "string" ||
766
- (args.maxBytes !== undefined && typeof args.maxBytes !== "number") ||
767
- !validOptionalCursor(args.cursor))
807
+ typeof args.ref !== "string")
768
808
  return {
769
809
  ok: false,
770
810
  markdown: "# Runtime argument error\n\nA stable ref returned by list or search is required.",
771
811
  };
772
- const maxBytes = typeof args.maxBytes === "number" ? args.maxBytes : 8192;
773
- if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > 8192)
774
- return {
775
- ok: false,
776
- markdown: "# Runtime argument error\n\nmaxBytes must be between 4 and 8192.",
777
- };
778
812
  const historyRefValue = args.ref.replace(/^@/u, "");
779
813
  const historyEntry = history.find(({ path }) => historyRef(path) === historyRefValue);
780
814
  if (historyEntry !== undefined)
781
- return pagedRead(historyRefValue, historyEntry.contents, maxBytes, args.cursor);
815
+ return {
816
+ ok: true,
817
+ markdown: `# Exact read @${historyRefValue}\n\n${historyEntry.contents}\n\n---\nSource: @${historyRefValue}\nComplete: yes`,
818
+ };
782
819
  const handle = parseStateReadHandle(args.ref);
783
820
  if (handle === null)
784
821
  return {
@@ -788,7 +825,6 @@ function executeContextRead(snapshot, history, args) {
788
825
  const resolved = snapshot.query({
789
826
  kind: "read_document",
790
827
  document: { shortRef: handle.shortRef },
791
- maxBytes: 4,
792
828
  });
793
829
  if (resolved.kind === "error")
794
830
  return renderStateQueryFailure(resolved);
@@ -796,19 +832,17 @@ function executeContextRead(snapshot, history, args) {
796
832
  return unexpectedStateQueryResult();
797
833
  if (handle.path === null)
798
834
  return resolved.codec === "markdown"
799
- ? readMarkdownDocument(snapshot, resolved.document, maxBytes, args.cursor)
800
- : readProjectedNode(snapshot, resolved.document, { yaml: [] }, args.ref, maxBytes, args.cursor, true);
835
+ ? readMarkdownDocument(snapshot, resolved.document)
836
+ : readProjectedNode(snapshot, resolved.document, { yaml: [] }, args.ref, true);
801
837
  const locator = resolved.codec === "yaml"
802
838
  ? { yaml: handle.path.map(parseYamlHandleSegment) }
803
839
  : { markdown: handle.path };
804
- return readProjectedNode(snapshot, resolved.document, locator, args.ref, maxBytes, args.cursor, false);
840
+ return readProjectedNode(snapshot, resolved.document, locator, args.ref, false);
805
841
  }
806
- function readMarkdownDocument(snapshot, document, maxBytes, cursor) {
842
+ function readMarkdownDocument(snapshot, document) {
807
843
  const result = snapshot.query({
808
844
  kind: "read_document",
809
845
  document: { shortRef: document.shortRef },
810
- maxBytes,
811
- ...(typeof cursor === "string" || cursor === null ? { cursor } : {}),
812
846
  });
813
847
  if (result.kind === "error")
814
848
  return renderStateQueryFailure(result);
@@ -816,21 +850,15 @@ function readMarkdownDocument(snapshot, document, maxBytes, cursor) {
816
850
  return unexpectedStateQueryResult();
817
851
  return {
818
852
  ok: true,
819
- markdown: `# Exact read @${document.shortRef}\n\n${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${result.body.trimEnd()}\n[Writable body ${result.page.complete ? "ends" : "continues"}]\n\n---\nScope: state · @${document.shortRef}\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} bytes\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
853
+ markdown: `# Exact read @${document.shortRef}\n\n${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${result.body.trimEnd()}\n[Writable body ends]\n\n${renderPlayerViewBindings(snapshot, document.shortRef)}\n\n---\nScope: state · @${document.shortRef}\nComplete: yes`,
820
854
  readAuthorization: {
821
855
  snapshotId: snapshot.id,
822
- readKey: readAuthorizationKey(document.shortRef, null, maxBytes),
823
856
  shortRef: document.shortRef,
824
857
  locator: null,
825
- page: {
826
- start: result.page.start,
827
- end: result.page.end,
828
- total: result.page.total,
829
- },
830
858
  },
831
859
  };
832
860
  }
833
- function readProjectedNode(snapshot, document, locator, requestedHandle, maxBytes, cursor, wholeDocument) {
861
+ function readProjectedNode(snapshot, document, locator, requestedHandle, wholeDocument) {
834
862
  const result = snapshot.query({
835
863
  kind: "select_node",
836
864
  document: { shortRef: document.shortRef },
@@ -849,15 +877,24 @@ function readProjectedNode(snapshot, document, locator, requestedHandle, maxByte
849
877
  const text = wholeDocument
850
878
  ? `${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${body}\n[Writable body ends]\n`
851
879
  : `# ${document.title} · ${renderLocator(result.node.locator)} [${requestedHandle}]\n\n${body}\n`;
852
- return pagedStateProjection(snapshot, requestedHandle, text, maxBytes, cursor, document.shortRef, wholeDocument ? null : result.node.locator);
880
+ return {
881
+ ok: true,
882
+ markdown: `# Exact read ${requestedHandle}\n\n${text.trimEnd()}\n\n${renderPlayerViewBindings(snapshot, document.shortRef)}\n\n---\nScope: state · ${requestedHandle}\nComplete: yes`,
883
+ readAuthorization: {
884
+ snapshotId: snapshot.id,
885
+ shortRef: document.shortRef,
886
+ locator: wholeDocument ? null : result.node.locator,
887
+ },
888
+ };
853
889
  }
854
890
  function renderDocumentMetadata(document) {
855
891
  const metadata = stringify({
856
892
  title: document.title,
857
893
  summary: document.summary,
858
894
  aliases: document.aliases,
895
+ ...(document.retired ? { retired: true } : {}),
859
896
  }, { indent: 2, lineWidth: 0 }).trimEnd();
860
- return `[Document metadata: not body content; use set_metadata to replace the entire group]\n${metadata}`;
897
+ return `[Document metadata: not body content; set_metadata updates one or more fields and preserves omitted fields; retired is changed only by world_retire]\n${metadata}`;
861
898
  }
862
899
  function toolWorldDocumentValue(value) {
863
900
  if (Array.isArray(value))
@@ -873,98 +910,6 @@ function toolWorldDocumentValue(value) {
873
910
  toolWorldDocumentValue(child),
874
911
  ]));
875
912
  }
876
- function pagedStateProjection(snapshot, handle, text, maxBytes, cursor, shortRef, locator) {
877
- const scope = JSON.stringify({
878
- kind: "state_projected_read",
879
- snapshotId: snapshot.id,
880
- handle,
881
- maxBytes,
882
- });
883
- const offset = parseStateProjectionCursor(snapshot, cursor, scope);
884
- const bytes = Buffer.from(text, "utf8");
885
- if (offset === null || offset > bytes.length)
886
- return {
887
- ok: false,
888
- markdown: "# Runtime argument error\n\nThe cursor does not match the current snapshot or complete query criteria.",
889
- };
890
- const end = safeUtf8ReadEnd(bytes, offset, maxBytes);
891
- const complete = end === bytes.length;
892
- const page = bytes.subarray(offset, end).toString("utf8");
893
- return {
894
- ok: true,
895
- markdown: `# Exact read ${handle}\n\n${page}\n\n---\nScope: state · ${handle}\nThis page: ${offset}..${end} / ${bytes.length} bytes\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${stateProjectionCursorFor(snapshot, scope, end)}`}`,
896
- readAuthorization: {
897
- snapshotId: snapshot.id,
898
- readKey: readAuthorizationKey(shortRef, locator, maxBytes),
899
- shortRef,
900
- locator,
901
- page: { start: offset, end, total: bytes.length },
902
- },
903
- };
904
- }
905
- function readAuthorizationKey(shortRef, locator, maxBytes) {
906
- return JSON.stringify({ shortRef, locator, maxBytes });
907
- }
908
- const stateProjectionCursorSecrets = new WeakMap();
909
- function stateProjectionCursorFor(snapshot, scope, offset) {
910
- const payload = Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
911
- const signature = createHmac("sha256", stateProjectionCursorSecret(snapshot))
912
- .update(snapshot.id)
913
- .update("\0")
914
- .update(scope)
915
- .update("\0")
916
- .update(payload)
917
- .digest("base64url");
918
- return `${payload}.${signature}`;
919
- }
920
- function parseStateProjectionCursor(snapshot, cursor, scope) {
921
- if (cursor === undefined || cursor === null)
922
- return 0;
923
- if (typeof cursor !== "string")
924
- return null;
925
- const parts = cursor.split(".");
926
- if (parts.length !== 2)
927
- return null;
928
- const [payload, suppliedSignature] = parts;
929
- if (payload === undefined || suppliedSignature === undefined)
930
- return null;
931
- try {
932
- const supplied = Buffer.from(suppliedSignature, "base64url");
933
- if (supplied.toString("base64url") !== suppliedSignature)
934
- return null;
935
- const expected = createHmac("sha256", stateProjectionCursorSecret(snapshot))
936
- .update(snapshot.id)
937
- .update("\0")
938
- .update(scope)
939
- .update("\0")
940
- .update(payload)
941
- .digest();
942
- if (supplied.length !== expected.length ||
943
- !timingSafeEqual(supplied, expected))
944
- return null;
945
- const decoded = Buffer.from(payload, "base64url");
946
- if (decoded.toString("base64url") !== payload)
947
- return null;
948
- const value = JSON.parse(decoded.toString("utf8"));
949
- return record(value) &&
950
- Object.keys(value).length === 1 &&
951
- Number.isSafeInteger(value.offset) &&
952
- Number(value.offset) >= 0
953
- ? Number(value.offset)
954
- : null;
955
- }
956
- catch {
957
- return null;
958
- }
959
- }
960
- function stateProjectionCursorSecret(snapshot) {
961
- const existing = stateProjectionCursorSecrets.get(snapshot);
962
- if (existing !== undefined)
963
- return existing;
964
- const created = randomBytes(32);
965
- stateProjectionCursorSecrets.set(snapshot, created);
966
- return created;
967
- }
968
913
  function parseStateReadHandle(value) {
969
914
  const match = /^@([a-z][a-z0-9-]*)(?:#\/(.+))?$/u.exec(value);
970
915
  if (match === null)
@@ -1026,33 +971,6 @@ function snippet(text, query) {
1026
971
  function historyRef(path) {
1027
972
  return `history-message-${path.replace(/\.md$/u, "")}`;
1028
973
  }
1029
- function pagedRead(ref, text, maxBytes, cursor) {
1030
- const scope = JSON.stringify({
1031
- kind: "read",
1032
- ref,
1033
- sha256: createHash("sha256").update(text).digest("hex"),
1034
- maxBytes,
1035
- });
1036
- const offset = parseCursor(cursor, scope);
1037
- if (offset === null)
1038
- return {
1039
- ok: false,
1040
- markdown: "# Runtime argument error\n\nThe cursor does not match the current read.",
1041
- };
1042
- const bytes = Buffer.from(text, "utf8");
1043
- const end = safeUtf8ReadEnd(bytes, offset, maxBytes);
1044
- const page = bytes.subarray(offset, end).toString("utf8");
1045
- return {
1046
- ok: true,
1047
- markdown: `# Exact read @${ref}\n\n${page}\n\n---\nSource: @${ref}\nThis page: ${offset}..${end} bytes\nComplete: ${end === bytes.length ? "yes" : "no"}\n${end === bytes.length ? "" : `Next-page cursor: ${cursorFor(scope, end)}`}`,
1048
- };
1049
- }
1050
- function safeUtf8ReadEnd(bytes, offset, maxBytes) {
1051
- let end = Math.min(bytes.length, offset + maxBytes);
1052
- while (end > offset && (bytes[end] ?? 0) >= 0x80 && (bytes[end] ?? 0) < 0xc0)
1053
- end -= 1;
1054
- return end;
1055
- }
1056
974
  function cursorFor(scope, offset) {
1057
975
  return Buffer.from(JSON.stringify({
1058
976
  scope: createHash("sha256").update(scope).digest("hex"),