narraeon 0.7.10 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/node/protocol/modelConnections.js +15 -0
  2. package/dist/node/protocol/modelUsage.js +4 -0
  3. package/dist/node/protocol/sillyTavernContent.js +1 -0
  4. package/dist/node/protocol/v1.js +97 -0
  5. package/dist/node/runtime/V1Runtime.js +105 -3
  6. package/dist/node/runtime/authoring/AuthoringEditPlan.js +79 -0
  7. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  8. package/dist/node/runtime/authoring/AuthoringRetry.js +59 -0
  9. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  10. package/dist/node/runtime/authoring/ModelToolConversation.js +66 -25
  11. package/dist/node/runtime/content/ContentWorkspace.js +10 -2
  12. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +14 -8
  13. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  14. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  15. package/dist/node/runtime/model/DispatchFailure.js +8 -0
  16. package/dist/node/runtime/model/FileNativeModelAdapters.js +627 -18
  17. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  18. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  19. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  20. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  21. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  22. package/dist/node/runtime/model/ProviderStream.js +4 -2
  23. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  24. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  25. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +199 -10
  26. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  27. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +51 -23
  28. package/dist/node/runtime/play/PlayCallChain.js +92 -8
  29. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  30. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  31. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +321 -15
  32. package/dist/node/runtime/setting/SettingImprovementSession.js +285 -21
  33. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +236 -15
  34. package/dist/node/runtime/world-revision/WorldRevisionSession.js +324 -50
  35. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +47 -1
  36. package/dist/node/shared/application-preset-files.js +29 -0
  37. package/dist/node/shared/default-preset-host.js +50 -38
  38. package/dist/node/shared/first-party-action-choices.js +7 -11
  39. package/dist/node/shared/first-party-generic-panels.js +7 -11
  40. package/dist/node/shared/first-party-player-view.js +7 -11
  41. package/dist/node/shared/ordered-author-prompts.js +6 -4
  42. package/dist/node/shared/ordered-play-prompts.js +4 -2
  43. package/dist/node/shared/prompt-arrangement.js +205 -0
  44. package/dist/node/shared/sillytavern-preset.js +454 -0
  45. package/dist/web/assets/index-BthiDIQu.js +1432 -0
  46. package/dist/web/assets/{index-DH2BVsYd.css → index-wO9Y4HT1.css} +1 -1
  47. package/dist/web/index.html +2 -2
  48. package/package.json +1 -1
  49. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -1,15 +1,17 @@
1
+ import { applicationPresetFilesForLocale } from "../../shared/application-preset-files.js";
1
2
  import { parseDisplayRegex, DisplayRegexError, } from "../../shared/display-regex.js";
2
3
  export { validatePlayPresetArtifactPayload } from "../../shared/artifact-payload-contract.js";
3
4
  import { defaultFollowupItems, } from "../../shared/ordered-followups.js";
4
5
  import { parseFollowupItems } from "./OrderedFollowups.js";
5
6
  import { builtinAuthorPrompts, defaultOrderedAuthorPrompts, } from "../../shared/ordered-author-prompts.js";
6
- import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts, withCurrentPromptBuiltins, } from "./OrderedPlayPrompts.js";
7
- import { defaultOrderedPlayPrompts, } from "../../shared/ordered-play-prompts.js";
7
+ import { parseOrderedPlayPrompts, parseReleasedOrderedPrompts, releasedPromptRequiredIdentities, parsePromptArrangement, migrateLegacyPlayPrompts, upgradeOrderedPromptFormat, } from "./OrderedPlayPrompts.js";
8
+ import { builtinPlayPrompts, } from "../../shared/ordered-play-prompts.js";
9
+ import { customizePromptArrangement, retiredBuiltinReplacements, } from "../../shared/prompt-arrangement.js";
8
10
  import { createHash, randomUUID } from "node:crypto";
9
11
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
10
12
  import { join } from "node:path";
11
13
  import { isDeepStrictEqual } from "node:util";
12
- import { parseDocument, stringify, visit, isAlias } from "yaml";
14
+ import { parseDocument, visit, isAlias } from "yaml";
13
15
  import { defaultPortableContentTreeLimits, portableContentTreePathKey, } from "../../protocol/contentTree.js";
14
16
  import { defaultAppLocale, } from "../../protocol/appPreferences.js";
15
17
  import { defaultNarrationPromptForLocale } from "../../shared/default-play-prompts.js";
