narraeon 0.5.5 → 0.5.6

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.
@@ -239,7 +239,7 @@ function createToolDefinitions(locale) {
239
239
  }
240
240
  const toolDescriptions = {
241
241
  en: {
242
- world_checkpoint: "Register a consolidation declaration and a history-replay boundary. No arguments. Registration takes effect only after the subsequent nonempty tool-free final prose successfully commits, and includes that prose. A fresh context replays committed player inputs and final narratives after this boundary. The player may choose a fresh context; registration neither switches context nor ends an in-world event. Normal replies need no checkpoint. Further tool calls may follow registration; Runtime does not verify semantic completeness.",
242
+ world_checkpoint: "Register a consolidation declaration and a history-replay boundary. No arguments. Registration takes effect only after the subsequent nonempty tool-free final prose successfully commits, and includes that prose. A fresh context replays committed player inputs and final narratives after this boundary. The player may choose a fresh context; registration neither switches context nor ends an in-world event. Further tool calls may follow registration; Runtime does not verify semantic completeness.",
243
243
  state_list: "List document and state directory handles known to Runtime. parent must be @dir-/ for the state root or an @dir-* handle from the initial directory index or an earlier state_list result; no preliminary listing is needed for an already supplied handle. Catalog directories declared by the frozen world frame remain listed and usable when empty. A cursor is valid only for the same state snapshot, parent, and limit. Pass returned document handles to context_read. This tool never lists committed history.",
244
244
  history_list: "Browse committed history on the current timeline. Omit anchors for the latest page; use only before or after with a Runtime ref (including refs in injected history) for the immediately adjacent page, excluding the anchor. Every page is oldest to newest. limit defaults to 20 (1–100 messages). Previews are only the first 120 Unicode code points. Use independent earlierCursor/laterCursor with {cursor} alone; no first-query parameters on continuation. Cursors bind world, tool, query and immutable ordered history. After history_changed rerun the query. Opening messages are queryable. Read full originals with history_read; positions are snapshot locations, not identities or turns.",
245
245
  history_search: "Search committed originals on the current timeline with queries: 1–5 nonempty literal strings (max 256 code points each); no query alias or semantic filters. match defaults to any, or all within one message. NFKC normalization always applies; caseSensitive defaults to false (locale-und case folding), normalized duplicate queries collapse. order defaults to newest_first, or oldest_first. limit defaults to 10 (1–50 matching messages). Optional after/before refs exclude timeline boundaries independently of order; after must precede before. Each result has at most 3 partial original fragments, query occurrence counts and complete-original length. Read a hit and nearby context with history_read. Continue with {cursor} alone; history_changed requires a new query.",
@@ -253,7 +253,7 @@ const toolDescriptions = {
253
253
  artifact_clear: "Clear the projection of an artifact that this follow-up declared in advance and explicitly allows to be cleared. The call cannot choose a channel, key, or any other output contract.",
254
254
  },
255
255
  "zh-CN": {
256
- world_checkpoint: "登记整理声明与历史重放边界,参数为空对象。仅在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;全新上下文补入边界之后的已提交玩家原文与最终叙事。玩家可自行选择全新上下文,登记不切换上下文,也不结束世界内事件。正常回复无需检查点,登记后仍可继续工具调用;Runtime 不认证语义整理完整。",
256
+ world_checkpoint: "登记整理声明与历史重放边界,参数为空对象。仅在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;全新上下文补入边界之后的已提交玩家原文与最终叙事。玩家可自行选择全新上下文,登记不切换上下文,也不结束世界内事件。登记后仍可继续工具调用;Runtime 不认证语义整理完整。",
257
257
  state_list: "列出 Runtime 已知的文档与状态目录句柄。状态根目录的 parent 使用 @dir-/;继续下级时使用初始目录索引或 state_list 返回的 @dir-*;已有句柄无需再次列目录确认。冻结世界 frame 声明的 catalog 目录即使为空,也会继续列出并可用。cursor 只对同一状态快照、parent 和 limit 有效。返回的文档句柄交给 context_read;本工具不列出已提交历史。",
258
258
  history_list: "浏览当前时间线已提交历史。无锚点时取最新一页;before 或 after 只能选一个,取锚点紧邻前/后的一页且不含锚点。页内始终由旧到新。limit 默认20,整数1–100。预览仅含前120个Unicode码点;完整原文用history_read。earlierCursor/laterCursor独立翻页,续页只传{cursor},不可混用首次参数。游标绑定世界、工具、查询和有序历史快照;history_changed须重新查询。开场白可查询。提示词注入中的ref可直接使用,无需再次列举;位置不是身份或回合数。",
259
259
  history_search: "搜索当前时间线已提交原文。queries必填:1–5个非空字面词,每词最多256个Unicode码点,不接受query别名或语义筛选。match默认any,all限于同一消息。始终NFKC规范化,caseSensitive默认false(locale-und大小写折叠),规范化重复词合并。order默认newest_first,可选oldest_first;limit默认10,整数1–50条命中消息。after/before始终按时间线先后排除边界,after必须早于before,与结果排序无关。每项最多3个部分原文片段、逐词命中次数及完整原文长度;可用history_read读取前后文。续页只传{cursor};history_changed须重新查询。",
@@ -1,3 +1,4 @@
1
+ import { isHistoryBackground, } from "../history/HistoryBackground.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { link, mkdir, open, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
3
4
  import { dirname, join } from "node:path";
@@ -799,7 +800,10 @@ function assertHead(value) {
799
800
  }
800
801
  function isHistoryMessage(value) {
801
802
  if (!isRecord(value) ||
802
- !hasExactKeys(value, ["messageId", "role", "exactText"]) ||
803
+ !hasExactKeys(value, ["messageId", "role", "exactText"], ["background"]) ||
804
+ (value.background !== undefined &&
805
+ (!isHistoryBackground(value.background) ||
806
+ (value.role === "player" && value.background.kind !== "snapshot"))) ||
803
807
  typeof value.messageId !== "string" ||
804
808
  (value.role !== "player" && value.role !== "narrator") ||
805
809
  typeof value.exactText !== "string")
@@ -912,9 +916,8 @@ function validRelativePath(path) {
912
916
  .split("/")
913
917
  .some((part) => part === "" || part === "." || part === ".."));
914
918
  }
915
- function hasExactKeys(value, keys) {
916
- return (Object.keys(value).length === keys.length &&
917
- keys.every((key) => Object.hasOwn(value, key)));
919
+ function hasExactKeys(value, keys, optional = []) {
920
+ return (Object.keys(value).every((key) => keys.includes(key) || optional.includes(key)) && keys.every((key) => Object.hasOwn(value, key)));
918
921
  }
919
922
  function isNarrativeCheckpoint(value, effective) {
920
923
  if (!isRecord(value) ||
@@ -1,4 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
+ export function historyBackgroundRecord(messages) {
3
+ return Object.fromEntries(messages.flatMap((message) => message.background === undefined
4
+ ? []
5
+ : [[message.messageId, structuredClone(message.background)]]));
6
+ }
7
+ export function projectHistoryBackgrounds(messages) {
8
+ return Object.entries(historyBackgroundRecord(messages)).map(([id, background]) => ({
9
+ path: `${id}.json`,
10
+ contents: `${JSON.stringify(background)}\n`,
11
+ }));
12
+ }
2
13
  export function fileNativeHistoryMessageIdFromProjectionPath(path) {
3
14
  const match = /^(\d{8})-(\d+)-(player|narrator)-[a-f0-9]{12}\.md$/u.exec(path);
4
15
  if (match === null)
@@ -1,3 +1,4 @@
1
+ import { captureNarrativeOrigin, snapshotBackground, isHistoryBackground, } from "../history/HistoryBackground.js";
1
2
  import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
2
3
  import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
3
4
  import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
@@ -15,7 +16,7 @@ import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.j
15
16
  import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
16
17
  import { FileNativeAuthorityV3, FileNativeAuthorityV3Error, } from "./FileNativeAuthorityV3.js";
17
18
  import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
18
- import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, } from "./FileNativeHistoryProjection.js";
19
+ import { fileNativeHistoryMessageIdFromProjectionPath, projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
19
20
  const publicationFile = "publication.json";
20
21
  const localMetadataFile = "local.json";
21
22
  export class FileNativeWorldCreationError extends Error {
@@ -266,7 +267,10 @@ export class FileNativeWorldStore {
266
267
  await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
267
268
  await rm(stagingRoot, { recursive: true, force: true });
268
269
  try {
269
- const openingMessage = genesisOpeningMessage(openingText(packageFiles));
270
+ const openingMessage = {
271
+ ...genesisOpeningMessage(openingText(packageFiles)),
272
+ ...backgroundField(snapshotBackground(captureNarrativeOrigin(inspection.worldDocumentSnapshot, 1).value)),
273
+ };
270
274
  const initialMaterials = [
271
275
  { kind: "history_message", message: openingMessage.messageId },
272
276
  ];
@@ -276,6 +280,7 @@ export class FileNativeWorldStore {
276
280
  await mkdir(join(stagingRoot, "history"), { recursive: true });
277
281
  await mkdir(join(stagingRoot, "runtime"), { recursive: true });
278
282
  await writeIdempotentText(join(stagingRoot, "history", historySurfaceFiles([openingMessage])[0].path), openingMessage.exactText);
283
+ await replaceMaterializedTree(join(stagingRoot, "runtime", "history-backgrounds"), projectHistoryBackgrounds([openingMessage]));
279
284
  const genesis = {
280
285
  schemaVersion: 1,
281
286
  type: "file_native_genesis",
@@ -304,6 +309,7 @@ export class FileNativeWorldStore {
304
309
  head: "genesis",
305
310
  sequence: 0,
306
311
  commitDigest: null,
312
+ ...backgroundCount([openingMessage]),
307
313
  });
308
314
  await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
309
315
  const publication = {
@@ -634,7 +640,9 @@ export class FileNativeWorldStore {
634
640
  const authorityStore = new FileNativeAuthorityV3(root);
635
641
  const authority = await authorityStore.readHead();
636
642
  const checkpoint = await readMaterializedCheckpoint(root);
637
- if (!sameMaterializedEndpoint(checkpoint, authority)) {
643
+ const cachedBackgrounds = await readTree(join(root, "runtime", "history-backgrounds"));
644
+ if (!sameMaterializedEndpoint(checkpoint, authority) ||
645
+ (checkpoint?.historyBackgroundCount ?? 0) !== cachedBackgrounds.length) {
638
646
  const recovered = await authorityStore.recover();
639
647
  let operation = null;
640
648
  if (authority.operationId !== null && authority.sequence > 0) {
@@ -654,11 +662,12 @@ export class FileNativeWorldStore {
654
662
  if (operation !== null)
655
663
  await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
656
664
  }
657
- const [state, control, history, materials] = await Promise.all([
665
+ const [state, control, history, materials, backgrounds] = await Promise.all([
658
666
  readTree(join(root, "state")),
659
667
  readTree(join(root, "control")),
660
668
  readTree(join(root, "history")),
661
669
  readOptionalJson(join(root, "runtime", "additional-materials.json")),
670
+ readTree(join(root, "runtime", "history-backgrounds")),
662
671
  ]);
663
672
  if ((materials?.head ?? "genesis") !== authority.head)
664
673
  throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
@@ -672,6 +681,7 @@ export class FileNativeWorldStore {
672
681
  }, {}),
673
682
  additionalMaterials: materials?.items ?? [],
674
683
  history: materializedHistoryRecord(history),
684
+ historyBackgrounds: materializedBackgroundRecord(backgrounds, materializedHistoryRecord(history)),
675
685
  narrativeCheckpoint: await authorityStore.readNarrativeCheckpoint(),
676
686
  };
677
687
  });
@@ -702,6 +712,7 @@ export class FileNativeWorldStore {
702
712
  }, {}),
703
713
  additionalMaterials: structuredClone(endpoint.additionalMaterials),
704
714
  narrativeCheckpoint: endpoint.narrativeCheckpoint,
715
+ historyBackgrounds: historyBackgroundRecord(endpoint.history),
705
716
  history: Object.fromEntries(endpoint.history.map(({ messageId, exactText }) => [
706
717
  messageId,
707
718
  exactText,
@@ -841,6 +852,7 @@ export class FileNativeWorldStore {
841
852
  await mkdir(join(staging, "history"), { recursive: true });
842
853
  for (const file of historySurfaceFiles(selected.history))
843
854
  await writeIdempotentText(join(staging, "history", file.path), file.contents);
855
+ await replaceMaterializedTree(join(staging, "runtime", "history-backgrounds"), projectHistoryBackgrounds(selected.history));
844
856
  const genesis = {
845
857
  schemaVersion: 1,
846
858
  type: "file_native_genesis",
@@ -867,6 +879,7 @@ export class FileNativeWorldStore {
867
879
  head: input.sourceHead,
868
880
  sequence: clonedHead.sequence,
869
881
  commitDigest: clonedHead.commitDigest,
882
+ ...backgroundCount(selected.history),
870
883
  });
871
884
  await input.stageTarget?.({
872
885
  targetWorldRoot: staging,
@@ -879,6 +892,7 @@ export class FileNativeWorldStore {
879
892
  return files;
880
893
  }, {}),
881
894
  additionalMaterials: structuredClone(selected.additionalMaterials),
895
+ historyBackgrounds: historyBackgroundRecord(selected.history),
882
896
  history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
883
897
  messageId,
884
898
  exactText,
@@ -1032,11 +1046,26 @@ export class FileNativeWorldStore {
1032
1046
  !replaced.historyAppend.some(({ role }) => role === "player") ||
1033
1047
  logicalParent !== input.restoresHead)
1034
1048
  throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
1049
+ const binding = await this.bindPlayCallChainAt(input.worldId, input.restoresHead);
1050
+ const snapshot = WorldDocumentStore.open({
1051
+ layout: "world_state",
1052
+ files: Object.entries(binding.files).map(([path, contents]) => ({
1053
+ path,
1054
+ contents,
1055
+ })),
1056
+ });
1057
+ const background = snapshotBackground(captureNarrativeOrigin(snapshot, 1).value);
1035
1058
  return this.#commitHistoryChange({
1036
1059
  operationId: input.operationId,
1037
1060
  worldId: input.worldId,
1038
1061
  parentHead: input.expectedCurrentHead,
1039
- historyAppend: [{ role: "player", exactText: input.replacementText }],
1062
+ historyAppend: [
1063
+ {
1064
+ role: "player",
1065
+ exactText: input.replacementText,
1066
+ ...backgroundField(background),
1067
+ },
1068
+ ],
1040
1069
  nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
1041
1070
  stateChanges: [],
1042
1071
  mode: "timeline_revision",
@@ -1328,9 +1357,10 @@ export class FileNativeWorldStore {
1328
1357
  return {
1329
1358
  endpoint: recovered.head,
1330
1359
  state: recovered.state,
1331
- history: recovered.history.map(({ role, exactText }) => ({
1360
+ history: recovered.history.map(({ role, exactText, background }) => ({
1332
1361
  role,
1333
1362
  exactText,
1363
+ ...backgroundField(background),
1334
1364
  })),
1335
1365
  additionalMaterials: recovered.additionalMaterials,
1336
1366
  };
@@ -1636,7 +1666,11 @@ async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
1636
1666
  commit.auditParent.head !== outcome.parentHead ||
1637
1667
  commit.head !== outcome.head ||
1638
1668
  commit.mode !== outcome.mode ||
1639
- !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })), outcome.historyAppend) ||
1669
+ !isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
1670
+ role,
1671
+ exactText,
1672
+ ...backgroundField(background),
1673
+ })), outcome.historyAppend) ||
1640
1674
  !isDeepStrictEqual(commit.nextAdditionalMaterials, outcome.nextAdditionalMaterials))
1641
1675
  throw corruptOperationOutcome();
1642
1676
  return commit;
@@ -1725,12 +1759,10 @@ async function readMaterializedCheckpoint(root) {
1725
1759
  if (value === null)
1726
1760
  return null;
1727
1761
  if (!isRecord(value) ||
1728
- !hasExactKeys(value, [
1729
- "schemaVersion",
1730
- "head",
1731
- "sequence",
1732
- "commitDigest",
1733
- ]) ||
1762
+ !hasRequiredAndOptionalKeys(value, ["schemaVersion", "head", "sequence", "commitDigest"], ["historyBackgroundCount"]) ||
1763
+ (value.historyBackgroundCount !== undefined &&
1764
+ (!Number.isSafeInteger(value.historyBackgroundCount) ||
1765
+ Number(value.historyBackgroundCount) < 1)) ||
1734
1766
  value.schemaVersion !== 3 ||
1735
1767
  typeof value.head !== "string" ||
1736
1768
  !Number.isSafeInteger(value.sequence) ||
@@ -1753,7 +1785,10 @@ function sameMaterializedEndpoint(checkpoint, authority) {
1753
1785
  }
1754
1786
  function isPlayHistoryAppendInput(value) {
1755
1787
  return (isRecord(value) &&
1756
- hasExactKeys(value, ["role", "exactText"]) &&
1788
+ hasRequiredAndOptionalKeys(value, ["role", "exactText"], ["background"]) &&
1789
+ (value.background === undefined ||
1790
+ (isHistoryBackground(value.background) &&
1791
+ (value.role !== "player" || value.background.kind === "snapshot"))) &&
1757
1792
  (value.role === "player" || value.role === "narrator") &&
1758
1793
  typeof value.exactText === "string");
1759
1794
  }
@@ -1763,6 +1798,7 @@ async function materializeRecoveredEndpoint(root, authority, endpoint) {
1763
1798
  await replaceMaterializedTree(join(root, "state"), endpoint.state);
1764
1799
  crashAtFileNativeAuthorityEdge("after_state_materialization");
1765
1800
  await replaceMaterializedTree(join(root, "history"), historySurfaceFiles(endpoint.history));
1801
+ await replaceMaterializedTree(join(root, "runtime", "history-backgrounds"), projectHistoryBackgrounds(endpoint.history));
1766
1802
  await publishJson(join(root, "runtime", "additional-materials.json"), {
1767
1803
  head: authority.head,
1768
1804
  items: endpoint.additionalMaterials,
@@ -1772,6 +1808,7 @@ async function materializeRecoveredEndpoint(root, authority, endpoint) {
1772
1808
  head: authority.head,
1773
1809
  sequence: authority.sequence,
1774
1810
  commitDigest: authority.commitDigest,
1811
+ ...backgroundCount(endpoint.history),
1775
1812
  });
1776
1813
  }
1777
1814
  async function replaceMaterializedTree(root, files) {
@@ -1900,7 +1937,11 @@ function authorityCommitMatchesInput(commit, input) {
1900
1937
  commit.auditParent.head === input.parentHead &&
1901
1938
  commit.timelineParent.head ===
1902
1939
  (input.timelineRevision?.restoresHead ?? input.parentHead) &&
1903
- isDeepStrictEqual(commit.historyAppend.map(({ role, exactText }) => ({ role, exactText })), input.historyAppend) &&
1940
+ isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
1941
+ role,
1942
+ exactText,
1943
+ ...backgroundField(background),
1944
+ })), input.historyAppend) &&
1904
1945
  stateChangesMatch &&
1905
1946
  isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
1906
1947
  isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
@@ -1989,7 +2030,9 @@ async function readGenesisForFork(worldRoot, worldId) {
1989
2030
  const prefix = `${worldId}.`;
1990
2031
  const history = value.history.map((message, index) => {
1991
2032
  if (!isRecord(message) ||
1992
- !hasExactKeys(message, ["messageId", "role", "exactText"]) ||
2033
+ !hasRequiredAndOptionalKeys(message, ["messageId", "role", "exactText"], ["background"]) ||
2034
+ (message.background !== undefined &&
2035
+ !isHistoryBackground(message.background)) ||
1993
2036
  typeof message.messageId !== "string" ||
1994
2037
  (message.role !== "player" && message.role !== "narrator") ||
1995
2038
  typeof message.exactText !== "string")
@@ -2007,6 +2050,7 @@ async function readGenesisForFork(worldRoot, worldId) {
2007
2050
  messageId,
2008
2051
  role,
2009
2052
  exactText: message.exactText,
2053
+ ...backgroundField(message.background),
2010
2054
  };
2011
2055
  });
2012
2056
  let additionalMaterials;
@@ -2122,3 +2166,21 @@ function crashAtFileNativeAuthorityEdge(edge) {
2122
2166
  }
2123
2167
  }
2124
2168
  import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
2169
+ function backgroundField(background) {
2170
+ return background === undefined ? {} : { background };
2171
+ }
2172
+ function materializedBackgroundRecord(files, history) {
2173
+ return Object.fromEntries(files.map(({ path, contents }) => {
2174
+ const id = path.replace(/\.json$/u, "");
2175
+ const value = JSON.parse(contents);
2176
+ if (path !== `${id}.json` ||
2177
+ !Object.hasOwn(history, id) ||
2178
+ !isHistoryBackground(value))
2179
+ throw new FileNativeWorldCreationError("world_corrupt", "Materialized history background is invalid or unreachable");
2180
+ return [id, value];
2181
+ }));
2182
+ }
2183
+ function backgroundCount(messages) {
2184
+ const count = messages.filter(({ background }) => background !== undefined).length;
2185
+ return count === 0 ? {} : { historyBackgroundCount: count };
2186
+ }
@@ -0,0 +1,28 @@
1
+ import { stringify } from "yaml";
2
+ /** Shared public YAML rendering; references keep their handle without expansion. */
3
+ export function renderWorldYamlSource(value, options = {}) {
4
+ return stringify(publicValue(value), {
5
+ indent: 2,
6
+ lineWidth: 0,
7
+ ...options,
8
+ }).trimEnd();
9
+ }
10
+ function publicValue(value) {
11
+ if (Array.isArray(value))
12
+ return value.map(publicValue);
13
+ if (record(value)) {
14
+ if (Object.keys(value).length === 2 &&
15
+ typeof value.$ref === "string" &&
16
+ record(value.target) &&
17
+ typeof value.target.shortRef === "string")
18
+ return { $ref: `@${value.target.shortRef}` };
19
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, publicValue(child)]));
20
+ }
21
+ return value;
22
+ }
23
+ function record(value) {
24
+ return (value !== undefined &&
25
+ value !== null &&
26
+ typeof value === "object" &&
27
+ !Array.isArray(value));
28
+ }
@@ -239,10 +239,13 @@ summary 是目录简介,用于识别文档和判断是否需要读取。只写
239
239
  先按玩家授权与互动需要确定本轮叙事范围和停笔位置,再保存该范围内的事实。保存进行中活动的实际进展、必要原话和待回应事项即可整理截至此刻的状态,不把尚未发生的完成结果提前写入。
240
240
 
241
241
  1. 玩家视图实际绑定的当前值改变,本轮终态叙事前写回。等待、赶路、休息或跨日后按世界粒度更新对应现值;绑定标记只说明界面读取范围。
242
- 2. 需要保存但玩家原文和最终叙事没有记录的信息,当轮保存,包括重要场外进展、未表露的认知和隐蔽后果。工具中间文本不能替代持久记录。
243
- 3. 其余可从玩家原文和最终叙事恢复的持续结果,允许事件收尾或合适的中间整理点归并。
242
+ 2. 当前情境已采用顶层 \`背景\` 时,时间、地点等定位值发生变化应在最终叙事提交前写回该节点,避免在其他情境字段维护同一现值的第二份。没有该字段的世界不必创建,未变化不必写入,仅维护记录不授权推进剧情。
243
+ 3. 需要保存但玩家原文和最终叙事没有记录的信息,当轮保存,包括重要场外进展、未表露的认知和隐蔽后果。工具中间文本不能替代持久记录。
244
+ 4. 其余可从玩家原文和最终叙事恢复的持续结果,允许事件收尾或合适的中间整理点归并。
244
245
 
245
- 按已确定的叙事范围完成本轮必须保存的写入。只有本轮选择登记检查点时,才核对上次检查点后的交互和本轮范围内的结果,补齐必要记录,更新认知、进程、约定和界面值,结束实际已失效的待办并清理重复。整理可以停在活动中途;准备好该停笔位置的记录后登记检查点,再输出最终叙事。检查点覆盖该叙事,若其中又增加重要结果,继续工具调用补齐。正常回复无需登记检查点,登记回执也不是语义完整性认证。
246
+ 按已确定的叙事范围完成本轮必须保存的写入。在本轮原定停笔位置判断整理时机:事件自然告一段落、实际转换场景,或持续活动经过多轮并积累了需要归并的进展时,主动集中整理,不等玩家要求,也不等整个活动完成。只有零散即时变化时,按上述保存时机继续互动;一次写入不等于一次检查点。
247
+
248
+ 整理时核对上次检查点后的交互和本轮范围内的结果,补齐必要记录,更新认知、进程、约定和界面值,结束实际已失效的待办并清理重复。此前已写回的事实核对后沿用,无需重复写入。准备好该停笔位置的记录后,调用 world_checkpoint,再输出最终叙事;进行中的活动和待回应事项按原状保存,整理不改变停笔位置。检查点覆盖该叙事,若其中又增加重要结果,继续工具调用补齐。登记回执不是语义完整性认证。
246
249
 
247
250
  ## 整理与连续性
248
251
 
@@ -494,10 +497,13 @@ summary is a catalog description for identifying a document and deciding whether
494
497
  First choose this turn's narrative scope and stopping point from player authorization and interaction needs, then save facts within that scope. Recording actual progress in an ongoing activity, necessary exact wording and matters awaiting a response can complete maintenance through this moment without writing unrealized completion outcomes in advance.
495
498
 
496
499
  1. Write changed current values actually selected by player views before this turn's final narrative. After waiting, travel, rest or a day change, update those values at the world's granularity; binding annotations describe read locations only.
497
- 2. Save needed information absent from player originals and final narrative during this turn, including important offstage progress, unexpressed knowledge and hidden consequences. Intermediate tool text is not a durable substitute.
498
- 3. Other durable outcomes recoverable from player originals and final narrative may be consolidated at event closure or a suitable intermediate point.
500
+ 2. If the current situation already uses the exact top-level \`背景\` field, write changed time, location or other positioning values there before final narrative submission; avoid maintaining a second current copy elsewhere in that situation. Worlds without the field need not create it, unchanged values need no write, and record maintenance does not authorize story advancement.
501
+ 3. Save needed information absent from player originals and final narrative during this turn, including important offstage progress, unexpressed knowledge and hidden consequences. Intermediate tool text is not a durable substitute.
502
+ 4. Other durable outcomes recoverable from player originals and final narrative may be consolidated at event closure or a suitable intermediate point.
503
+
504
+ Complete required writes within the chosen narrative scope. At this turn's already chosen stopping point, assess whether consolidation is due: proactively consolidate when an event naturally settles, the scene actually changes, or an ongoing activity has accumulated progress across several turns that needs consolidation. Do not wait for the player to request maintenance or for the whole activity to finish. With only isolated immediate changes, continue the interaction under the saving rules above; one write does not mean one checkpoint.
499
505
 
500
- Complete required writes within the chosen narrative scope. Only if you choose to register a checkpoint this turn, reconcile interactions since the previous checkpoint and outcomes within this turn's scope: fill necessary records, update knowledge, processes, commitments and interface values, close actually expired pending work and remove duplicates. Maintenance can stop during an activity; prepare records for that stopping point, register the checkpoint, then produce final narrative. The checkpoint covers that narrative; if it adds another important outcome, continue tool calls to save it. A normal reply needs no checkpoint, and registration is not certification of semantic completeness.
506
+ When consolidating, reconcile interactions since the previous checkpoint and outcomes within this turn's scope: fill necessary records, update knowledge, processes, commitments and interface values, close actually expired pending work and remove duplicates. Reuse already saved facts after checking them, without duplicate writes. Once records for that stopping point are ready, call world_checkpoint, then produce final narrative. Preserve ongoing activities and matters awaiting a response as they stand; maintenance does not move the stopping point. The checkpoint covers that narrative; if it adds another important outcome, continue tool calls to save it. Registration is not certification of semantic completeness.
501
507
 
502
508
  ## Maintenance and continuity
503
509
 
@@ -27,6 +27,8 @@ export const defaultSettingImprovementPromptZhCN = `# 系统推荐的创作方
27
27
 
28
28
  这些区别适用于物品、伤病、地点、组织、交易、调查与人物关系,不要求固定目录、字段或数量。独立引用、自身状态或生命周期需要独立文档;小规模信息可集中在合适载体中。当前情境只承接眼前地点、在场者、进行中的事及待回应事项,不承担全部历史或未来分支。
29
29
 
30
+ 当前情境的受限 YAML 可选用精确顶层「背景」保存简短定位值,例如「背景: {时间: 青历三年六月初八,夜间, 地点: 南门客栈}」。Runtime 会将该节点的完整值随已提交叙事保存,用于解释旧原文的“明天”“这里”;缺失、null、空白字符串、空映射或空数组不采集,0 和 false 保留。字段内部不设固定 schema,在场名单、待办和长局面留在其他字段;采用后在背景中维护对应现值,避免同一时间地点的第二份现值。无需为未采用的既有内容创建或搬迁字段;运行中修订不会改写旧历史背景。
31
+
30
32
  summary 是目录简介:只写识别文档所需的简短身份、主题或稳定线索,不复述具体状态和事件经过。具体事实放在正文,仅在识别信息改变时更新标题与简介。
31
33
 
32
34
  ## 承载与发现一起安排
@@ -65,6 +67,8 @@ Keep important event accounts in one place, using independently referenceable ev
65
67
 
66
68
  These distinctions apply to objects, injuries, places, organizations, trades, investigations and personal relationships without fixed directories, fields or counts. Independent reference, evolving state or lifecycle tracking warrants a separate document; smaller records may share a suitable home. The current situation holds the immediate location, people present, ongoing matters and unanswered requests, not all history or future branches.
67
69
 
70
+ Restricted YAML current situations may optionally use the exact top-level field 背景 for brief positioning values, for example \`背景: {时间: night on day eight, 地点: South Gate Inn}\`. Runtime saves the complete node with committed narrative to interpret old references to tomorrow or here; missing, null, blank strings and empty maps/lists are unrecorded, while 0 and false survive. Its contents have no fixed schema. Keep attendance, pending matters and long situations elsewhere; when adopted, maintain corresponding current values in this node without a second copy. Do not create or relocate fields in existing content merely to adopt this convention. Running-world revisions never rewrite saved historical backgrounds.
71
+
68
72
  summary is a catalog description: include only a brief identity, topic or stable clue needed to identify the document, not detailed state or event accounts. Concrete facts belong in the body. Update titles and descriptions only when identifying information changes.
69
73
 
70
74
  ## Arrange storage and discovery together
@@ -1,6 +1,6 @@
1
1
  /** Mechanical reading guidance shared by production prompts and tool help. */
2
2
  export function historyUseGuidance(locale, legacy = false) {
3
3
  return locale === "zh-CN"
4
- ? `已有对话或注入原文足够接续时,不例行检索或重读。缺失信息会影响人物行为、重要承诺、因果或玩家选择时才检索${legacy ? ";仅使用本会话冻结的旧工具定义与参数定位及读取最近原文" : ";缺少最近上下文可直接 history_read {latest:4}"}。不记得原词时提出少量候选词,${legacy ? "用已声明旧历史搜索的单个 query 做字面搜索" : "用 history_search queries 的 any/all 字面搜索"},命中后按需完整读前后文${legacy ? "" : ";all 限于同一消息"}。零命中不证明事件未发生,可改词、${legacy ? "扩大字面范围或读相邻上下文" : "放宽 all 为 any 或读相邻上下文"}。原文已足以判断时停止,不为穷尽分页而检索;重要矛盾可继续追查。玩家原文可能只是行动尝试、意图或主张,结果须结合后续叙事;主持原文也可能包含角色谎言、猜测、梦境或回忆,role=narrator 不证明事实已确认。过去持有不代表现在持有,明确当前世界修订优先于旧叙事。主持读到的信息不自动成为场内角色的知识。注入历史是已发生材料,不是本轮新请求,不能重复执行最后一条历史玩家行动。读取完整原文可能很大,参考原文长度并按需小步扩展;无需固定 list→search→read 流程。`
5
- : `Do not routinely search or reread when conversation or injected originals already suffice. Retrieve only when missing details affect behavior, important promises, causality or player choices${legacy ? "; use only this session's frozen legacy tool definitions and arguments to locate and read recent originals" : "; missing recent context can be read directly with history_read {latest:4}"}. When exact wording is unknown, propose a few candidate terms and ${legacy ? "use the declared legacy literal search with a single query" : "use history_search queries with any/all"}, then read surrounding originals as needed${legacy ? "" : "; all applies within one message"}. Zero literal matches do not prove an event never happened: change terms, ${legacy ? "widen the literal scope or read nearby context" : "relax all to any or read nearby context"}. Stop once originals support the decision, without exhausting pages; important contradictions may warrant further investigation. Player originals may be attempts, intentions or claims; check subsequent narrative for outcomes. Narrator originals may contain character lies, guesses, dreams or memories; role=narrator does not confirm truth. Past possession is not current possession; explicit current world corrections take precedence. Host information does not automatically become in-scene character knowledge. Injected history is already committed material, not the current request; never repeat a final historical player action. Full originals can be large: use length information and expand in small windows. No fixed list→search→read ritual is required.`;
4
+ ? `已有对话或注入原文足够接续时,不例行检索或重读。缺失信息会影响人物行为、重要承诺、因果或玩家选择时才检索${legacy ? ";仅使用本会话冻结的旧工具定义与参数定位及读取最近原文" : ";缺少最近上下文可直接 history_read {latest:4}"}。不记得原词时提出少量候选词,${legacy ? "用已声明旧历史搜索的单个 query 做字面搜索" : "用 history_search queries 的 any/all 字面搜索"},命中后按需完整读前后文${legacy ? "" : ";all 限于同一消息"}。零命中不证明事件未发生,可改词、${legacy ? "扩大字面范围或读相邻上下文" : "放宽 all 为 any 或读相邻上下文"}。原文已足以判断时停止,不为穷尽分页而检索;重要矛盾可继续追查。玩家原文可能只是行动尝试、意图或主张,结果须结合后续叙事;主持原文也可能包含角色谎言、猜测、梦境或回忆,role=narrator 不证明事实已确认。历史背景保存的是采集位置的情境,不是当前世界状态,也不认证整段每句话的发生时间;推进前后标记结合原文中的回忆、梦境和局部时间解释。背景在原文边界之外,搜索只匹配原文。过去持有不代表现在持有,明确当前世界修订优先于旧叙事。主持读到的信息不自动成为场内角色的知识。注入历史是已发生材料,不是本轮新请求,不能重复执行最后一条历史玩家行动。读取完整原文可能很大,参考原文长度并按需小步扩展;无需固定 list→search→read 流程。`
5
+ : `Do not routinely search or reread when conversation or injected originals already suffice. Retrieve only when missing details affect behavior, important promises, causality or player choices${legacy ? "; use only this session's frozen legacy tool definitions and arguments to locate and read recent originals" : "; missing recent context can be read directly with history_read {latest:4}"}. When exact wording is unknown, propose a few candidate terms and ${legacy ? "use the declared legacy literal search with a single query" : "use history_search queries with any/all"}, then read surrounding originals as needed${legacy ? "" : "; all applies within one message"}. Zero literal matches do not prove an event never happened: change terms, ${legacy ? "widen the literal scope or read nearby context" : "relax all to any or read nearby context"}. Stop once originals support the decision, without exhausting pages; important contradictions may warrant further investigation. Player originals may be attempts, intentions or claims; check subsequent narrative for outcomes. Narrator originals may contain character lies, guesses, dreams or memories; role=narrator does not confirm truth. Historical background records the captured situation, not current world state or the time of every sentence; interpret before/after annotations alongside memories, dreams and local times in the original. Background stays outside original-text boundaries and literal searches match only originals. Past possession is not current possession; explicit current world corrections take precedence. Host information does not automatically become in-scene character knowledge. Injected history is already committed material, not the current request; never repeat a final historical player action. Full originals can be large: use length information and expand in small windows. No fixed list→search→read ritual is required.`;
6
6
  }
@@ -22,7 +22,7 @@ export function builtinPlayPrompts(locale) {
22
22
 
23
23
  world_patch 的 YAML locator 可以包含 map key 和从零开始的数组下标;remove 删除一个精确位置。未提供的元数据保持原值。world_retire 从目录注入中退役文档,文档仍可读取、引用和恢复。工具响应的成功状态变化与后续最终叙事分别提交,后续失败不回滚已提交变化。成功写入回执表示当前值已改变;以真实回执的结算状态为准,模型文字不能提交世界。
24
24
 
25
- 包含工具调用的响应属于中间步,其中的文字尚未展示或提交为最终叙事。收到全部工具结果后,根据回执在最终正文中呈现本轮尚未展示的内容,并与已提交状态一致;没有工具调用的非空正文结束本次循环并由 Runtime 提交。world_checkpoint 仅登记整理声明与历史重放边界,在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;登记后仍可继续工具调用。正常回复无需检查点,登记不结束世界内事件,也不自动切换上下文,玩家自行选择。全新上下文补入边界后的已提交玩家原文与最终叙事,排除开场白、工具和推理。
25
+ 包含工具调用的响应属于中间步,其中的文字尚未展示或提交为最终叙事。收到全部工具结果后,根据回执在最终正文中呈现本轮尚未展示的内容,并与已提交状态一致;没有工具调用的非空正文结束本次循环并由 Runtime 提交。world_checkpoint 仅登记整理声明与历史重放边界,在随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文;登记后仍可继续工具调用。登记不结束世界内事件,也不自动切换上下文,玩家自行选择。全新上下文补入边界后的已提交玩家原文与最终叙事,排除开场白、工具和推理。
26
26
 
27
27
  ${historyUseGuidance(locale)}`
28
28
  : `# Tools and response settlement
@@ -31,7 +31,7 @@ Use only attached tools. Directories use @dir-* handles from the preload or tool
31
31
 
32
32
  YAML world_patch locators accept map keys and zero-based array indexes; remove deletes one exact location. Omitted metadata remains unchanged. world_retire removes a document from catalog injection while keeping it readable, referenceable and restorable. Successful tool-response state changes and later final narrative commit separately; later failure does not roll back committed changes. Successful write receipts change current values; the actual receipt determines settlement, not model prose.
33
33
 
34
- Responses containing tool calls are intermediate steps; their text has not yet been displayed or committed as final narrative. After all tool results, use the receipts to present this turn's still-unshown content in final prose consistent with committed state. A nonempty tool-free response ends this loop and Runtime commits it. world_checkpoint only registers a consolidation declaration and history-replay boundary; it takes effect after the subsequent nonempty tool-free final prose successfully commits, including that prose. Further tool calls may follow registration. Normal replies need no checkpoint; registration neither ends an in-world event nor switches context, which the player chooses. Fresh-context replay contains committed player originals and final narratives after the boundary, excluding the opening, tools and reasoning.
34
+ Responses containing tool calls are intermediate steps; their text has not yet been displayed or committed as final narrative. After all tool results, use the receipts to present this turn's still-unshown content in final prose consistent with committed state. A nonempty tool-free response ends this loop and Runtime commits it. world_checkpoint only registers a consolidation declaration and history-replay boundary; it takes effect after the subsequent nonempty tool-free final prose successfully commits, including that prose. Further tool calls may follow registration. Registration neither ends an in-world event nor switches context, which the player chooses. Fresh-context replay contains committed player originals and final narratives after the boundary, excluding the opening, tools and reasoning.
35
35
 
36
36
  ${historyUseGuidance(locale)}`,
37
37
  },