narraeon 0.6.0 → 0.6.2
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 +11 -0
- package/README.zh-CN.md +8 -0
- package/dist/node/protocol/v1.js +12 -2
- package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
- package/dist/node/runtime/V1Runtime.js +5 -5
- package/dist/node/runtime/archive/WorldArchives.js +14 -46
- package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
- package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +25 -2
- package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
- package/dist/node/runtime/config/dataRootLock.js +108 -0
- package/dist/node/runtime/content/ContentWorkspace.js +0 -10
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
- package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
- package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
- package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
- package/dist/node/runtime/play/PlayCallChain.js +338 -366
- package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
- package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
- package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +39 -7
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +14 -6
- package/dist/node/runtime/setting/SettingImprovementSession.js +39 -25
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
- package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
- package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
- package/dist/node/runtime/world/WorldActivity.js +14 -22
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
- package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
- package/dist/node/runtime/world/WorldQueue.js +46 -0
- package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -13
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +29 -2
- package/dist/node/server/mobile.js +52 -0
- package/dist/node/server/startWebServer.js +15 -8
- package/dist/web/assets/index-CXnFaYK9.js +1323 -0
- package/dist/web/assets/{index-BJ-1Mb0e.css → index-LPMV0pl3.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
- package/dist/web/assets/index-BgBL8G-A.js +0 -1323
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { projectFileNativeHistorySurface, projectHistoryBackgrounds, } from "./FileNativeHistoryProjection.js";
|
|
5
|
+
import { appendTurn, initializeLedger, listTurns, readFileAt, readHead as readLedgerHead, readTreeAt, readTurnDigest, resolveEndpoint, } from "./WorldTurnLedger.js";
|
|
6
|
+
export class WorldAuthorityError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "WorldAuthorityError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const genesisFile = join("runtime", "genesis.json");
|
|
15
|
+
const materialsFile = join("runtime", "additional-materials.json");
|
|
16
|
+
function headName(position) {
|
|
17
|
+
return position === 0 ? "genesis" : `commit:${String(position)}`;
|
|
18
|
+
}
|
|
19
|
+
function parsePosition(head) {
|
|
20
|
+
if (head === "genesis")
|
|
21
|
+
return 0;
|
|
22
|
+
const match = /^commit:([1-9][0-9]*)$/u.exec(head);
|
|
23
|
+
if (match === null)
|
|
24
|
+
throw new WorldAuthorityError("corrupt", `端点名称不合法:${head}`);
|
|
25
|
+
return Number.parseInt(match[1], 10);
|
|
26
|
+
}
|
|
27
|
+
async function readJsonOrNull(path) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
33
|
+
return null;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isNodeError(error) {
|
|
38
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
39
|
+
}
|
|
40
|
+
export class WorldLedgerAuthority {
|
|
41
|
+
#root;
|
|
42
|
+
constructor(worldRoot) {
|
|
43
|
+
this.#root = worldRoot;
|
|
44
|
+
}
|
|
45
|
+
async exists() {
|
|
46
|
+
try {
|
|
47
|
+
await stat(join(this.#root, "runtime", "head.json"));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
52
|
+
return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async initialize() {
|
|
57
|
+
await initializeLedger(this.#root);
|
|
58
|
+
}
|
|
59
|
+
/** 当前端点:逻辑位置(回退后即被恢复的轮)及其元数据。 */
|
|
60
|
+
async readHead() {
|
|
61
|
+
const physical = await readLedgerHead(this.#root);
|
|
62
|
+
const position = await resolveEndpoint(this.#root, physical);
|
|
63
|
+
return {
|
|
64
|
+
head: headName(position),
|
|
65
|
+
sequence: position,
|
|
66
|
+
operationId: await this.#operationIdAt(position),
|
|
67
|
+
commitDigest: await readTurnDigest(this.#root, position),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async currentOperationId() {
|
|
71
|
+
return (await this.readHead()).operationId;
|
|
72
|
+
}
|
|
73
|
+
async #operationIdAt(position) {
|
|
74
|
+
if (position === 0) {
|
|
75
|
+
const genesis = await readJsonOrNull(join(this.#root, genesisFile));
|
|
76
|
+
return typeof genesis?.operationId === "string"
|
|
77
|
+
? genesis.operationId
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
return (await this.#commitAtPosition(position)).operationId;
|
|
81
|
+
}
|
|
82
|
+
async #positionOf(head) {
|
|
83
|
+
if (head === undefined)
|
|
84
|
+
return (await this.readHead()).sequence;
|
|
85
|
+
return resolveEndpoint(this.#root, parsePosition(head));
|
|
86
|
+
}
|
|
87
|
+
async #commitAtPosition(position) {
|
|
88
|
+
const turn = (await listTurns(this.#root)).find((candidate) => candidate.sequence === position);
|
|
89
|
+
const commit = turn?.payload;
|
|
90
|
+
if (commit?.sequence !== position)
|
|
91
|
+
throw new WorldAuthorityError("corrupt", `第 ${position} 轮缺少提交元数据`);
|
|
92
|
+
return commit;
|
|
93
|
+
}
|
|
94
|
+
/** 端点位置处可见的全部提交(按轮号),不含 genesis 与回退轮。 */
|
|
95
|
+
async #commitsUpTo(position) {
|
|
96
|
+
const turns = await listTurns(this.#root);
|
|
97
|
+
return turns
|
|
98
|
+
.filter((turn) => turn.sequence <= position && turn.payload !== undefined)
|
|
99
|
+
.map((turn) => turn.payload);
|
|
100
|
+
}
|
|
101
|
+
async #genesisHistory() {
|
|
102
|
+
const genesis = await readJsonOrNull(join(this.#root, genesisFile));
|
|
103
|
+
return structuredClone(genesis?.history ?? []);
|
|
104
|
+
}
|
|
105
|
+
async #materialsAt(position) {
|
|
106
|
+
const text = await readFileAt(this.#root, position, materialsFile);
|
|
107
|
+
if (text === null)
|
|
108
|
+
return [];
|
|
109
|
+
const parsed = JSON.parse(text);
|
|
110
|
+
return structuredClone(parsed.items ?? []);
|
|
111
|
+
}
|
|
112
|
+
async #stateAt(position) {
|
|
113
|
+
const tree = await readTreeAt(this.#root, position, ["state"]);
|
|
114
|
+
return [...tree]
|
|
115
|
+
.map(([path, contents]) => ({
|
|
116
|
+
path: path.slice("state/".length),
|
|
117
|
+
contents,
|
|
118
|
+
}))
|
|
119
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
120
|
+
}
|
|
121
|
+
#checkpointAt(commits) {
|
|
122
|
+
for (let index = commits.length - 1; index >= 0; index -= 1) {
|
|
123
|
+
const commit = commits[index];
|
|
124
|
+
if (commit.narrativeCheckpoint === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
return {
|
|
127
|
+
...commit.narrativeCheckpoint,
|
|
128
|
+
head: commit.head,
|
|
129
|
+
historyMessageId: commit.historyAppend.at(-1)?.messageId ?? "",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
async recover(head) {
|
|
135
|
+
const position = await this.#positionOf(head);
|
|
136
|
+
const commits = await this.#commitsUpTo(position);
|
|
137
|
+
const history = [
|
|
138
|
+
...(await this.#genesisHistory()),
|
|
139
|
+
...commits.flatMap((commit) => commit.historyAppend),
|
|
140
|
+
];
|
|
141
|
+
const checkpoint = this.#checkpointAt(commits);
|
|
142
|
+
return {
|
|
143
|
+
head: headName(position),
|
|
144
|
+
state: await this.#stateAt(position),
|
|
145
|
+
history: structuredClone(history),
|
|
146
|
+
additionalMaterials: await this.#materialsAt(position),
|
|
147
|
+
...(checkpoint === undefined
|
|
148
|
+
? {}
|
|
149
|
+
: { narrativeCheckpoint: structuredClone(checkpoint) }),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async recoverHeadResult() {
|
|
153
|
+
return this.recover();
|
|
154
|
+
}
|
|
155
|
+
async readNarrativeCheckpoint(head) {
|
|
156
|
+
const position = await this.#positionOf(head);
|
|
157
|
+
return this.#checkpointAt(await this.#commitsUpTo(position));
|
|
158
|
+
}
|
|
159
|
+
async recoverState(head) {
|
|
160
|
+
return this.#stateAt(await this.#positionOf(head));
|
|
161
|
+
}
|
|
162
|
+
async recoverMaterials(head) {
|
|
163
|
+
return this.#materialsAt(await this.#positionOf(head));
|
|
164
|
+
}
|
|
165
|
+
async readHistory() {
|
|
166
|
+
const position = await this.#positionOf();
|
|
167
|
+
return {
|
|
168
|
+
head: headName(position),
|
|
169
|
+
commits: await this.#commitsUpTo(position),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** 已提交的一轮;genesis 没有提交,返回 null。 */
|
|
173
|
+
async commitAt(head) {
|
|
174
|
+
const position = await this.#positionOf(head);
|
|
175
|
+
if (position === 0)
|
|
176
|
+
return null;
|
|
177
|
+
return this.#commitAtPosition(position);
|
|
178
|
+
}
|
|
179
|
+
/** 某一轮修改的状态文件内容:即该轮结束时的文件,并校验哈希。 */
|
|
180
|
+
async readStateChangeContents(commit, change) {
|
|
181
|
+
const contents = await readFileAt(this.#root, commit.sequence, `state/${change.relativePath}`);
|
|
182
|
+
if (contents === null || sha256Hex(contents) !== change.nextHash)
|
|
183
|
+
throw new WorldAuthorityError("corrupt", `State change content conflicts with its commit: ${change.relativePath}`);
|
|
184
|
+
return contents;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 结构校验(归档导入使用):头、轮记录与 genesis 可读,轮序号与路径合法。
|
|
188
|
+
* 当前文件本身不在账本里,因此这里不尝试从账本复算内容。
|
|
189
|
+
*/
|
|
190
|
+
async verifyStructure() {
|
|
191
|
+
await this.readHead();
|
|
192
|
+
const turns = await listTurns(this.#root);
|
|
193
|
+
for (const turn of turns) {
|
|
194
|
+
for (const file of turn.files) {
|
|
195
|
+
if (file.path.startsWith("runtime/turns/") ||
|
|
196
|
+
file.path === "runtime/head.json")
|
|
197
|
+
throw new WorldAuthorityError("corrupt", "轮记录引用了账本自身的文件");
|
|
198
|
+
}
|
|
199
|
+
if (turn.payload !== undefined) {
|
|
200
|
+
const commit = turn.payload;
|
|
201
|
+
if (commit.sequence !== turn.sequence)
|
|
202
|
+
throw new WorldAuthorityError("corrupt", `第 ${turn.sequence} 轮的提交序号不一致`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const genesis = await readJsonOrNull(join(this.#root, genesisFile));
|
|
206
|
+
if (genesis === null || !Array.isArray(genesis.state))
|
|
207
|
+
throw new WorldAuthorityError("corrupt", "genesis.json 缺少状态");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 以账本端点重建当前文件(状态、历史投影、历史背景与材料清单)。
|
|
211
|
+
* 只用于尚未发布的 staging 世界:它先删除投影目录再写入,中途失败由调用方丢弃整个 staging。
|
|
212
|
+
*/
|
|
213
|
+
async materializeCurrentFiles() {
|
|
214
|
+
const endpoint = await this.recover();
|
|
215
|
+
const writes = new Map();
|
|
216
|
+
for (const file of endpoint.state)
|
|
217
|
+
writes.set(`state/${file.path}`, file.contents);
|
|
218
|
+
for (const file of projectFileNativeHistorySurface(endpoint.history, (messageId) => {
|
|
219
|
+
throw new WorldAuthorityError("corrupt", `History-message identity cannot form a file path: ${messageId}`);
|
|
220
|
+
}))
|
|
221
|
+
writes.set(`history/${file.path}`, file.contents);
|
|
222
|
+
for (const file of projectHistoryBackgrounds(endpoint.history))
|
|
223
|
+
writes.set(`runtime/history-backgrounds/${file.path}`, file.contents);
|
|
224
|
+
writes.set(materialsFile, `${JSON.stringify({ head: endpoint.head, items: endpoint.additionalMaterials }, null, 2)}\n`);
|
|
225
|
+
for (const directory of [
|
|
226
|
+
"state",
|
|
227
|
+
"history",
|
|
228
|
+
join("runtime", "history-backgrounds"),
|
|
229
|
+
])
|
|
230
|
+
await rm(join(this.#root, directory), { recursive: true, force: true });
|
|
231
|
+
for (const [path, contents] of writes)
|
|
232
|
+
await writeTextAtomic(join(this.#root, path), contents);
|
|
233
|
+
}
|
|
234
|
+
async authorityFactAt(head) {
|
|
235
|
+
const position = await this.#positionOf(head);
|
|
236
|
+
if (position === 0)
|
|
237
|
+
throw new WorldAuthorityError("conflict", "Genesis is not a commit");
|
|
238
|
+
const commit = await this.#commitAtPosition(position);
|
|
239
|
+
const digest = await readTurnDigest(this.#root, position);
|
|
240
|
+
if (digest === null)
|
|
241
|
+
throw new WorldAuthorityError("corrupt", `第 ${position} 轮缺少记录`);
|
|
242
|
+
return { commit, digest };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 在当前端点之上追加一轮。调用方先校验内容;这里校验状态的前像哈希,
|
|
246
|
+
* 并一次性计算本轮需要替换的全部文件(状态、历史、历史背景与材料清单)。
|
|
247
|
+
*/
|
|
248
|
+
async appendCommit(input) {
|
|
249
|
+
const current = await this.readHead();
|
|
250
|
+
if (current.head !== input.parentHead)
|
|
251
|
+
throw new WorldAuthorityError("stale", "The parent endpoint of the Authority operation has changed");
|
|
252
|
+
const physical = await readLedgerHead(this.#root);
|
|
253
|
+
const sequence = physical + 1;
|
|
254
|
+
const head = `commit:${String(sequence)}`;
|
|
255
|
+
const currentState = new Map((await this.#stateAt(current.sequence)).map((file) => [
|
|
256
|
+
file.path,
|
|
257
|
+
file.contents,
|
|
258
|
+
]));
|
|
259
|
+
const changes = [];
|
|
260
|
+
for (const change of input.stateChanges) {
|
|
261
|
+
const previous = currentState.get(change.relativePath);
|
|
262
|
+
if ((previous === undefined ? null : sha256Hex(previous)) !==
|
|
263
|
+
change.expectedPreviousHash)
|
|
264
|
+
throw new WorldAuthorityError("conflict", `State-change previous hash conflicts: ${change.relativePath}`);
|
|
265
|
+
if (sha256Hex(change.canonicalNextBytes) !== change.nextHash)
|
|
266
|
+
throw new WorldAuthorityError("conflict", `State-change next hash conflicts: ${change.relativePath}`);
|
|
267
|
+
changes.push({
|
|
268
|
+
path: `state/${change.relativePath}`,
|
|
269
|
+
content: change.canonicalNextBytes,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const messages = input.historyAppend.map((message, index) => ({
|
|
273
|
+
messageId: `message.${String(sequence)}.${String(index + 1)}.${message.role}`,
|
|
274
|
+
role: message.role,
|
|
275
|
+
exactText: message.exactText,
|
|
276
|
+
...(message.background === undefined
|
|
277
|
+
? {}
|
|
278
|
+
: { background: structuredClone(message.background) }),
|
|
279
|
+
}));
|
|
280
|
+
for (const file of projectFileNativeHistorySurface(messages, (messageId) => {
|
|
281
|
+
throw new WorldAuthorityError("corrupt", `History-message identity cannot form a file path: ${messageId}`);
|
|
282
|
+
}))
|
|
283
|
+
changes.push({ path: `history/${file.path}`, content: file.contents });
|
|
284
|
+
for (const file of projectHistoryBackgrounds(messages))
|
|
285
|
+
changes.push({
|
|
286
|
+
path: `runtime/history-backgrounds/${file.path}`,
|
|
287
|
+
content: file.contents,
|
|
288
|
+
});
|
|
289
|
+
changes.push({
|
|
290
|
+
path: materialsFile,
|
|
291
|
+
content: `${JSON.stringify({ head, items: input.nextMaterials }, null, 2)}\n`,
|
|
292
|
+
});
|
|
293
|
+
const commit = {
|
|
294
|
+
sequence,
|
|
295
|
+
head,
|
|
296
|
+
operationId: input.operationId,
|
|
297
|
+
parentHead: input.parentHead,
|
|
298
|
+
mode: input.mode,
|
|
299
|
+
...(input.playContext === undefined
|
|
300
|
+
? {}
|
|
301
|
+
: { playContext: input.playContext }),
|
|
302
|
+
...(input.worldClock === undefined
|
|
303
|
+
? {}
|
|
304
|
+
: { worldClock: input.worldClock }),
|
|
305
|
+
historyAppend: messages,
|
|
306
|
+
stateChanges: input.stateChanges.map((change) => {
|
|
307
|
+
const { canonicalNextBytes, ...stored } = change;
|
|
308
|
+
void canonicalNextBytes;
|
|
309
|
+
return structuredClone(stored);
|
|
310
|
+
}),
|
|
311
|
+
nextAdditionalMaterials: structuredClone([...input.nextMaterials]),
|
|
312
|
+
...(input.correctionTargets === undefined
|
|
313
|
+
? {}
|
|
314
|
+
: { correctionTargets: [...input.correctionTargets] }),
|
|
315
|
+
...(input.corrects === undefined ? {} : { corrects: input.corrects }),
|
|
316
|
+
...(input.narrativeCheckpoint === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { narrativeCheckpoint: structuredClone(input.narrativeCheckpoint) }),
|
|
319
|
+
};
|
|
320
|
+
const record = await appendTurn(this.#root, {
|
|
321
|
+
mode: input.mode,
|
|
322
|
+
changes,
|
|
323
|
+
payload: commit,
|
|
324
|
+
});
|
|
325
|
+
const commitDigest = await readTurnDigest(this.#root, record.sequence);
|
|
326
|
+
if (commitDigest === null)
|
|
327
|
+
throw new WorldAuthorityError("corrupt", "新提交缺少轮记录");
|
|
328
|
+
return { commit, commitDigest };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// 与状态前像哈希保持同一格式(sha256:<hex>)。
|
|
332
|
+
async function writeTextAtomic(path, contents) {
|
|
333
|
+
await mkdir(dirname(path), { recursive: true });
|
|
334
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
335
|
+
await writeFile(temporary, contents, "utf8");
|
|
336
|
+
await rename(temporary, path);
|
|
337
|
+
}
|
|
338
|
+
function sha256Hex(value) {
|
|
339
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
340
|
+
}
|