@@ -95,27 +97,78 @@ export function isPlayPresetBinding(value) {
95
97
  const builtInDefault = value.id === "builtin-default" &&
96
98
  value.name === "default" &&
97
99
  value.revision === "builtin-default-v1" &&
98
- value.scriptsEnabled === true &&
99
- (isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("en")) ||
100
- isDeepStrictEqual(files, defaultPlayPresetFilesForLocale("zh-CN")) ||
101
- isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("en")) ||
102
- isDeepStrictEqual(files, legacyDefaultPlayPresetFilesForLocale("zh-CN")));
100
+ value.scriptsEnabled === true;
101
+ // This internal sentinel predates content-hash revisions. Its original
102
+ // snapshot must validate on its own, never against today's seed template.
103
103
  return (parsed.kind === "valid" &&
104
- isDeepStrictEqual(normalizedFrozenDefinition(value.definition), parsed.definition) &&
104
+ frozenDefinitionMatches(value.definition, parsed.definition, files) &&
105
105
  (builtInDefault || value.revision === revisionForPlayPresetFiles(files)));
106
106
  }
107
+ /** A frozen v3 binding validates against the catalog identities it captured.
108
+ * Newly shipped defaults belong to new sends, never to a saved request. */
109
+ function frozenDefinitionMatches(snapshot, current, files) {
110
+ if (current.format !== "narraeon.play-preset/v3")
111
+ return isDeepStrictEqual(normalizedFrozenDefinition(snapshot), current);
112
+ try {
113
+ const source = readYaml(files, "preset.yaml", "preset.yaml");
114
+ const expected = { ...current };
115
+ const normalizedSnapshot = { ...snapshot };
116
+ for (const key of ["playPrompts", "authorPrompts"]) {
117
+ if (source[key] === undefined && snapshot[key] === undefined)
118
+ continue;
119
+ const author = key === "authorPrompts";
120
+ const catalog = author
121
+ ? builtinAuthorPrompts("en")
122
+ : builtinPlayPrompts("en");
123
+ const raw = snapshot[key];
124
+ if (!Array.isArray(raw))
125
+ return false;
126
+ const identities = new Set(raw.flatMap((e) => isRecord(e) && e.kind === "builtin" && typeof e.builtin === "string"
127
+ ? (retiredBuiltinReplacements[e.builtin]?.map((r) => r.builtin) ?? [
128
+ e.builtin,
129
+ ])
130
+ : []));
131
+ // v3's original mandatory identities are part of its validation contract.
132
+ // Later required additions are mandatory only for newly compiled requests.
133
+ const requiredAtRelease = releasedPromptRequiredIdentities(!author);
134
+ const capturedCatalog = catalog.filter((c) => identities.has(c.id) || requiredAtRelease.includes(c.id));
135
+ const captured = parseOrderedPlayPrompts(raw, capturedCatalog, !author);
136
+ const frozenCatalog = captured.flatMap((entry) => {
137
+ if (entry.kind !== "builtin")
138
+ return [];
139
+ const item = capturedCatalog.find((item) => item.id === entry.builtin);
140
+ return item === undefined
141
+ ? []
142
+ : [{ ...item, defaultEnabled: entry.enabled }];
143
+ });
144
+ if (!isDeepStrictEqual(captured, parsePromptArrangement(source[key], frozenCatalog, !author)))
145
+ return false;
146
+ expected[key] = captured;
147
+ normalizedSnapshot[key] = captured;
148
+ }
149
+ return isDeepStrictEqual(normalizedSnapshot, expected);
150
+ }
151
+ catch {
152
+ return false;
153
+ }
154
+ }
107
155
  /**
108
156
  * A frozen definition captured before a builtin was retired still lists the
109
- * old entry; reading its ordered list through the parser yields the same
110
- * successors the files now parse to, so the binding stays consistent.
157
+ * old entry; reading both ordered lists through their respective parsers yields
158
+ * the same successors as the files, so the binding stays consistent.
111
159
  */
112
160
  function normalizedFrozenDefinition(definition) {
113
- if (!Array.isArray(definition.playPrompts))
114
- return definition;
115
161
  try {
116
162
  return {
117
163
  ...definition,
118
- playPrompts: parseOrderedPlayPrompts(definition.playPrompts),
164
+ ...(Array.isArray(definition.playPrompts)
165
+ ? { playPrompts: parseReleasedOrderedPrompts(definition.playPrompts) }
166
+ : {}),
167
+ ...(Array.isArray(definition.authorPrompts)
168
+ ? {
169
+ authorPrompts: parseReleasedOrderedPrompts(definition.authorPrompts, builtinAuthorPrompts("en"), false),
170
+ }
171
+ : {}),
119
172
  };
120
173
  }
121
174
  catch {
@@ -151,14 +204,7 @@ followups: []
151
204
  };
152
205
  }
