narraeon 0.7.6 → 0.7.9

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 (29) hide show
  1. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +14 -1
  2. package/dist/node/runtime/history/HistoryRendering.js +25 -6
  3. package/dist/node/runtime/history/HistoryTools.js +31 -0
  4. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +12 -4
  5. package/dist/node/runtime/play/PlayCallChain.js +10 -21
  6. package/dist/node/runtime/play/PlayDocumentTools.js +72 -43
  7. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +35 -19
  8. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -12
  9. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +0 -67
  10. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +326 -62
  11. package/dist/node/runtime/setting/SettingImprovementSession.js +40 -3
  12. package/dist/node/runtime/world/FileNativeWorldStore.js +13 -3
  13. package/dist/node/runtime/world/WorldDocumentMaintenance.js +9 -2
  14. package/dist/node/runtime/world/WorldDocumentStore.js +441 -389
  15. package/dist/node/runtime/world/WorldLedgerAuthority.js +18 -2
  16. package/dist/node/runtime/world/WorldStateRevision.js +6 -3
  17. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +61 -17
  18. package/dist/node/runtime/world-revision/LegacyWorldRevisionEpochUpgrade.js +46 -0
  19. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +83 -30
  20. package/dist/node/runtime/world-revision/WorldRevisionSession.js +64 -4
  21. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +32 -11
  22. package/dist/node/shared/default-setting-improvement-prompt.js +23 -8
  23. package/dist/node/shared/ordered-author-prompts.js +9 -7
  24. package/dist/node/shared/ordered-play-prompts.js +2 -2
  25. package/dist/web/assets/index-B2kbaOGe.js +1408 -0
  26. package/dist/web/assets/{index-BjxBYYQi.css → index-DH2BVsYd.css} +1 -1
  27. package/dist/web/index.html +2 -2
  28. package/package.json +1 -1
  29. package/dist/web/assets/index-Cj2JYCia.js +0 -1408
@@ -1,4 +1,15 @@
1
1
  import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
