narraeon 0.5.5 → 0.5.7
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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/node/protocol/appPreferences.js +4 -2
- package/dist/node/protocol/playFailure.js +13 -0
- package/dist/node/protocol/protagonistTemplates.js +13 -0
- package/dist/node/protocol/v1.js +25 -19
- package/dist/node/protocol/worldArchives.js +7 -0
- package/dist/node/runtime/V1Runtime.js +48 -6
- package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
- package/dist/node/runtime/archive/WorldArchives.js +389 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
- package/dist/node/runtime/content/ContentWorkspace.js +5 -0
- package/dist/node/runtime/history/HistoryBackground.js +84 -0
- package/dist/node/runtime/history/HistoryQuery.js +7 -2
- package/dist/node/runtime/history/HistoryRendering.js +31 -4
- package/dist/node/runtime/history/StoryExport.js +33 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
- package/dist/node/runtime/model/ModelHost.js +12 -1
- package/dist/node/runtime/model/ModelHostStream.js +11 -3
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +2 -0
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +5 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +91 -4
- package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
- package/dist/node/runtime/play/PlayCallChain.js +212 -61
- package/dist/node/runtime/play/PlayCallChainError.js +3 -1
- package/dist/node/runtime/play/PlayContinuity.js +3 -20
- package/dist/node/runtime/play/PlayFailure.js +20 -0
- package/dist/node/runtime/play/PlayTraceRetention.js +10 -11
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +12 -59
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +2 -2
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +7 -4
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +11 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +250 -55
- package/dist/node/runtime/world/WorldActivity.js +143 -0
- package/dist/node/runtime/world/WorldYamlRendering.js +28 -0
- package/dist/node/server/createServer.js +48 -0
- package/dist/node/shared/default-preset-host.js +18 -8
- package/dist/node/shared/default-setting-improvement-prompt.js +4 -0
- package/dist/node/shared/history-use-guidance.js +2 -2
- package/dist/node/shared/ordered-play-prompts.js +2 -2
- package/dist/node/shared/sample-packages/index.js +10 -0
- package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
- package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
- package/dist/web/assets/index-DxNtxzLR.js +1255 -0
- package/dist/web/assets/{index-CVN48xrw.css → index-DzOnu9uv.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/dist/web/assets/index-DTFZQan4.js +0 -1223
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parseDocument } from "yaml";
|
|
2
|
+
import { renderWorldYamlSource } from "../world/WorldYamlRendering.js";
|
|
3
|
+
export function captureNarrativeOrigin(snapshot, firstEventId) {
|
|
4
|
+
const source = snapshot.files.find(({ path }) => path === "control/frame.yaml")?.contents;
|
|
5
|
+
if (source === undefined)
|
|
6
|
+
throw new Error("Current situation frame is missing");
|
|
7
|
+
const parsed = parseDocument(source, { schema: "core", uniqueKeys: true });
|
|
8
|
+
if (parsed.errors.length > 0)
|
|
9
|
+
throw new Error("Current situation frame is invalid");
|
|
10
|
+
const frame = parsed.toJS({ maxAliasCount: 0 });
|
|
11
|
+
const binding = record(frame) && record(frame.bindings)
|
|
12
|
+
? frame.bindings.currentSituation
|
|
13
|
+
: undefined;
|
|
14
|
+
if (typeof binding !== "string" || binding.length === 0)
|
|
15
|
+
throw new Error("Current situation binding is invalid");
|
|
16
|
+
const read = snapshot.query({
|
|
17
|
+
kind: "read_document",
|
|
18
|
+
document: binding.startsWith("@")
|
|
19
|
+
? { shortRef: binding.slice(1) }
|
|
20
|
+
: { documentId: binding },
|
|
21
|
+
});
|
|
22
|
+
if (read.kind !== "read_document")
|
|
23
|
+
throw new Error("Bound current situation cannot be read");
|
|
24
|
+
const documentId = read.document.documentId;
|
|
25
|
+
return {
|
|
26
|
+
firstEventId,
|
|
27
|
+
documentId,
|
|
28
|
+
value: readBackground(snapshot, documentId),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function readBackground(snapshot, documentId) {
|
|
32
|
+
const selected = snapshot.query({
|
|
33
|
+
kind: "select_node",
|
|
34
|
+
document: { documentId },
|
|
35
|
+
locator: { yaml: ["背景"] },
|
|
36
|
+
});
|
|
37
|
+
if (selected.kind !== "select_node" || selected.node.codec !== "yaml")
|
|
38
|
+
return null;
|
|
39
|
+
const value = selected.node.value;
|
|
40
|
+
if (value === null ||
|
|
41
|
+
(typeof value === "string" && value.trim() === "") ||
|
|
42
|
+
(typeof value === "object" && Object.keys(value).length === 0))
|
|
43
|
+
return null;
|
|
44
|
+
// Quoted multiline scalars retain trailing newlines when display framing trims.
|
|
45
|
+
return renderWorldYamlSource(value, { blockQuote: false });
|
|
46
|
+
}
|
|
47
|
+
export function snapshotBackground(value) {
|
|
48
|
+
return value === null ? undefined : { kind: "snapshot", value };
|
|
49
|
+
}
|
|
50
|
+
export function narrativeBackground(origin, snapshot) {
|
|
51
|
+
// A released frozen request without an origin stays unrecorded on recovery.
|
|
52
|
+
if (origin === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const after = readBackground(snapshot, origin.documentId);
|
|
55
|
+
return origin.value === null && after === null
|
|
56
|
+
? undefined
|
|
57
|
+
: { kind: "narrative", before: origin.value, after };
|
|
58
|
+
}
|
|
59
|
+
export function isHistoryBackground(value) {
|
|
60
|
+
if (!record(value))
|
|
61
|
+
return false;
|
|
62
|
+
if (value.kind === "snapshot")
|
|
63
|
+
return Object.keys(value).length === 2 && nonempty(value.value);
|
|
64
|
+
return (value.kind === "narrative" &&
|
|
65
|
+
Object.keys(value).length === 3 &&
|
|
66
|
+
(value.before === null || nonempty(value.before)) &&
|
|
67
|
+
(value.after === null || nonempty(value.after)) &&
|
|
68
|
+
(value.before !== null || value.after !== null));
|
|
69
|
+
}
|
|
70
|
+
export function isNarrativeOrigin(value) {
|
|
71
|
+
return (record(value) &&
|
|
72
|
+
Object.keys(value).length === 3 &&
|
|
73
|
+
Number.isSafeInteger(value.firstEventId) &&
|
|
74
|
+
Number(value.firstEventId) > 0 &&
|
|
75
|
+
typeof value.documentId === "string" &&
|
|
76
|
+
value.documentId.length > 0 &&
|
|
77
|
+
(value.value === null || nonempty(value.value)));
|
|
78
|
+
}
|
|
79
|
+
function nonempty(value) {
|
|
80
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
81
|
+
}
|
|
82
|
+
function record(value) {
|
|
83
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
}
|
|
@@ -12,6 +12,9 @@ export class HistoryQuery {
|
|
|
12
12
|
const refs = inputs.map(({ id }) => historyMessageRef(id));
|
|
13
13
|
this.messages = inputs.map((message, index) => Object.freeze({
|
|
14
14
|
...message,
|
|
15
|
+
...(message.background === undefined
|
|
16
|
+
? {}
|
|
17
|
+
: { background: Object.freeze(structuredClone(message.background)) }),
|
|
15
18
|
ref: refs[index],
|
|
16
19
|
position: index + 1,
|
|
17
20
|
isOpening: message.isOpening === true,
|
|
@@ -20,11 +23,12 @@ export class HistoryQuery {
|
|
|
20
23
|
length: Array.from(message.text).length,
|
|
21
24
|
}));
|
|
22
25
|
this.snapshotId = createHash("sha256")
|
|
23
|
-
.update(JSON.stringify(inputs.map(({ id, role, isOpening, text }) => [
|
|
26
|
+
.update(JSON.stringify(inputs.map(({ id, role, isOpening, text, background }) => [
|
|
24
27
|
id,
|
|
25
28
|
role,
|
|
26
29
|
isOpening === true,
|
|
27
30
|
text,
|
|
31
|
+
background ?? null,
|
|
28
32
|
])))
|
|
29
33
|
.digest("hex");
|
|
30
34
|
}
|
|
@@ -380,11 +384,12 @@ export function historyMessageRef(id) {
|
|
|
380
384
|
}
|
|
381
385
|
/** Decode storage identity once at the boundary; never infer order from it. */
|
|
382
386
|
export function historyInputs(entries) {
|
|
383
|
-
return entries.map(({ path, contents }) => {
|
|
387
|
+
return entries.map(({ path, contents, background }) => {
|
|
384
388
|
const id = path.replace(/\.md$/u, "");
|
|
385
389
|
return {
|
|
386
390
|
id,
|
|
387
391
|
text: contents,
|
|
392
|
+
...(background === undefined ? {} : { background }),
|
|
388
393
|
role: /(?:\.|-)player(?:-|$)/u.test(id) ? "player" : "narrator",
|
|
389
394
|
isOpening: id.endsWith("message.genesis.narrator"),
|
|
390
395
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
1
3
|
/** Shared metadata keeps injected refs and tool results directly navigable. */
|
|
2
4
|
export function historyMessageLabel(message, locale) {
|
|
3
5
|
const zh = locale === "zh-CN";
|
|
@@ -12,7 +14,7 @@ export function historyRoleLabel(message, locale) {
|
|
|
12
14
|
return locale === "zh-CN" ? "主持叙事" : "Host narrative";
|
|
13
15
|
}
|
|
14
16
|
export function renderHistoryMessage(message, locale) {
|
|
15
|
-
return `## ${historyMessageLabel(message, locale)}\n\n${locale === "zh-CN" ? "[原文开始]" : "[Original text begins]"}\n${message.text}\n${locale === "zh-CN" ? "[原文结束;正文完整]" : "[Original text ends; body complete]"}`;
|
|
17
|
+
return `## ${historyMessageLabel(message, locale)}\n${renderHistoryBackground(message.background, locale)}\n${locale === "zh-CN" ? "[原文开始]" : "[Original text begins]"}\n${message.text}\n${locale === "zh-CN" ? "[原文结束;正文完整]" : "[Original text ends; body complete]"}`;
|
|
16
18
|
}
|
|
17
19
|
export function renderHistoryInjectionNotice(total, selected, kind, locale) {
|
|
18
20
|
const zh = locale === "zh-CN";
|
|
@@ -51,7 +53,7 @@ export function renderHistoryResult(result, locale = "en", legacyRead = false) {
|
|
|
51
53
|
if (result.kind === "legacy_list")
|
|
52
54
|
return {
|
|
53
55
|
ok: true,
|
|
54
|
-
markdown: `# Directory listing\n\n${header}\nOrder: ${result.order}\n${result.messages.map((message) => `- ${message.ref}, position ${message.position}, role=${message.role}${message.isOpening ? ", isOpening=true" : ""}, ${message.length} Unicode code points`).join("\n") || "(empty)"}\n\n---\nThis page: ${result.offset}..${result.offset + result.messages.length} / ${result.total} items\nComplete: ${result.nextCursor === null ? "yes" : "no"}${result.nextCursor === null ? "" : `\nNext-page cursor: ${result.nextCursor}`}`,
|
|
56
|
+
markdown: `# Directory listing\n\n${header}\nOrder: ${result.order}\n${result.messages.map((message) => `- ${message.ref}, position ${message.position}, role=${message.role}${message.isOpening ? ", isOpening=true" : ""}, ${message.length} Unicode code points${message.background === undefined ? "" : `\n${renderHistoryBackground(message.background, locale)}`}`).join("\n") || "(empty)"}\n\n---\nThis page: ${result.offset}..${result.offset + result.messages.length} / ${result.total} items\nComplete: ${result.nextCursor === null ? "yes" : "no"}${result.nextCursor === null ? "" : `\nNext-page cursor: ${result.nextCursor}`}`,
|
|
55
57
|
};
|
|
56
58
|
if (result.kind === "list")
|
|
57
59
|
return {
|
|
@@ -59,7 +61,7 @@ export function renderHistoryResult(result, locale = "en", legacyRead = false) {
|
|
|
59
61
|
markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${header}\nOrder: oldest_first (${zh ? "页内始终由旧到新" : "every page oldest to newest"})\n${result.messages
|
|
60
62
|
.map((message) => {
|
|
61
63
|
const preview = Array.from(message.text).slice(0, 120).join("");
|
|
62
|
-
return `### ${historyMessageLabel(message, locale)}\n${message.length} Unicode code points\n[${zh ? "部分原文预览" : "Partial original preview"}]\n${preview}\n[${message.length > 120 ? (zh ? "预览有省略" : "preview omitted remaining text") : zh ? "预览结束" : "preview ends"}]`;
|
|
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"}]`;
|
|
63
65
|
})
|
|
64
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}"}` : ""}`,
|
|
65
67
|
};
|
|
@@ -70,7 +72,7 @@ export function renderHistoryResult(result, locale = "en", legacyRead = false) {
|
|
|
70
72
|
};
|
|
71
73
|
return {
|
|
72
74
|
ok: true,
|
|
73
|
-
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${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."}`,
|
|
75
|
+
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."}`,
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
function localizedRecovery(result, zh) {
|
|
@@ -84,3 +86,28 @@ function localizedRecovery(result, zh) {
|
|
|
84
86
|
return "该 ref 不在当前时间线中,可能仍保存在旧端点;请重新查询当前历史。";
|
|
85
87
|
return `参数不符合要求,请按工具定义修正。${result.recovery}`;
|
|
86
88
|
}
|
|
89
|
+
/** Every result carries its own annotation; no implicit neighboring inheritance. */
|
|
90
|
+
export function renderHistoryBackground(background, locale) {
|
|
91
|
+
if (background === undefined)
|
|
92
|
+
return "";
|
|
93
|
+
const zh = locale === "zh-CN";
|
|
94
|
+
const node = (value) => value === null
|
|
95
|
+
? zh
|
|
96
|
+
? "未记录"
|
|
97
|
+
: "not recorded"
|
|
98
|
+
: `\n[${zh ? "背景值开始" : "Background value begins"}]\n${value}\n[${zh ? "背景值结束" : "Background value ends"}]`;
|
|
99
|
+
const notice = zh
|
|
100
|
+
? "历史背景仅说明采集位置的状态,不是当前世界状态;回忆、梦境或段内局部时间结合原文解释,不认证事实或授予角色知识。"
|
|
101
|
+
: "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.";
|
|
102
|
+
if (background.kind === "snapshot" ||
|
|
103
|
+
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`;
|
|
106
|
+
}
|
|
107
|
+
function equalBackground(before, after) {
|
|
108
|
+
if (before === after)
|
|
109
|
+
return true;
|
|
110
|
+
if (before === null || after === null)
|
|
111
|
+
return false;
|
|
112
|
+
return isDeepStrictEqual(parseDocument(before).toJS({ maxAliasCount: 0 }), parseDocument(after).toJS({ maxAliasCount: 0 }));
|
|
113
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function exportStory(title, messages, format, locale) {
|
|
2
|
+
const player = locale === "zh-CN" ? "我" : "Me";
|
|
3
|
+
const separator = locale === "zh-CN" ? ":" : ": ";
|
|
4
|
+
const blocks = messages.map(({ role, text }) => {
|
|
5
|
+
if (role === "narrator")
|
|
6
|
+
return text;
|
|
7
|
+
if (format === "text")
|
|
8
|
+
return `${player}${separator}${text}`;
|
|
9
|
+
return text
|
|
10
|
+
.split("\n")
|
|
11
|
+
.map((line, index) => index === 0 ? `> **${player}**${separator}${line}` : `> ${line}`)
|
|
12
|
+
.join("\n");
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
fileName: storyFileName(title, format),
|
|
16
|
+
content: [format === "markdown" ? `# ${title}` : title, ...blocks].join("\n\n"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function storyFileName(title, format) {
|
|
20
|
+
const extension = format === "markdown" ? ".md" : ".txt";
|
|
21
|
+
let stem = title
|
|
22
|
+
.replace(/[<>:"/\\|?*\p{Cc}]/gu, "")
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[. ]+$/u, "");
|
|
25
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(stem))
|
|
26
|
+
stem = `_${stem}`;
|
|
27
|
+
const characters = Array.from(stem).slice(0, 80 - extension.length);
|
|
28
|
+
// Also fit filesystems that limit a filename to 255 UTF-8 bytes.
|
|
29
|
+
while (Buffer.byteLength(characters.join("") + extension, "utf8") > 255)
|
|
30
|
+
characters.pop();
|
|
31
|
+
stem = characters.join("").replace(/[. ]+$/u, "");
|
|
32
|
+
return `${stem || "story"}${extension}`;
|
|
33
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
var _a;
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
|
|
3
4
|
import { dirname } from "node:path";
|
|
4
5
|
import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, } from "../../protocol/modelConnections.js";
|
|
5
|
-
import { ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
6
|
+
import { assertModelOutputComplete, ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
6
7
|
import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
|
|
7
8
|
import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
|
|
8
9
|
import { defaultRuntimeToolDefinitionStrategy, portableRuntimeToolInputSchema, } from "../prompt/FileNativeToolRegistry.js";
|
|
@@ -77,7 +78,31 @@ export class FileNativeModelHost {
|
|
|
77
78
|
.digest("hex"),
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
freezeRequest(request) {
|
|
82
|
+
const frozen = structuredClone(request);
|
|
83
|
+
if (frozen.modelConfig === undefined) {
|
|
84
|
+
const { baseUrl: _baseUrl, apiKey: _apiKey, ...config } = this.#connection;
|
|
85
|
+
void _baseUrl;
|
|
86
|
+
void _apiKey;
|
|
87
|
+
frozen.modelConfig = config;
|
|
88
|
+
}
|
|
89
|
+
return frozen;
|
|
90
|
+
}
|
|
91
|
+
#requestHost(request) {
|
|
92
|
+
if (request.modelConfig === undefined)
|
|
93
|
+
return this;
|
|
94
|
+
if (request.modelConfig.provider !== this.#connection.provider)
|
|
95
|
+
throw new ModelHostBindingMismatchError("The request protocol does not match the selected model host");
|
|
96
|
+
return new _a({
|
|
97
|
+
...request.modelConfig,
|
|
98
|
+
baseUrl: this.#connection.baseUrl,
|
|
99
|
+
apiKey: this.#connection.apiKey,
|
|
100
|
+
}, this.#fetch, this.#failureLog);
|
|
101
|
+
}
|
|
102
|
+
exchange(request, observer) {
|
|
103
|
+
return this.#requestHost(request).#exchange(request, observer);
|
|
104
|
+
}
|
|
105
|
+
async #exchange(request, observer) {
|
|
81
106
|
assertExchangeCompatibility(this.#connection, request);
|
|
82
107
|
await trace("model_host_exchange", modelHostTrace(request));
|
|
83
108
|
const signal = observer?.signal;
|
|
@@ -99,6 +124,9 @@ export class FileNativeModelHost {
|
|
|
99
124
|
}
|
|
100
125
|
/** Uses the production encoders and deliberately omits credentials. */
|
|
101
126
|
previewRequest(request) {
|
|
127
|
+
return this.#requestHost(request).#previewRequest(request);
|
|
128
|
+
}
|
|
129
|
+
#previewRequest(request) {
|
|
102
130
|
assertExchangeCompatibility(this.#connection, request);
|
|
103
131
|
const provider = this.#connection.provider;
|
|
104
132
|
const body = provider === "chat_completions"
|
|
@@ -183,6 +211,7 @@ export class FileNativeModelHost {
|
|
|
183
211
|
await trace("chat_response", payload);
|
|
184
212
|
const message = payload.choices?.[0]?.message;
|
|
185
213
|
const finishReason = payload.choices?.[0]?.finish_reason;
|
|
214
|
+
assertModelOutputComplete(finishReason);
|
|
186
215
|
if (message === undefined)
|
|
187
216
|
throw unknownProviderResponse("Chat Completions");
|
|
188
217
|
if (finishReason !== undefined &&
|
|
@@ -271,6 +300,7 @@ export class FileNativeModelHost {
|
|
|
271
300
|
if (!isRecord(rawPayload) || !Array.isArray(rawPayload.content))
|
|
272
301
|
throw unknownProviderResponse("Anthropic Messages");
|
|
273
302
|
const payload = rawPayload;
|
|
303
|
+
assertModelOutputComplete(payload.stop_reason);
|
|
274
304
|
const content = payload.content;
|
|
275
305
|
if (!validAnthropicContent(content))
|
|
276
306
|
throw unknownProviderResponse("Anthropic Messages");
|
|
@@ -310,6 +340,7 @@ export class FileNativeModelHost {
|
|
|
310
340
|
});
|
|
311
341
|
}
|
|
312
342
|
}
|
|
343
|
+
_a = FileNativeModelHost;
|
|
313
344
|
function assertExchangeCompatibility(connection, request) {
|
|
314
345
|
if (request.bootstrap.provider.protocol !== connection.provider)
|
|
315
346
|
throw new ModelHostBindingMismatchError("The compiled prompt protocol does not match the selected model host");
|
|
@@ -1048,6 +1079,7 @@ async function openAIResponsesRequest(input) {
|
|
|
1048
1079
|
await trace(`${input.tracePrefix}_response`, payload);
|
|
1049
1080
|
if (!isRecord(payload) || !Array.isArray(payload.output))
|
|
1050
1081
|
throw unknownProviderResponse("Responses API");
|
|
1082
|
+
assertModelOutputComplete(responsesStopReason(payload));
|
|
1051
1083
|
const output = cloneResponseOutput(payload.output);
|
|
1052
1084
|
if (!validResponsesOutput(output))
|
|
1053
1085
|
throw unknownProviderResponse("Responses API");
|
|
@@ -33,10 +33,21 @@ export class ModelHostFailureError extends Error {
|
|
|
33
33
|
this.details = options.details;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/** Reject provider-confirmed truncation before accepting text or tool calls. */
|
|
37
|
+
export function assertModelOutputComplete(stopReason) {
|
|
38
|
+
if (stopReason === "length" ||
|
|
39
|
+
stopReason === "max_tokens" ||
|
|
40
|
+
stopReason === "max_output_tokens" ||
|
|
41
|
+
stopReason === "incomplete:max_output_tokens") {
|
|
42
|
+
throw new ModelHostFailureError("The model response reached the output token limit before completion. Increase maxOutputTokens and start a fresh context; reasoning may also consume the output budget.", { details: { code: "model_output_limit", stopReason } });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Replaying the frozen request cannot recover a context or output limit. */
|
|
37
46
|
export function modelHostFailureRequiresFreshContext(error) {
|
|
38
47
|
if (!(error instanceof ModelHostFailureError))
|
|
39
48
|
return false;
|
|
49
|
+
if (failureDetailStrings(error.details).includes("model_output_limit"))
|
|
50
|
+
return true;
|
|
40
51
|
const evidence = [error.message, ...failureDetailStrings(error.details)]
|
|
41
52
|
.join("\n")
|
|
42
53
|
.toLocaleLowerCase("und");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelHostFailureError } from "./ModelHost.js";
|
|
1
|
+
import { assertModelOutputComplete, ModelHostFailureError, } from "./ModelHost.js";
|
|
2
2
|
import { providerStreamEvents } from "./ProviderStream.js";
|
|
3
3
|
export async function aggregateChatModelStream(body, onDelta) {
|
|
4
4
|
let content = "";
|
|
@@ -110,6 +110,7 @@ export async function aggregateChatModelStream(body, onDelta) {
|
|
|
110
110
|
}
|
|
111
111
|
if (!completed || !sawPayload)
|
|
112
112
|
throw new Error("Chat SSE ended before a complete response");
|
|
113
|
+
assertModelOutputComplete(stopReason);
|
|
113
114
|
const toolCalls = [...calls.entries()]
|
|
114
115
|
.sort(([left], [right]) => left - right)
|
|
115
116
|
.map(([, call]) => {
|
|
@@ -259,8 +260,6 @@ export async function aggregateAnthropicModelStream(body, onDelta) {
|
|
|
259
260
|
if (block === undefined || block.stopped)
|
|
260
261
|
throw new Error("Anthropic SSE content block stop is invalid");
|
|
261
262
|
block.stopped = true;
|
|
262
|
-
if (block.value.type === "tool_use" && block.partialJson !== "")
|
|
263
|
-
block.value.input = parseStreamJson(block.partialJson);
|
|
264
263
|
break;
|
|
265
264
|
}
|
|
266
265
|
case "message_delta": {
|
|
@@ -289,11 +288,14 @@ export async function aggregateAnthropicModelStream(body, onDelta) {
|
|
|
289
288
|
}
|
|
290
289
|
if (!started || !stopped)
|
|
291
290
|
throw new Error("Anthropic SSE ended before a complete response");
|
|
291
|
+
assertModelOutputComplete(stopReason);
|
|
292
292
|
const content = [...blocks.entries()]
|
|
293
293
|
.sort(([left], [right]) => left - right)
|
|
294
294
|
.map(([, block]) => {
|
|
295
295
|
if (!block.stopped)
|
|
296
296
|
throw new Error("Anthropic SSE content block did not end");
|
|
297
|
+
if (block.value.type === "tool_use" && block.partialJson !== "")
|
|
298
|
+
block.value.input = parseStreamJson(block.partialJson);
|
|
297
299
|
return block.value;
|
|
298
300
|
});
|
|
299
301
|
const text = content
|
|
@@ -364,6 +366,12 @@ export async function aggregateResponsesModelStream(body, onDelta) {
|
|
|
364
366
|
payload.type === "response.incomplete" ||
|
|
365
367
|
payload.type === "response.cancelled" ||
|
|
366
368
|
payload.type === "error") {
|
|
369
|
+
if (payload.type === "response.incomplete" &&
|
|
370
|
+
isRecord(payload.response)) {
|
|
371
|
+
const details = payload.response.incomplete_details;
|
|
372
|
+
if (isRecord(details))
|
|
373
|
+
assertModelOutputComplete(details.reason);
|
|
374
|
+
}
|
|
367
375
|
throw new ModelHostFailureError("Responses SSE returned an explicit failure");
|
|
368
376
|
}
|
|
369
377
|
}
|
|
@@ -83,6 +83,7 @@ export class FileNativeContinuityCorrection {
|
|
|
83
83
|
materials: structuredClone(binding.additionalMaterials),
|
|
84
84
|
originalMaterials: structuredClone(binding.additionalMaterials),
|
|
85
85
|
history: structuredClone(binding.history),
|
|
86
|
+
historyBackgrounds: structuredClone(binding.historyBackgrounds ?? {}),
|
|
86
87
|
reads: new Map(),
|
|
87
88
|
previewedVersion: null,
|
|
88
89
|
stateOperationClaim: claimed.handle,
|
|
@@ -236,6 +237,7 @@ export class FileNativeContinuityCorrection {
|
|
|
236
237
|
documentSnapshot: candidate.snapshot,
|
|
237
238
|
additionalMaterials: materials,
|
|
238
239
|
history: candidate.history,
|
|
240
|
+
historyBackgrounds: candidate.historyBackgrounds,
|
|
239
241
|
documentMaintenance: candidate.documentMaintenance,
|
|
240
242
|
documentMaintenanceUnavailableReason: candidate.documentMaintenanceUnavailableReason,
|
|
241
243
|
narrativeCheckpoint: candidate.narrativeCheckpoint,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isNarrativeOrigin, isHistoryBackground, } from "../history/HistoryBackground.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -119,6 +120,8 @@ function assertBase(value) {
|
|
|
119
120
|
value.schemaVersion !== 1 ||
|
|
120
121
|
value.kind !== "play_advance" ||
|
|
121
122
|
(value.advanceKind !== "player" && value.advanceKind !== "response") ||
|
|
123
|
+
(value.narrativeOrigin !== undefined &&
|
|
124
|
+
!isNarrativeOrigin(value.narrativeOrigin)) ||
|
|
122
125
|
typeof value.worldId !== "string" ||
|
|
123
126
|
typeof value.chainId !== "string" ||
|
|
124
127
|
typeof value.advanceId !== "string" ||
|
|
@@ -170,6 +173,8 @@ function assertSettlementPrepared(value) {
|
|
|
170
173
|
(value.schemaVersion !== 1 ||
|
|
171
174
|
value.stage !== "settlement_prepared" ||
|
|
172
175
|
!isRecord(value.settlement) ||
|
|
176
|
+
(value.settlement.background !== undefined &&
|
|
177
|
+
!isHistoryBackground(value.settlement.background)) ||
|
|
173
178
|
typeof value.recordedAt !== "number"))
|
|
174
179
|
throw new Error("Settlement-prepared play fact has an invalid shape");
|
|
175
180
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { legacyModelBindingFailure, projectLegacyModelBindingFailure, } from "./LegacyModelBindingFailure.js";
|
|
2
|
+
import { isPlayFailureKind, } from "../../protocol/playFailure.js";
|
|
3
|
+
import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
|
|
1
4
|
import { encodePlayPromptRun, decodePlayPromptRun, } from "./PlayPromptRun.js";
|
|
2
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
6
|
import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
@@ -70,7 +73,8 @@ export class FileNativePlayTimelineStore {
|
|
|
70
73
|
const eventCount = prefixLength(input.source.events, input.target.events);
|
|
71
74
|
const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
|
|
72
75
|
const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
|
|
73
|
-
if (input.
|
|
76
|
+
if (input.retainCompleteContext ||
|
|
77
|
+
input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
|
|
74
78
|
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
|
|
75
79
|
if (encoding !== null)
|
|
76
80
|
await publishImmutableJson(join(targetRoot, "initial-request-encoding.json"), encoding);
|
|
@@ -162,6 +166,9 @@ export class FileNativePlayTimelineStore {
|
|
|
162
166
|
chainId: value.chainId,
|
|
163
167
|
nextMaterials: structuredClone(value.nextMaterials),
|
|
164
168
|
lastRequestDigest,
|
|
169
|
+
...(value.narrativeOrigin === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { narrativeOrigin: structuredClone(value.narrativeOrigin) }),
|
|
165
172
|
};
|
|
166
173
|
const continuationDigest = createHash("sha256")
|
|
167
174
|
.update(JSON.stringify(continuation))
|
|
@@ -203,6 +210,9 @@ export class FileNativePlayTimelineStore {
|
|
|
203
210
|
continuationDigest,
|
|
204
211
|
lastRequestAttempt: value.lastRequestAttempt,
|
|
205
212
|
lastFailure: value.lastFailure,
|
|
213
|
+
...(value.lastFailureKind === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { lastFailureKind: value.lastFailureKind }),
|
|
206
216
|
updatedAt: value.updatedAt,
|
|
207
217
|
};
|
|
208
218
|
await publishJson(join(root, "state.json"), state);
|
|
@@ -231,6 +241,20 @@ export class FileNativePlayTimelineStore {
|
|
|
231
241
|
throw new Error("Play timeline head generation is inconsistent");
|
|
232
242
|
return loaded;
|
|
233
243
|
}
|
|
244
|
+
/** Read only the small active context index, never its transcript or bootstrap. */
|
|
245
|
+
async currentContextRequiresFreshContext(worldId) {
|
|
246
|
+
assertIdentity(worldId, "World ID");
|
|
247
|
+
const head = await this.#readTimelineHead(worldId);
|
|
248
|
+
if (head === null)
|
|
249
|
+
return false;
|
|
250
|
+
const index = await readJson(join(this.#contextRoot(worldId, head.chainId), "index.json"));
|
|
251
|
+
assertContextIndex(index);
|
|
252
|
+
if (index.worldId !== worldId ||
|
|
253
|
+
index.chainId !== head.chainId ||
|
|
254
|
+
index.timelineGeneration !== head.generation)
|
|
255
|
+
throw new Error("Play context durable identity is inconsistent");
|
|
256
|
+
return index.importedFromArchive === true;
|
|
257
|
+
}
|
|
234
258
|
async readContext(worldId, chainId) {
|
|
235
259
|
assertIdentity(worldId, "World ID");
|
|
236
260
|
assertIdentity(chainId, "Call-chain ID");
|
|
@@ -273,6 +297,7 @@ export class FileNativePlayTimelineStore {
|
|
|
273
297
|
chainId,
|
|
274
298
|
previousChainId: indexValue.previousChainId,
|
|
275
299
|
timelineGeneration: indexValue.timelineGeneration,
|
|
300
|
+
...(indexValue.importedFromArchive ? { importedFromArchive: true } : {}),
|
|
276
301
|
...(baseValue.continuityContextId === undefined
|
|
277
302
|
? {}
|
|
278
303
|
: { continuityContextId: baseValue.continuityContextId }),
|
|
@@ -313,11 +338,19 @@ export class FileNativePlayTimelineStore {
|
|
|
313
338
|
promptRuns,
|
|
314
339
|
changedDocuments: structuredClone(stateValue.changedDocuments),
|
|
315
340
|
nextMaterials: structuredClone(continuationValue.nextMaterials),
|
|
341
|
+
...(continuationValue.narrativeOrigin === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: {
|
|
344
|
+
narrativeOrigin: structuredClone(continuationValue.narrativeOrigin),
|
|
345
|
+
}),
|
|
316
346
|
nextEventId: stateValue.nextEventId,
|
|
317
347
|
exchange: stateValue.exchange,
|
|
318
348
|
lastRequest,
|
|
319
349
|
lastRequestAttempt: stateValue.lastRequestAttempt,
|
|
320
350
|
lastFailure: stateValue.lastFailure,
|
|
351
|
+
...(stateValue.lastFailureKind === undefined
|
|
352
|
+
? {}
|
|
353
|
+
: { lastFailureKind: stateValue.lastFailureKind }),
|
|
321
354
|
updatedAt: stateValue.updatedAt,
|
|
322
355
|
};
|
|
323
356
|
return {
|
|
@@ -436,7 +469,10 @@ export class FileNativePlayTimelineStore {
|
|
|
436
469
|
kind: "event",
|
|
437
470
|
chainId: pointer.chainId,
|
|
438
471
|
current: pointer.chainId === head?.chainId,
|
|
439
|
-
|
|
472
|
+
...(context.index.importedFromArchive
|
|
473
|
+
? { requiresFreshContext: true }
|
|
474
|
+
: {}),
|
|
475
|
+
event: projectLegacyModelBindingFailure(summary),
|
|
440
476
|
});
|
|
441
477
|
pointer = { ...pointer, eventId: pointer.eventId - 1 };
|
|
442
478
|
continue;
|
|
@@ -489,6 +525,9 @@ export class FileNativePlayTimelineStore {
|
|
|
489
525
|
activeStatus: active?.state.status ?? null,
|
|
490
526
|
activeCanRetry: active?.state.canRetry ?? false,
|
|
491
527
|
activeLastFailure: active?.state.lastFailure ?? null,
|
|
528
|
+
...(active?.state.lastFailureKind === undefined
|
|
529
|
+
? {}
|
|
530
|
+
: { activeLastFailureKind: active.state.lastFailureKind }),
|
|
492
531
|
items: reverse.reverse(),
|
|
493
532
|
nextCursor: pointer.chainId === null && pointer.genesisIndex < 0
|
|
494
533
|
? null
|
|
@@ -507,7 +546,7 @@ export class FileNativePlayTimelineStore {
|
|
|
507
546
|
assertPlayEvent(event);
|
|
508
547
|
if (event.id !== eventId)
|
|
509
548
|
throw new Error("Play timeline event identity is inconsistent");
|
|
510
|
-
return normalizePlayEvent(event);
|
|
549
|
+
return projectLegacyModelBindingFailure(normalizePlayEvent(event));
|
|
511
550
|
}
|
|
512
551
|
newGeneration() {
|
|
513
552
|
return `timeline:${randomUUID()}`;
|
|
@@ -588,11 +627,48 @@ export class FileNativePlayTimelineStore {
|
|
|
588
627
|
indexValue.chainId !== chainId ||
|
|
589
628
|
stateValue.chainId !== chainId)
|
|
590
629
|
throw new Error("Play context durable identity is inconsistent");
|
|
630
|
+
// Old versions persisted a configuration preflight failure as a terminal
|
|
631
|
+
// context failure. Recover only the disposition proven by its event prefix.
|
|
632
|
+
// Archive contexts and genuine provider failures retain their restrictions.
|
|
633
|
+
if (!indexValue.importedFromArchive &&
|
|
634
|
+
stateValue.status === "interrupted" &&
|
|
635
|
+
stateValue.lastFailure === legacyModelBindingFailure &&
|
|
636
|
+
(stateValue.lastFailureKind === undefined ||
|
|
637
|
+
stateValue.lastFailureKind === "context_unavailable")) {
|
|
638
|
+
for (let id = stateValue.eventCount; id > 0; id -= 1) {
|
|
639
|
+
const event = await readJson(join(root, "events", numbered(id)));
|
|
640
|
+
assertPlayEvent(event);
|
|
641
|
+
if (event.kind === "followup" ||
|
|
642
|
+
(event.kind === "failure" &&
|
|
643
|
+
event.message === legacyModelBindingFailure))
|
|
644
|
+
continue;
|
|
645
|
+
if (event.kind === "assistant" &&
|
|
646
|
+
event.status === "completed" &&
|
|
647
|
+
event.responseKind === "narrative" &&
|
|
648
|
+
event.continuation === "available" &&
|
|
649
|
+
event.committedHead === stateValue.parentHead) {
|
|
650
|
+
stateValue.status = "ready";
|
|
651
|
+
stateValue.canRetry = false;
|
|
652
|
+
stateValue.lastFailure = null;
|
|
653
|
+
delete stateValue.lastFailureKind;
|
|
654
|
+
}
|
|
655
|
+
else if (event.kind === "failure") {
|
|
656
|
+
stateValue.canRetry = event.failureKind === "retryable";
|
|
657
|
+
stateValue.lastFailure = event.message;
|
|
658
|
+
if (event.failureKind === undefined)
|
|
659
|
+
delete stateValue.lastFailureKind;
|
|
660
|
+
else
|
|
661
|
+
stateValue.lastFailureKind = event.failureKind;
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
591
666
|
return { root, index: indexValue, state: stateValue };
|
|
592
667
|
}
|
|
593
668
|
}
|
|
594
669
|
function contextIndex(value) {
|
|
595
670
|
return {
|
|
671
|
+
...(value.importedFromArchive ? { importedFromArchive: true } : {}),
|
|
596
672
|
schemaVersion: 3,
|
|
597
673
|
kind: "play_context_index",
|
|
598
674
|
worldId: value.worldId,
|
|
@@ -715,6 +791,8 @@ function assertContextIndex(value) {
|
|
|
715
791
|
if (!isRecord(value) ||
|
|
716
792
|
value.schemaVersion !== 3 ||
|
|
717
793
|
value.kind !== "play_context_index" ||
|
|
794
|
+
(value.importedFromArchive !== undefined &&
|
|
795
|
+
value.importedFromArchive !== true) ||
|
|
718
796
|
typeof value.worldId !== "string" ||
|
|
719
797
|
typeof value.chainId !== "string" ||
|
|
720
798
|
(value.previousChainId !== null &&
|
|
@@ -760,6 +838,8 @@ function assertContextState(value) {
|
|
|
760
838
|
!/^[a-f0-9]{64}$/u.test(value.continuationDigest) ||
|
|
761
839
|
!validCount(value.lastRequestAttempt) ||
|
|
762
840
|
(value.lastFailure !== null && typeof value.lastFailure !== "string") ||
|
|
841
|
+
(value.lastFailureKind !== undefined &&
|
|
842
|
+
!isPlayFailureKind(value.lastFailureKind)) ||
|
|
763
843
|
typeof value.updatedAt !== "number")
|
|
764
844
|
throw new Error("Play context state has an invalid shape");
|
|
765
845
|
}
|
|
@@ -767,6 +847,8 @@ function assertContextContinuation(value) {
|
|
|
767
847
|
if (!isRecord(value) ||
|
|
768
848
|
value.schemaVersion !== 3 ||
|
|
769
849
|
value.kind !== "play_context_continuation" ||
|
|
850
|
+
(value.narrativeOrigin !== undefined &&
|
|
851
|
+
!isNarrativeOrigin(value.narrativeOrigin)) ||
|
|
770
852
|
typeof value.chainId !== "string" ||
|
|
771
853
|
!Array.isArray(value.nextMaterials) ||
|
|
772
854
|
(value.lastRequestDigest !== null &&
|
|
@@ -828,6 +910,8 @@ function isReleasedPlayContext(value) {
|
|
|
828
910
|
(value.lastRequest !== null && !isRecord(value.lastRequest)) ||
|
|
829
911
|
!validCount(value.lastRequestAttempt) ||
|
|
830
912
|
(value.lastFailure !== null && typeof value.lastFailure !== "string") ||
|
|
913
|
+
(value.lastFailureKind !== undefined &&
|
|
914
|
+
!isPlayFailureKind(value.lastFailureKind)) ||
|
|
831
915
|
typeof value.updatedAt !== "number")
|
|
832
916
|
return false;
|
|
833
917
|
try {
|
|
@@ -965,7 +1049,10 @@ function assertPlayEvent(value) {
|
|
|
965
1049
|
return;
|
|
966
1050
|
}
|
|
967
1051
|
if ((value.kind !== "failure" && value.kind !== "cancellation") ||
|
|
968
|
-
typeof value.message !== "string"
|
|
1052
|
+
typeof value.message !== "string" ||
|
|
1053
|
+
(value.kind === "failure" &&
|
|
1054
|
+
value.failureKind !== undefined &&
|
|
1055
|
+
!isPlayFailureKind(value.failureKind)))
|
|
969
1056
|
throw new Error("Play timeline event has an invalid kind");
|
|
970
1057
|
}
|
|
971
1058
|
function assertTimelineEventSummary(value) {
|