153
206
  export function defaultPlayPresetFilesForLocale(locale) {
154
- const files = legacyDefaultPlayPresetFilesForLocale(locale);
155
- const preset = parseDocument(files["preset.yaml"]);
156
- preset.set("format", "narraeon.play-preset/v2");
157
- preset.set("playPrompts", defaultOrderedPlayPrompts());
158
- preset.set("authorPrompts", defaultOrderedAuthorPrompts());
159
- preset.set("mergeConsecutiveMessages", false);
160
- files["preset.yaml"] = stringify(preset.toJS());
161
- return files;
207
+ return applicationPresetFilesForLocale(locale);
162
208
  }
163
209
  export const defaultPlayPresetFiles = defaultPlayPresetFilesForLocale(defaultAppLocale);
164
210
  /**
@@ -230,8 +276,8 @@ export class FileNativePlayPresetStore {
230
276
  const document = await this.#read();
231
277
  const migrated = this.#markLegacyBuiltinDefault(document);
232
278
  const synchronized = this.#synchronizeBuiltinDefault(document);
233
- const tabletop = this.#migrateTabletopPromptBuiltins(document);
234
- if (migrated || synchronized || tabletop)
279
+ const customizations = this.#migratePromptCustomizations(document);
280
+ if (migrated || synchronized || customizations)
235
281
  await this.#write(document);
236
282
  });
237
283
  }
@@ -291,10 +337,18 @@ export class FileNativePlayPresetStore {
291
337
  const stored = findStored(document, input.presetId);
292
338
  delete stored.builtinDefault;
293
339
  let files = cloneFiles(input.files);
340
+ const previous = parsePlayPresetFiles(stored.revisions[stored.currentRevision]);
294
341
  if (input.structure !== undefined) {
295
- files = applyPlayPresetStructuredEditor(input.files, parsePlayPresetStructuredEditor(input.structure));
342
+ const structure = parsePlayPresetStructuredEditor(input.structure);
343
+ if (previous.kind === "valid") {
344
+ for (const key of ["playPrompts", "authorPrompts"]) {
345
+ if (structure[key] === undefined)
346
+ continue;
347
+ assertReadonlyPromptsRetained(previous.definition[key] ?? [], structure[key]);
348
+ }
349
+ }
350
+ files = applyPlayPresetStructuredEditor(input.files, structure);
296
351
  }
297
- const previous = parsePlayPresetFiles(stored.revisions[stored.currentRevision]);
298
352
  if (previous.kind === "valid" &&
299
353
  previous.definition.followupItems !== undefined) {
300
354
  const candidate = readYaml(files, "call-chain.yaml", "call-chain.yaml");
@@ -305,31 +359,23 @@ export class FileNativePlayPresetStore {
305
359
  if (previous.kind === "valid" &&
306
360
  previous.definition.playPrompts !== undefined) {
307
361
  const candidate = readYaml(files, "preset.yaml", "preset.yaml");
308
- if (candidate.format !== "narraeon.play-preset/v2")
362
+ if (candidate.format !== "narraeon.play-preset/v2" &&
363
+ candidate.format !== "narraeon.play-preset/v3")
309
364
  throw new FileNativePlayPresetError("readonly_prompt_removed", "Ordered presets cannot downgrade to v1");
310
- const candidates = parseOrderedPlayPrompts(candidate.playPrompts);
311
- for (const entry of previous.definition.playPrompts.filter((entry) => entry.kind === "builtin" || entry.kind === "world")) {
312
- const retained = candidates.find((item) => item.id === entry.id);
313
- if (retained?.kind !== entry.kind ||
314
- (entry.kind === "builtin" &&
315
- retained.kind === "builtin" &&
316
- retained.builtin !== entry.builtin))
317
- throw new FileNativePlayPresetError("readonly_prompt_removed", "System prompts and the world placeholder cannot be deleted or replaced; disable optional prompts or clone them instead");
318
- }
365
+ const candidates = candidate.format === "narraeon.play-preset/v3"
366
+ ? parsePromptArrangement(candidate.playPrompts)
367
+ : parseOrderedPlayPrompts(candidate.playPrompts);
368
+ assertReadonlyPromptsRetained(previous.definition.playPrompts, candidates);
319
369
  }
320
370
  if (previous.kind === "valid" &&
321
371
  previous.definition.authorPrompts !== undefined) {
322
372
  const candidate = readYaml(files, "preset.yaml", "preset.yaml");
323
- const entries = parseOrderedAuthorPrompts(candidate.authorPrompts);
324
- for (const entry of previous.definition.authorPrompts) {
325
- if (entry.kind !== "builtin")
326
- continue;
327
- const retained = entries.find((item) => item.id === entry.id);
328
- if (retained?.kind !== "builtin" ||
329
- retained.builtin !== entry.builtin)
330
- throw new FileNativePlayPresetError("readonly_prompt_removed", "Authoring system prompts cannot be deleted or replaced; disable or clone optional prompts");
331
- }
373
+ const entries = candidate.format === "narraeon.play-preset/v3"
374
+ ? parsePromptArrangement(candidate.authorPrompts, builtinAuthorPrompts("en"), false)
375
+ : parseOrderedAuthorPrompts(candidate.authorPrompts);
376
+ assertReadonlyPromptsRetained(previous.definition.authorPrompts, entries);
332
377
  }
378
+ files = upgradeValidOrderedPromptFormat(files, this.#limits);
333
379
  const revision = revisionForFiles(files, this.#limits);
334
380
  stored.name = normalizeName(input.name);
335
381
  stored.revisions[revision] = cloneFiles(files);
@@ -472,14 +518,16 @@ export class FileNativePlayPresetStore {
472
518
  }
473
519
  /** Portable export deliberately contains no id, revision, world, operation or provider state. */