2
+ export function isAuthoringPreloadRecord(value) {
3
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4
+ return false;
5
+ const record = value;
6
+ return (Object.keys(record).length === 3 &&
7
+ typeof record.revision === "string" &&
8
+ Array.isArray(record.worldDocumentIds) &&
9
+ record.worldDocumentIds.every((id) => typeof id === "string") &&
10
+ Array.isArray(record.opaquePaths) &&
11
+ record.opaquePaths.every((path) => typeof path === "string"));
12
+ }
2
13
  export function validAuthoringRequests(value, validPrompt, session) {
3
14
  if (!Array.isArray(value) ||
4
15
  value.length === 0 ||
@@ -14,7 +25,9 @@ export function validAuthoringRequests(value, validPrompt, session) {
14
25
  if (typeof raw !== "object" || raw === null || Array.isArray(raw))
15
26
  return false;
16
27
  const item = raw;
17
- if (Object.keys(item).length !== 4 ||
28
+ const keys = Object.keys(item).length;
29
+ if ((keys !== 4 && !(keys === 5 && "preload" in item)) ||
30
+ (item.preload !== undefined && !isAuthoringPreloadRecord(item.preload)) ||
18
31
  typeof item.requestId !== "string" ||
19
32
  !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(item.requestId) ||
20
33
  ids.has(item.requestId) ||
@@ -1,5 +1,15 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { parseDocument } from "yaml";
3
+ /** Scope and total only; ordering and continuation rules live in tool descriptions. */
4
+ function compactHeader(total, locale) {
5
+ const zh = locale === "zh-CN";
6
+ return `Scope: ${zh ? "当前时间线" : "current timeline"}\n${zh ? "本次读取快照消息总数" : "Snapshot total messages"}: ${total}`;
7
+ }
8
+ /** Position, role and ref only; role=x would repeat the localized role label. */
9
+ function compactMessageLabel(message, locale) {
10
+ const zh = locale === "zh-CN";
11
+ return `${zh ? "位置" : "position"} ${message.position} · ${historyRoleLabel(message, locale)}${message.isOpening ? " · isOpening=true" : ""} · ${message.ref} · ${message.length} Unicode code points`;
12
+ }
3
13
  /** Shared metadata keeps injected refs and tool results directly navigable. */
4
14
  export function historyMessageLabel(message, locale) {
5
15
  const zh = locale === "zh-CN";
@@ -58,18 +68,26 @@ export function renderHistoryResult(result, locale = "en", legacyRead = false) {
58
68
  if (result.kind === "list")
59
69
  return {
60
70
  ok: true,
61
- markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${header}\nOrder: oldest_first (${zh ? "页内始终由旧到新" : "every page oldest to newest"})\n${result.messages
71
+ markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${compactHeader(result.total, locale)}\n${result.messages
62
72
  .map((message) => {
63
73
  const preview = Array.from(message.text).slice(0, 120).join("");
64
- return `### ${historyMessageLabel(message, locale)}\n${message.length} Unicode code points\n${renderHistoryBackground(message.background, locale)}[${zh ? "部分原文预览" : "Partial original preview"}]\n${preview}\n[${message.length > 120 ? (zh ? "预览有省略" : "preview omitted remaining text") : zh ? "预览结束" : "preview ends"}]`;
74
+ const omitted = message.length > 120
75
+ ? `\n${zh ? "…(预览省略)" : "…(preview omitted)"}`
76
+ : "";
77
+ return `### ${compactMessageLabel(message, locale)}${renderHistoryBackground(message.background, locale, false)}\n${preview}${omitted}`;
65
78
  })
66
- .join("\n\n") || (zh ? "(空页)" : "(empty page)")}\n\n---\n${zh ? "本页位置/条数" : "Page positions / count"}: ${range(result.messages)} / ${result.messages.length}\n${cursorLine("earlierCursor", result.earlierCursor)}\n${cursorLine("laterCursor", result.laterCursor)}\n${zh ? "续页只传 {cursor}。" : "Continue with only {cursor}."}${result.messages.length === 0 && result.anchor !== null ? `\n${zh ? "可从锚点转向浏览" : "Browse from the anchor in the other direction"}: {before:"${result.anchor}"} / {after:"${result.anchor}"}` : ""}`,
79
+ .join("\n\n") || (zh ? "(空页)" : "(empty page)")}\n\n---\n${zh ? "本页位置/条数" : "Page positions / count"}: ${range(result.messages)} / ${result.messages.length}\n${cursorLine("earlierCursor", result.earlierCursor)}\n${cursorLine("laterCursor", result.laterCursor)}${result.messages.length === 0 && result.anchor !== null ? `\n${zh ? "可从锚点转向浏览" : "Browse from the anchor in the other direction"}: {before:"${result.anchor}"} / {after:"${result.anchor}"}` : ""}`,
67
80
  };
68
81
  if (result.kind === "read")
69
82
  return {
70
83
  ok: true,
71
84
  markdown: `# ${zh ? "完整历史原文" : "Complete history originals"}\n\n${header}\nMode: ${result.mode}${result.center === null ? "" : `\nCenter: ${result.center}`}\nOrder: oldest_first\n${zh ? "实际位置" : "Actual positions"}: ${range(result.messages)}\n${zh ? "每条正文完整,无摘要、无截断" : "Each body is complete, without summary or truncation"}\n\n${result.messages.map((message) => renderHistoryMessage(message, locale)).join("\n\n") || (zh ? "(历史为空)" : "(empty history)")}\n\n---\n${cursorLine("earlier", result.earlier)}\n${cursorLine("later", result.later)}\n${result.earlier === null ? (zh ? "已到历史开头。" : "At the history beginning.") : legacyRead ? `context_read {ref:"${result.earlier}"}` : zh ? "向前不重复读取:history_read {ref:earlier,before:N}。" : "Read earlier without repeating this window: history_read {ref:earlier,before:N}."}\n${result.later === null ? (zh ? "已到历史结尾。" : "At the history end.") : legacyRead ? `context_read {ref:"${result.later}"}` : zh ? "向后不重复读取:history_read {ref:later,after:N}。" : "Read later without repeating this window: history_read {ref:later,after:N}."}\n${legacyRead ? (zh ? "按需逐条读取相邻原文。" : "Read adjacent originals one at a time as needed.") : zh ? "按需小步扩展;21 条消息不代表固定输出体积。" : "Expand in small windows as needed; 21 messages do not imply a fixed output size."}`,
72
85
  };
86
+ if (!result.legacy)
87
+ return {
88
+ ok: true,
89
+ markdown: `# ${zh ? "历史字面搜索" : "History literal search"}\n\n${compactHeader(result.total, locale)}\n${zh ? "搜索位置范围(排除边界)" : "Search position range (boundaries excluded)"}: ${result.range.start === null ? "empty" : `${result.range.start}..${result.range.end}`}\n${zh ? "查询词" : "Queries"}: ${JSON.stringify(result.queries)}\n${zh ? "命中消息总数" : "Total matching messages"}: ${result.matches}\n\n${result.messages.map(({ message, hits, fragments, omittedMatches }) => `### ${compactMessageLabel(message, locale)}${renderHistoryBackground(message.background, locale, false)}${zh ? "逐词命中次数" : "Matches per query"}: ${hits.map(({ query, count }) => `${JSON.stringify(query)}: ${count}`).join("; ")}\n${fragments.map((fragment) => `[${zh ? "部分原文片段" : "Partial original fragment"}: ${fragment.start}..${fragment.end}${fragment.omittedBefore ? "; prefix omitted" : ""}${fragment.omittedAfter ? "; suffix omitted" : ""}]\n${fragment.text}`).join("\n")}${omittedMatches ? `\n${zh ? "另有命中未展示;可完整读取核实。" : "Additional matches omitted; read the complete original to verify."}` : ""}`).join("\n\n") || (zh ? "零字面命中不证明事件没有发生;可改词、从 all 放宽为 any 或读取相邻上下文。" : "Zero literal matches do not prove an event never happened. Try other words, relax all to any, or read nearby context.")}\n\n---\n${cursorLine("nextCursor", result.nextCursor)}`,
90
+ };
73
91
  return {
74
92
  ok: true,
75
93
  markdown: `# ${zh ? "历史字面搜索" : "History literal search"}\n\n${header}${result.within === null ? "" : `\nLegacy selected scope: ${result.within}`}\n${zh ? "搜索位置范围(排除边界)" : "Search position range (boundaries excluded)"}: ${result.range.start === null ? "empty" : `${result.range.start}..${result.range.end}`}\nOrder: ${result.order}\nMatch: ${result.match} (${zh ? "同一消息内" : "within one message"})\nNormalization: NFKC${result.caseSensitive ? " (case sensitive)" : " + case folding (locale und)"}\n${zh ? "查询词" : "Queries"}: ${JSON.stringify(result.queries)}\n${zh ? "命中消息总数" : "Total matching messages"}: ${result.matches}\n\n${result.messages.map(({ message, hits, fragments, omittedMatches }) => `### ${historyMessageLabel(message, locale)}\n${message.length} Unicode code points\n${renderHistoryBackground(message.background, locale)}${zh ? "逐词命中次数" : "Matches per query"}: ${hits.map(({ query, count }) => `${JSON.stringify(query)}: ${count}`).join("; ")}\n${fragments.map((fragment) => `[${zh ? "部分原文片段" : "Partial original fragment"}: ${fragment.start}..${fragment.end}${fragment.omittedBefore ? "; prefix omitted" : ""}${fragment.omittedAfter ? "; suffix omitted" : ""}]\n${fragment.text}\n[${zh ? "片段结束" : "fragment ends"}]`).join("\n")}\n${omittedMatches ? (zh ? "另有命中未展示;可完整读取核实。" : "Additional matches omitted; read the complete original to verify.") : zh ? "全部命中均在所示片段内。" : "All matches are covered by these fragments."}`).join("\n\n") || (result.legacy ? (zh ? "零字面命中不证明事件没有发生;可改用其他 query 或读取相邻原文。" : "Zero literal matches do not prove an event never happened. Try another query or read adjacent originals.") : zh ? "零字面命中不证明事件没有发生;可改词、从 all 放宽为 any 或读取相邻上下文。" : "Zero literal matches do not prove an event never happened. Try other words, relax all to any, or read nearby context.")}\n\n---\n${cursorLine(result.legacy ? "Next-page cursor" : "nextCursor", result.nextCursor)}\n${result.legacy ? (zh ? "续页保留原source=history、query、caseSensitive、within与limit并使用返回cursor。" : "Legacy continuation keeps the original source=history, query, caseSensitive, within and limit with the returned cursor.") : zh ? "续页只传 {cursor};已有原文足以判断时停止检索。all 仅限同一消息;相邻消息可改用 any 后读前后文。" : "Continue with only {cursor}; stop when the originals suffice. all applies to one message; use any then read context for terms in adjacent messages."}`,
@@ -87,7 +105,7 @@ function localizedRecovery(result, zh) {
87
105
  return `参数不符合要求,请按工具定义修正。${result.recovery}`;
88
106
  }
89
107
  /** Every result carries its own annotation; no implicit neighboring inheritance. */
90
- export function renderHistoryBackground(background, locale) {
108
+ export function renderHistoryBackground(background, locale, withNotice = true) {
91
109
  if (background === undefined)
92
110
  return "";
93
111
  const zh = locale === "zh-CN";
@@ -99,10 +117,11 @@ export function renderHistoryBackground(background, locale) {
99
117
  const notice = zh
100
118
  ? "历史背景仅说明采集位置的状态,不是当前世界状态;回忆、梦境或段内局部时间结合原文解释,不认证事实或授予角色知识。"
101
119
  : "Historical background describes the captured state, not current world state; interpret memories, dreams and local times with the original text. It does not certify facts or grant character knowledge.";
120
+ const suffix = withNotice ? `\n${notice}` : "";
102
121
  if (background.kind === "snapshot" ||
103
122
  equalBackground(background.before, background.after))
104
- return `\n${zh ? "历史背景" : "Historical background"}:${node(background.kind === "snapshot" ? background.value : background.before)}\n${notice}\n`;
105
- return `\n${zh ? "推进前背景" : "Background before advancement"}:${node(background.before)}\n${zh ? "推进后背景" : "Background after advancement"}:${node(background.after)}\n${notice}\n`;
123
+ return `\n${zh ? "历史背景" : "Historical background"}:${node(background.kind === "snapshot" ? background.value : background.before)}${suffix}\n`;
124
+ return `\n${zh ? "推进前背景" : "Background before advancement"}:${node(background.before)}\n${zh ? "推进后背景" : "Background after advancement"}:${node(background.after)}${suffix}\n`;
106
125
  }
107
126
  function equalBackground(before, after) {
108
127
  if (before === after)
@@ -0,0 +1,31 @@
1
+ import { HistoryQuery, historyInputs } from "./HistoryQuery.js";
2
+ import { renderHistoryResult } from "./HistoryRendering.js";
3
+ export const historyToolNames = [
4
+ "history_list",
5
+ "history_search",
6
+ "history_read",
7
+ ];
8
+ export function isHistoryToolName(name) {
9
+ return historyToolNames.includes(name);
10
+ }
11
+ export function historyEntries(history, backgrounds = {}) {
12
+ return Object.entries(history).map(([path, contents]) => ({
13
+ path,
14
+ contents,
15
+ ...(backgrounds[path] === undefined
16
+ ? {}
17
+ : { background: structuredClone(backgrounds[path]) }),
18
+ }));
19
+ }
20
+ /** Read-only committed-history navigation shared by play and world revision. */
21
+ export function createHistoryQuery(scope, entries) {
22
+ return new HistoryQuery(scope, historyInputs(entries));
23
+ }
24
+ export function runHistoryTool(query, call, locale) {
25
+ const result = call.name === "history_search"
26
+ ? query.search(call.arguments)
27
+ : call.name === "history_read"
28
+ ? query.read(call.arguments)
29
+ : query.list(call.arguments);
30
+ return renderHistoryResult(result, locale);
31
+ }
@@ -4,7 +4,7 @@ import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js
4
4
  import { validateMaterialList } from "../prompt/MaterialSelection.js";
5
5
  import { FileNativeWorldCreationError, } from "../world/FileNativeWorldStore.js";
6
6
  import { WorldOperationBusyError, } from "../world/WorldOperationCoordinator.js";
7
- import { beginWorldStateRevision, fileNativeStateChanges, reviseWorldState, worldDocumentRevisionFailureMessage, worldStateRevisionChanges, } from "../world/WorldStateRevision.js";
7
+ import { beginWorldStateRevision, fileNativeStateChanges, acceptWorldStateRevision, worldDocumentRevisionFailureMessage, worldStateRevisionChanges, } from "../world/WorldStateRevision.js";
8
8
  export class FileNativeContinuityCorrection {
9
9
  #worlds;
10
10
  #compiler;
@@ -260,9 +260,17 @@ export class FileNativeContinuityCorrection {
260
260
  return document;
261
261
  }
262
262
  #reviseDocument(candidate, command) {
263
- const revised = reviseWorldState(candidate, { commands: [command] });
264
- if (!revised.ok || revised.snapshotStatus !== "usable")
265
- throw new Error(worldDocumentRevisionFailureMessage(revised.diagnostics));
263
+ // A correction applies all of its edits or none of them, so a skipped edit
264
+ // is a failure here even though the revision itself settled.
265
+ const revised = candidate.snapshot.revise({ commands: [command] });
266
+ const failures = revised.ok
267
+ ? revised.editOutcomes.flatMap(({ status, diagnostic }) => status === "failed" && diagnostic !== undefined ? [diagnostic] : [])
268
+ : revised.diagnostics;
269
+ if (!revised.ok ||
270
+ revised.snapshotStatus !== "usable" ||
271
+ failures.length > 0)
272
+ throw new Error(worldDocumentRevisionFailureMessage(failures));
273
+ acceptWorldStateRevision(candidate, revised);
266
274
  candidate.reads.clear();
267
275
  }
268
276
  #advance(candidate) {
@@ -2,6 +2,7 @@ import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js
2
2
  import { dispatchFailureEnding, emptyResponseEnding, lastEndingCancelled, noContinuationEnding, PlayAdvanceLifecycle, settlementFailureEnding, } from "./PlayAdvanceLifecycle.js";
3
3
  import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTabletop, seededRandom, } from "./TabletopResolution.js";
4
4
  import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
5
+ import { historyEntries } from "../history/HistoryTools.js";
5
6
  import { isDeepStrictEqual } from "node:util";
6
7
  import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
7
8
  import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
@@ -13,7 +14,6 @@ import { createHash, randomBytes } from "node:crypto";
13
14
  import { isTurnGuideLine } from "../../shared/ordered-play-prompts.js";
14
15
  import { turnGuideWithTurnNote } from "../../shared/turn-guide.js";
15
16
  import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, turnGuideNotice, } from "./PlayContinuity.js";
16
- import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
17
17
  import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
18
18
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
19
19
  import { equalModelHostBinding, ModelHostCancelledError, } from "../model/ModelHost.js";
@@ -1329,7 +1329,7 @@ export class PlayCallChain {
1329
1329
  ],
1330
1330
  });
1331
1331
  worldClock = readDeclaredWorldClock(snapshot);
1332
- const { coverage, maintenance } = this.#compiler.inspectWorldMaterials({
1332
+ const { maintenance } = this.#compiler.inspectWorldMaterials({
1333
1333
  controlFingerprint: "receipt",
1334
1334
  documentSnapshot: snapshot,
1335
1335
  history: Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
@@ -1337,23 +1337,18 @@ export class PlayCallChain {
1337
1337
  });
1338
1338
  for (const { result } of writes) {
1339
1339
  const write = result.candidateWrite;
1340
- write.freshContextCoverage = renderFreshContextCoverage(snapshot, coverage, write.shortRef, this.#compiler.locale);
1341
1340
  const previous = session.documents.committedSnapshot.query({
1342
1341
  kind: "read_document",
1343
1342
  document: { shortRef: write.shortRef },
1344
1343
  });
1345
- const advice = renderWriteSizeAdvice(maintenance, write.shortRef, previous.kind === "read_document"
1344
+ write.sizeAdvice = renderWriteSizeAdvice(maintenance, write.shortRef, previous.kind === "read_document"
1346
1345
  ? Buffer.byteLength(previous.body, "utf8")
1347
1346
  : null, this.#compiler.locale);
1348
- if (advice.length > 0)
1349
- write.freshContextCoverage += `\n${advice}`;
1350
1347
  }
1351
1348
  }
1352
- catch (error) {
1353
- // A coverage prediction must never reject an otherwise valid write.
1354
- const reason = error instanceof Error ? error.message : "Material selection failed";
1355
- for (const { result } of writes)
1356
- result.candidateWrite.freshContextCoverage = `${this.#compiler.locale === "zh-CN" ? "暂时无法判定下次注入;写入结果不受影响。原因" : "Fresh-context coverage unavailable; the write result is unaffected. Reason"}: ${reason}`;
1349
+ catch {
1350
+ // Maintenance advice is diagnostic: a failure omits it and never
1351
+ // rejects or undoes an accepted write.
1357
1352
  }
1358
1353
  }
1359
1354
  const previousNarrative = session.events.findLast((event) => event.kind === "assistant" &&
@@ -2015,7 +2010,7 @@ function finalizeWriteReceipts(settlement) {
2015
2010
  const status = candidateWrite.changed && committedWriteRefs.has(candidateWrite.shortRef)
2016
2011
  ? `@${candidateWrite.shortRef} write succeeded`
2017
2012
  : `@${candidateWrite.shortRef} unchanged`;
2018
- const markdown = [status, candidateWrite.freshContextCoverage]
2013
+ const markdown = [status, candidateWrite.sizeAdvice]
2019
2014
  .filter(Boolean)
2020
2015
  .join("\n");
2021
2016
  completed.result.markdown = markdown;
@@ -2169,6 +2164,9 @@ function duplicatePlayerExchange(session, exchangeId, playerText, context) {
2169
2164
  }
2170
2165
  function mergeChangedDocuments(session, changes) {
2171
2166
  for (const change of changes) {
2167
+ // Play and continuity writes never remove a document; only world-revision apply does.
2168
+ if (change.kind === "delete")
2169
+ continue;
2172
2170
  const projected = {
2173
2171
  kind: change.kind,
2174
2172
  ref: `@${change.stableShortRef}`,
@@ -2352,15 +2350,6 @@ function revisionTimelineGeneration(sourceGeneration, operationId, worldId, cont
2352
2350
  .digest("hex")
2353
2351
  .slice(0, 40)}`;
2354
2352
  }
2355
- function historyEntries(history, backgrounds = {}) {
2356
- return Object.entries(history).map(([path, contents]) => ({
2357
- path,
2358
- contents,
2359
- ...(backgrounds[path] === undefined
2360
- ? {}
2361
- : { background: structuredClone(backgrounds[path]) }),
2362
- }));
2363
- }
2364
2353
  function operationId(chainId, key) {
2365
2354
  return `play-${createHash("sha256")
2366
2355
  .update(`${chainId}\0${key}`)
@@ -8,7 +8,7 @@ import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
8
8
  import { coerceToolArguments, renderCoercionNote, } from "../model/ToolArgumentCoercion.js";
9
9
  import { runtimeToolInputSchema } from "../prompt/FileNativeToolRegistry.js";
10
10
  import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
11
- import {} from "../world/WorldDocumentStore.js";
11
+ import { formatWorldDocumentLocator, } from "../world/WorldDocumentStore.js";
12
12
  import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
13
13
  class PlayDocumentToolFailure extends Error {
14
14
  failureKind;
@@ -274,10 +274,12 @@ function applyPlayCandidatePatch(candidate, reads, args) {
274
274
  },
275
275
  ],
276
276
  });
277
+ if (!revised.ok && revised.editOutcomes.length > 0)
278
+ throw new PlayDocumentToolFailure(`world_patch applied none of its edits to @${ref}. ${formatEditNotes(revised.editOutcomes)}`, "candidate");
277
279
  if (!revised.ok || revised.snapshotStatus !== "usable")
278
280
  throw new PlayDocumentToolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
279
281
  const changed = revised.changes.length > 0;
280
- const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes);
282
+ const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes, revised.editOutcomes);
281
283
  const previousSnapshot = candidate.snapshot;
282
284
  acceptWorldStateRevision(candidate, revised);
283
285
  carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
@@ -353,12 +355,41 @@ function authorizationsAfterEdits(authorization, edits) {
353
355
  }
354
356
  return authorization;
355
357
  }
356
- function formatToolRevisionReceipt(shortRef, changes) {
358
+ function formatToolRevisionReceipt(shortRef, changes, outcomes) {
357
359
  if (changes.length > 1)
358
360
  throw new Error("A world_patch revision must affect only its target document");
359
- return changes.length === 1
360
- ? `@${shortRef} pending write`
361
- : `@${shortRef} unchanged`;
361
+ const failed = outcomes.filter(({ status }) => status === "failed").length;
362
+ const parts = [
363
+ changes.length === 1
364
+ ? `@${shortRef} pending write`
365
+ : `@${shortRef} unchanged`,
366
+ ];
367
+ if (failed > 0)
368
+ parts.push(`${outcomes.length - failed}/${outcomes.length} edits applied`);
369
+ else if (outcomes.some(({ status }) => status === "no_op"))
370
+ parts.push(`${outcomes.length} edits applied`);
371
+ const notes = formatEditNotes(outcomes);
372
+ if (notes !== "")
373
+ parts.push(notes);
374
+ if (failed > 0)
375
+ parts.push("applied edits are not resent; send again only the edits listed above");
376
+ return parts.join(" · ");
377
+ }
378
+ /** Lists only the edits that did not apply cleanly: failed ones and no-ops. */
379
+ function formatEditNotes(outcomes) {
380
+ return outcomes
381
+ .flatMap(({ editIndex, status, diagnostic }) => {
382
+ if (status === "applied" || diagnostic === undefined)
383
+ return [];
384
+ const label = `edit #${editIndex + 1}`;
385
+ if (status === "no_op")
386
+ return [`${label} no-op: ${diagnostic.message}`];
387
+ const location = diagnostic.locator
388
+ ? ` (${formatWorldDocumentLocator(diagnostic.locator)})`
389
+ : "";
390
+ return [`${label}${location}: ${diagnostic.message}`];
391
+ })
392
+ .join(" · ");
362
393
  }
363
394
  function applyPlayCandidateRetirement(candidate, reads, args) {
364
395
  if (!record(args) ||
@@ -640,15 +671,15 @@ function knownStateDirectory(snapshot, directory, declaredDirectories) {
640
671
  return snapshot.files.some(({ path }) => path.startsWith(logicalPrefix));
641
672
  }
642
673
  function executeStateList(snapshot, args, declaredDirectories) {
674
+ // Frozen sessions may still send obsolete cursor/limit; directories are
675
+ // listed in full, so those keys are accepted and ignored.
643
676
  if (!record(args) ||
644
677
  !hasOnlyToolKeys(args, ["parent", "cursor", "limit"]) ||
645
678
  typeof args.parent !== "string" ||
646
- parseStateDirectoryHandle(args.parent) === null ||
647
- (args.limit !== undefined && typeof args.limit !== "number") ||
648
- !validOptionalCursor(args.cursor))
679
+ parseStateDirectoryHandle(args.parent) === null)
649
680
  return {
650
681
  ok: false,
651
- 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.",
682
+ markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime; use history_list for committed history.",
652
683
  };
653
684
  return executeContextList(snapshot, { ...args, source: "state" }, declaredDirectories);
654
685
  }
@@ -668,19 +699,11 @@ function executeContextList(snapshot, args, declaredDirectories = []) {
668
699
  (args.order !== "newest_first" && args.order !== "oldest_first"));
669
700
  if (!hasOnlyToolKeys(args, allowedKeys) ||
670
701
  invalidStateShape ||
671
- invalidHistoryShape ||
672
- (args.limit !== undefined && typeof args.limit !== "number") ||
673
- !validOptionalCursor(args.cursor))
702
+ invalidHistoryShape)
674
703
  return {
675
704
  ok: false,
676
705
  markdown: "# Runtime argument error\n\nstate must use an @dir-* parent returned by the Runtime and omit order; history must use newest_first or oldest_first order and omit parent.",
677
706
  };
678
- const limit = typeof args.limit === "number" ? args.limit : 20;
679
- if (!Number.isInteger(limit) || limit < 1 || limit > 100)
680
- return {
681
- ok: false,
682
- markdown: "# Runtime argument error\n\nlimit must be between 1 and 100.",
683
- };
684
707
  if (args.source === "state") {
685
708
  const parent = args.parent;
686
709
  const directory = parseStateDirectoryHandle(parent);
@@ -691,32 +714,38 @@ function executeContextList(snapshot, args, declaredDirectories = []) {
691
714
  ok: false,
692
715
  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.",
693
716
  };
694
- const result = snapshot.query({
695
- kind: "catalog",
696
- directory,
697
- includeRetired: true,
698
- declaredDirectories,
699
- limit,
700
- ...(typeof args.cursor === "string" || args.cursor === null
701
- ? { cursor: args.cursor }
702
- : {}),
703
- });
704
- if (result.kind === "error")
705
- return renderStateQueryFailure(result);
706
- if (result.kind !== "catalog")
707
- return unexpectedStateQueryResult();
708
- const entries = result.entries.map((entry) => {
709
- if (entry.kind === "directory") {
710
- const relative = entry.logicalPath.slice(`${snapshot.logicalRoot}/`.length);
711
- return `- Directory ${stateDirectoryHandle(relative)}`;
712
- }
713
- if (entry.document === undefined)
714
- return "- Damaged document (currently not addressable)";
715
- return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
716
- });
717
+ const entries = [];
718
+ let complete = true;
719
+ let cursor = null;
720
+ do {
721
+ const result = snapshot.query({
722
+ kind: "catalog",
723
+ directory,
724
+ includeRetired: true,
725
+ declaredDirectories,
726
+ limit: 100,
727
+ ...(cursor === null ? {} : { cursor }),
728
+ });
729
+ if (result.kind === "error")
730
+ return renderStateQueryFailure(result);
731
+ if (result.kind !== "catalog")
732
+ return unexpectedStateQueryResult();
733
+ if (result.coverage.status !== "complete")
734
+ complete = false;
735
+ entries.push(...result.entries.map((entry) => {
736
+ if (entry.kind === "directory") {
737
+ const relative = entry.logicalPath.slice(`${snapshot.logicalRoot}/`.length);
738
+ return `- Directory ${stateDirectoryHandle(relative)}`;
739
+ }
740
+ if (entry.document === undefined)
741
+ return "- Damaged document (currently not addressable)";
742
+ return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
743
+ }));
744
+ cursor = result.page.nextCursor;
745
+ } while (cursor !== null);
717
746
  return {
718
747
  ok: true,
719
- markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${result.coverage.status === "complete" ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} items\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
748
+ markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${complete ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nComplete: yes`,
720
749
  };
721
750
  }
722
751
  return toolFailure("Use history_list for committed history.");
@@ -43,7 +43,7 @@ const runtimeContracts = {
43
43
 
44
44
  - Use only tools attached to the current request. Read results state their scope, cursor, and completeness. Directories must use @dir-* handles returned by Runtime; documents and history must use @handles returned by Runtime. Never substitute a world/ path or natural-language name for a handle.
45
45
  - A document marked \`full body injected\` in the material-coverage report already has its complete writable body in the request and carries write authorization. Do not read it again merely to confirm body structure, check body fields, or “be safe.” context_read additionally exposes the current title, summary, and aliases. world_patch preserves metadata fields omitted from set_metadata, so do not read merely to copy unchanged metadata. Read only when a decision depends on unseen metadata or material the report says is not covered.
46
- - A YAML world_patch locator may mix map keys and zero-based array indexes; remove deletes exactly one existing key or array item. Retire an entity that should leave future frame catalogs with world_retire instead of destroying its document; retired documents remain listed, readable, referenceable, and restorable.
46
+ - A YAML world_patch locator may mix map keys and zero-based array indexes. add and replace upsert: they write whether or not the target exists, and an index past the end appends. remove deletes one existing key or array item; a missing target is a no-op the receipt reports. Each edit settles on its own: a failed edit is skipped and the others still apply, so when a receipt lists failed edits, send again only those. Retire an entity that should leave future frame catalogs with world_retire instead of destroying its document; retired documents remain listed, readable, referenceable, and restorable.
47
47
  - Zero literal-search matches do not prove that a fact does not exist. Tools, directories, documents, archives, searches, matches, handles, Runtime, and failure processes are private adjudication details. Player-visible narrative must not mention internal phrases such as “nothing was found.” When information is insufficient, preserve uncertainty inside the world instead of inventing an internal process.
48
48
  - World-write tools change only an uncommitted working copy. Their changes are not official world facts until Runtime accepts and completes the commit.
49
49
 
@@ -74,7 +74,7 @@ ${historyUseGuidance("en")}`,
74
74
 
75
75
  - 只使用请求随附的工具;读取结果会明确范围、cursor 与完整性。目录只能使用 Runtime 返回的 @dir-*,文档和历史只能使用 Runtime 返回的 @句柄,不要把 world/路径或自然语言名称冒充句柄。
76
76
  - 材料覆盖报告标为 \`已注入完整正文\` 的文档,其完整可写正文已经在本次请求里,写入资格同样已经具备。不要为确认正文结构、核对正文字段或“保险起见”重读它。context_read 会额外显示当前 title、summary 和 aliases;world_patch 会保留 set_metadata 中未提供的元数据字段,因此不要只为照抄未改变的元数据而读取。只有后续决策确实依赖尚未显示的元数据,或材料覆盖报告未覆盖的内容时才读。
77
- - YAML world_patch 的 locator 可混合 map key 与从 0 开始的数组下标;remove 只删除一个精确的已有键或数组项。对象需要退出后续 frame catalog 时用 world_retire 退役,不要销毁文档;退役文档仍可列出、读取、引用和恢复。
77
+ - YAML world_patch 的 locator 可混合 map key 与从 0 开始的数组下标。add 与 replace 是 upsert:无论目标是否存在都写入,数组下标达到或超过长度时追加。remove 只删除一个精确的已有键或数组项;目标不存在时为无操作,回执会注明。每条 edit 独立结算:失败的 edit 被跳过,其余照常生效;回执列出失败项时,只补发列出的 edit。对象需要退出后续 frame catalog 时用 world_retire 退役,不要销毁文档;退役文档仍可列出、读取、引用和恢复。
78
78
  - 字面搜索 0 命中不证明事实不存在。工具、目录、文档、档案、检索、命中、句柄、Runtime 及失败过程只供私下裁决;玩家可见叙事不得出现“没搜到/没找到资料”等内部措辞。信息不足时保持世界内的不确定性,不编造内部过程。
79
79
  - 世界写入工具只修改尚未提交的工作副本;Runtime 接受并完成提交前,这些修改都不是正式世界事实。
80
80
 
@@ -148,15 +148,13 @@ export class FileNativePromptCompiler {
148
148
  playPreset: input.playPreset,
149
149
  modelBinding: input.modelBinding,
150
150
  tools: input.tools,
151
+ ...(input.preload === undefined ? {} : { preload: input.preload }),
151
152
  });
152
153
  }
153
154
  compileWorldRevision(input) {
154
155
  const identity = this.#locale === "zh-CN"
155
156
  ? `# 当前修订世界\n\n${input.worldTitle}`
156
157
  : `# World being revised\n\n${input.worldTitle}`;
157
- const boundary = this.#locale === "zh-CN"
158
- ? "# 世界修订工作树边界\n\n随附工具只读写当前锁定 epoch 的 state/* 与 control/* 工作树。成功工具调用尚未进入世界 Authority;只有玩家点击应用才会整体提交并解锁。"
159
- : "# World-revision worktree boundary\n\nThe attached tools read and write only state/* and control/* in the current locked epoch. Successful tool calls do not enter world Authority until the player applies the complete revision and unlocks it.";
160
158
  return this.#compileAuthoringConversation({
161
159
  identity: {
162
160
  source: "world-revision:title",
@@ -165,10 +163,7 @@ export class FileNativePromptCompiler {
165
163
  ? `\n\nEpoch: ${input.epoch.id}\nBase: ${input.epoch.baseHead}`
166
164
  : ""),
167
165
  },
168
- boundary: {
169
- source: "runtime:world-revision-worktree-boundary",
170
- markdown: boundary,
171
- },
166
+ boundary: null,
172
167
  runtimeSource: "runtime:builtin/world-revision",
173
168
  target: "world-revision",
174
169
  gameplay: input.gameplay,
@@ -177,6 +172,7 @@ export class FileNativePromptCompiler {
177
172
  playPreset: input.playPreset,
178
173
  modelBinding: input.modelBinding,
179
174
  tools: input.tools,
175
+ ...(input.preload === undefined ? {} : { preload: input.preload }),
180
176
  });
181
177
  }
182
178
  #compileAuthoringConversation(input) {
@@ -185,7 +181,7 @@ export class FileNativePromptCompiler {
185
181
  const logicalMessages = [];
186
182
  const ordered = input.playPreset.definition.authorPrompts ??
187
183
  legacyAuthorPrompts(input.playPreset.definition);
188
- const catalog = builtinAuthorPrompts(this.#locale);
184
+ const catalog = builtinAuthorPrompts(this.#locale, input.target);
189
185
  for (const entry of ordered) {
190
186
  if (entry.kind === "world" || !entry.enabled)
191
187
  continue;
@@ -210,7 +206,7 @@ export class FileNativePromptCompiler {
210
206
  if (builtin?.id === "author.target")
211
207
  blocks = [
212
208
  input.identity,
213
- input.boundary,
209
+ ...(input.boundary === null ? [] : [input.boundary]),
214
210
  ...(input.gameplay.mode === "tabletop"
215
211
  ? [
216
212
  tabletopAuthoringRulesBlock(input.gameplay, input.target, this.#locale),
@@ -238,6 +234,20 @@ export class FileNativePromptCompiler {
238
234
  blocks,
239
235
  });
240
236
  }
237
+ // Preloaded material follows every author prompt so the stable prefix stays byte-identical.
238
+ if (input.preload !== undefined && input.preload.length > 0) {
239
+ const preloadBlocks = input.preload.map(({ source, markdown }) => ({
240
+ source,
241
+ markdown,
242
+ }));
243
+ logicalMessages.push({
244
+ messageRole: "system",
245
+ promptId: "runtime.preload",
246
+ role: "world_context",
247
+ markdown: joinBlocks(preloadBlocks),
248
+ blocks: preloadBlocks,
249
+ });
250
+ }
241
251
  const tools = structuredClone(input.tools);
242
252
  const toolStrategy = this.#toolStrategyOverride ??
243
253
  defaultRuntimeToolDefinitionStrategy(input.modelBinding.provider);
@@ -870,9 +880,13 @@ function settingImprovementPresetReference(binding, locale, target, gameplay) {
870
880
  return [
871
881
  {
872
882
  ...worldInstructionPlaceholder,
873
- markdown: locale === "zh-CN"
874
- ? "世界指令在此展开;frame 选定材料仅在全新游玩上下文中作为初始工具结果提供。请通过本轮创作工具检查实际内容。"
875
- : "World instructions expand here; frame-selected material is supplied as an initial tool result only for a fresh play context. Inspect actual content through the current authoring tools.",
883
+ markdown: revision
884
+ ? locale === "zh-CN"
885
+ ? "世界指令在此展开;启用块原文见本请求的 Runtime 预载。"
886
+ : "World instructions expand here; enabled block text appears in this request's Runtime preload."
887
+ : locale === "zh-CN"
888
+ ? "世界指令在此展开;frame 选定材料仅在全新游玩上下文中作为初始工具结果提供。请通过本轮创作工具检查实际内容。"
889
+ : "World instructions expand here; frame-selected material is supplied as an initial tool result only for a fresh play context. Inspect actual content through the current authoring tools.",
876
890
  },
877
891
  ];
878
892
  if (entry.kind === "tail" || !entry.enabled)
@@ -949,11 +963,13 @@ function settingImprovementPresetReference(binding, locale, target, gameplay) {
949
963
  : "Player-visible narrative semantics", locale === "zh-CN"
950
964
  ? "这些块只在未来游玩生成玩家可见正文时生效;不得把它们当作本轮 YAML 或 Markdown 设定正文的范文。"
951
965
  : "These blocks apply only when future play produces player-visible story. Do not use them as prose models for YAML or Markdown setting documents in this conversation.", narrativeBlocks),
952
- settingImprovementReferenceGroup(locale === "zh-CN"
953
- ? "本轮提示(每次发送的尾部)"
954
- : "Turn guide (per-send suffix)", locale === "zh-CN"
955
- ? "这些行在每次游玩发送时追加在玩家输入之后,由已启用块的尾部行按同一顺序拼成;它们是提醒,不是世界事实。"
956
- : "These lines are appended after the player input on every play send, assembled from the enabled blocks' turn-guide lines in the same order. They are reminders, not world facts.", turnGuideBlocks),
966
+ revision
967
+ ? null
968
+ : settingImprovementReferenceGroup(locale === "zh-CN"
969
+ ? "本轮提示(每次发送的尾部)"
970
+ : "Turn guide (per-send suffix)", locale === "zh-CN"
971
+ ? "这些行在每次游玩发送时追加在玩家输入之后,由已启用块的尾部行按同一顺序拼成;它们是提醒,不是世界事实。"
972
+ : "These lines are appended after the player input on every play send, assembled from the enabled blocks' turn-guide lines in the same order. They are reminders, not world facts.", turnGuideBlocks),
957
973
  ].filter((group) => group !== null);
958
974
  return [
959
975
  heading,