474
520
  async exportPortable(id) {
475
- const binding = await this.bindRevision(id);
476
- return Object.keys(binding.files)
521
+ await this.#tail;
522
+ const stored = findStored(await this.#read(), id);
523
+ const files = stored.revisions[stored.currentRevision];
524
+ return Object.keys(files)
477
525
  .sort()
478
- .map((path) => ({ path, contents: binding.files[path] }));
526
+ .map((path) => ({ path, contents: files[path] }));
479
527
  }
480
528
  async importPortable(input) {
481
529
  // A released preset gains the current builtins at import, as at upgrade.
482
- const files = withCurrentPromptBuiltins(toFileMap(input.files, this.#limits));
530
+ const files = upgradeOrderedPromptFormat(toFileMap(input.files, this.#limits));
483
531
  const parsed = parsePlayPresetFiles(files, this.#limits);
484
532
  if (parsed.kind === "invalid")
485
533
  throw parsed.error;
@@ -537,7 +585,7 @@ export class FileNativePlayPresetStore {
537
585
  return document;
538
586
  }
539
587
  try {
540
- // Validation hashes and parses every stored revision, so a library
588
+ // Validation checks the structure and hash of every stored revision, so a library
541
589
  // whose bytes were already validated is served from that result. Each
542
590
  // caller gets its own copy because callers edit what they read.
543
591
  if (this.#validated !== undefined && raw === this.#validated.raw)
@@ -597,19 +645,17 @@ export class FileNativePlayPresetStore {
597
645
  return true;
598
646
  }
599
647
  /**
600
- * One-time upgrade for ordered presets saved before the tabletop or
601
- * continuation builtins existed. An affected revision is replaced by a new revision that inserts
602
- * them; the old revision stays for frozen requests. A pending draft gets the
603
- * same insertion, because a draft that kept the old list would fail the
604
- * builtin-retention rule on save. The library is one atomically written file,
605
- * so an interrupted upgrade leaves the released bytes and reruns cleanly.
648
+ * Convert released ordered lists to sparse customizations exactly once.
649
+ * Original revisions remain immutable. Current and valid draft endpoints
650
+ * advance together in one atomic library replacement; invalid drafts retain
651
+ * their original bytes for repair. Catalog evolution never changes v3 files.
606
652
  */
607
- #migrateTabletopPromptBuiltins(document) {
653
+ #migratePromptCustomizations(document) {
608
654
  let changed = false;
609
655
  for (const stored of document.presets) {
610
656
  const current = stored.revisions[stored.currentRevision];
611
657
  if (current !== undefined) {
612
- const migrated = withCurrentPromptBuiltins(current);
658
+ const migrated = upgradeValidOrderedPromptFormat(current, this.#limits);
613
659
  if (migrated !== current) {
614
660
  const revision = revisionForFiles(migrated, this.#limits);
615
661
  stored.revisions[revision] = cloneFiles(migrated);
@@ -621,7 +667,7 @@ export class FileNativePlayPresetStore {
621
667
  ? undefined
622
668
  : stored.revisions[stored.draftRevision];
623
669
  if (draft !== undefined) {
624
- const migrated = withCurrentPromptBuiltins(draft);
670
+ const migrated = upgradeValidOrderedPromptFormat(draft, this.#limits);
625
671
  if (migrated !== draft) {
626
672
  const revision = revisionForFiles(migrated, this.#limits);
627
673
  stored.revisions[revision] = cloneFiles(migrated);
@@ -653,7 +699,7 @@ export class FileNativePlayPresetStore {
653
699
  }
654
700
  #stored(id, rawName, rawFiles) {
655
701
  const name = normalizeName(rawName);
656
- const files = cloneFiles(rawFiles);
702
+ const files = cloneFiles(upgradeValidOrderedPromptFormat(rawFiles, this.#limits));
657
703
  const revision = revisionForFiles(files, this.#limits);
658
704
  return {
659
705
  id,
@@ -784,7 +830,7 @@ export function toPlayPresetStructuredEditor(definition) {
784
830
  * public structure and never bypasses the normal codec.
785
831
  */
786
832
  export function applyPlayPresetStructuredEditor(files, input) {
787
- const next = cloneFiles(files);
833
+ const next = cloneFiles(upgradeOrderedPromptFormat(files));
788
834
  const presetSource = next["preset.yaml"];
789
835
  const callChainSource = next[input.callChainPath];
790
836
  if (presetSource === undefined || callChainSource === undefined)
@@ -805,10 +851,10 @@ export function applyPlayPresetStructuredEditor(files, input) {
805
851
  if (input.mergeConsecutiveMessages !== undefined)
806
852
  preset.set("mergeConsecutiveMessages", parseMessageMerging(input.mergeConsecutiveMessages));
807
853
  if (input.authorPrompts !== undefined)
808
- preset.set("authorPrompts", parseOrderedAuthorPrompts(input.authorPrompts));
854
+ preset.set("authorPrompts", customizePromptArrangement(parseOrderedAuthorPrompts(input.authorPrompts), builtinAuthorPrompts("en"), false));
809
855
  if (input.playPrompts !== undefined) {
810
- preset.set("format", "narraeon.play-preset/v2");
811
- preset.set("playPrompts", parseOrderedPlayPrompts(input.playPrompts));
856
+ preset.set("format", "narraeon.play-preset/v3");
857
+ preset.set("playPrompts", customizePromptArrangement(parseOrderedPlayPrompts(input.playPrompts), builtinPlayPrompts("en")));
812
858
  }
813
859
  preset.set("callChain", input.callChainPath);
814
860
  if (input.settingImprovementPrompt === undefined)
@@ -965,7 +1011,8 @@ export function parsePlayPresetFiles(files, limits = {}) {
965
1011
  if (callChainPath !== "call-chain.yaml")
966
1012
  invalid("call_chain_path_invalid", "Entry callChain must be call-chain.yaml", "preset.yaml");
967
1013
  if (preset.format !== "narraeon.play-preset/v1" &&
968
- preset.format !== "narraeon.play-preset/v2")
1014
+ preset.format !== "narraeon.play-preset/v2" &&
1015
+ preset.format !== "narraeon.play-preset/v3")
969
1016
  invalid("preset_format_invalid", "preset.yaml format must be narraeon.play-preset/v1", "preset.yaml");
970
1017
  if (preset.format === "narraeon.play-preset/v1" &&
971
1018
  (preset.playPrompts !== undefined ||
@@ -1009,12 +1056,21 @@ export function parsePlayPresetFiles(files, limits = {}) {
1009
1056
  : {
1010
1057
  mergeConsecutiveMessages: parseMessageMerging(preset.mergeConsecutiveMessages),
1011
1058
  }),
1012
- ...(preset.format === "narraeon.play-preset/v2"
1013
- ? { playPrompts: parseOrderedPlayPrompts(preset.playPrompts) }
1059
+ ...(preset.format === "narraeon.play-preset/v2" ||
1060
+ preset.format === "narraeon.play-preset/v3"
1061
+ ? {
1062
+ playPrompts: preset.format === "narraeon.play-preset/v3"
1063
+ ? parsePromptArrangement(preset.playPrompts)
1064
+ : parseReleasedOrderedPrompts(preset.playPrompts),
1065
+ }
1014
1066
  : {}),
1015
1067
  ...(preset.authorPrompts === undefined
1016
1068
  ? {}
1017
- : { authorPrompts: parseOrderedAuthorPrompts(preset.authorPrompts) }),
1069
+ : {
1070
+ authorPrompts: preset.format === "narraeon.play-preset/v3"
1071
+ ? parsePromptArrangement(preset.authorPrompts, builtinAuthorPrompts("en"), false)
1072
+ : parseReleasedOrderedPrompts(preset.authorPrompts, builtinAuthorPrompts("en"), false),
1073
+ }),
1018
1074
  name: name.trim(),
1019
1075
  callChainPath,
1020
1076
  ...(settingImprovementPrompt === undefined
@@ -1819,6 +1875,13 @@ function toFileMap(input, limits) {
1819
1875
  validateFileTree(files, limits);
1820
1876
  return files;
1821
1877
  }
1878
+ function upgradeValidOrderedPromptFormat(files, limits) {
1879
+ const upgraded = upgradeOrderedPromptFormat(files);
1880
+ return upgraded !== files &&
1881
+ validatePlayPresetFiles(upgraded, limits).status === "valid"
1882
+ ? upgraded
1883
+ : files;
1884
+ }
1822
1885
  function parseStoredDocument(value, limits = defaultLimits) {
1823
1886
  if (!isRecord(value) ||
1824
1887
  !hasExactKeys(value, ["schemaVersion", "currentPresetId", "presets"]) ||
@@ -1875,14 +1938,6 @@ function parseStoredDocument(value, limits = defaultLimits) {
1875
1938
  }
1876
1939
  if (expectedRevision !== revision)
1877
1940
  throw new FileNativePlayPresetError("store_invalid", "Play-preset revision content hash does not match");
1878
- // A revision saved by an earlier release may still name builtins that
1879
- // the catalog has since replaced; it counts as current-format when the
1880
- // load-time migration makes it so, and its bytes stay as written.
1881
- if (revision !== raw.draftRevision &&
1882
- validatePlayPresetFiles(clone, limits).status !== "valid" &&
1883
- validatePlayPresetFiles(withCurrentPromptBuiltins(clone), limits)
1884
- .status !== "valid")
1885
- throw new FileNativePlayPresetError("store_invalid", "Play-preset revision does not match the current format");
1886
1941
  revisions[revision] = clone;
1887
1942
  }
1888
1943
  if (revisions[raw.currentRevision] === undefined)
@@ -1919,6 +1974,18 @@ function findStored(document, id) {
1919
1974
  throw new FileNativePlayPresetError("not_found", "Requested play preset was not found");
1920
1975
  return preset;
1921
1976
  }
1977
+ function assertReadonlyPromptsRetained(previous, candidate) {
1978
+ for (const entry of previous) {
1979
+ if (entry.kind !== "builtin" && entry.kind !== "world")
1980
+ continue;
1981
+ const retained = candidate.find((item) => item.id === entry.id);
1982
+ if (retained?.kind !== entry.kind ||
1983
+ (entry.kind === "builtin" &&
1984
+ retained.kind === "builtin" &&
1985
+ retained.builtin !== entry.builtin))
1986
+ throw new FileNativePlayPresetError("readonly_prompt_removed", "System prompts and the world placeholder cannot be deleted or replaced; disable optional prompts or clone them instead");
1987
+ }
1988
+ }
1922
1989
  function normalizeName(raw) {
1923
1990
  const name = raw.trim();
1924
1991
  if (!namePattern.test(name) || name.includes(String.fromCharCode(0)))
@@ -1959,13 +2026,18 @@ export function legacyAuthorPrompts(definition) {
1959
2026
  const entries = defaultOrderedAuthorPrompts();
1960
2027
  const path = definition.settingImprovementPrompt?.path;
1961
2028
  if (path !== undefined)
1962
- entries[1] = {
2029
+ entries.splice(1, 1, {
2030
+ id: "author.guidance",
2031
+ kind: "builtin",
2032
+ builtin: "author.guidance",
2033
+ enabled: false,
2034
+ }, {
1963
2035
  id: "legacy-author",
1964
2036
  kind: "user",
1965
2037
  name: "Authoring",
1966
2038
  body: definition.files[path],
1967
2039
  enabled: true,
1968
- };
2040
+ });
1969
2041
  return entries;
1970
2042
  }
1971
2043
  function parseMessageMerging(value) {