narraeon 0.6.1 → 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/dist/node/protocol/v1.js +12 -0
- package/dist/node/runtime/V1Runtime.js +4 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +25 -2
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +39 -7
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +4 -1
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +14 -6
- package/dist/node/runtime/setting/SettingImprovementSession.js +39 -25
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -1
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +29 -2
- package/dist/node/server/mobile.js +46 -14
- 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 +1 -1
- package/dist/web/assets/index-7GzLSUpU.js +0 -1323
package/dist/node/protocol/v1.js
CHANGED
|
@@ -68,6 +68,11 @@ const requiredFields = {
|
|
|
68
68
|
continuation: "object",
|
|
69
69
|
},
|
|
70
70
|
"setting-improvement.cancel": { sessionId: "string" },
|
|
71
|
+
"setting-improvement.resume": {
|
|
72
|
+
packageId: "string",
|
|
73
|
+
sessionId: "string",
|
|
74
|
+
requestId: "string",
|
|
75
|
+
},
|
|
71
76
|
"play.create": { name: "string" },
|
|
72
77
|
"play.copy": { presetId: "string" },
|
|
73
78
|
"play.save": { presetId: "string", name: "string", files: "object" },
|
|
@@ -120,6 +125,11 @@ const requiredFields = {
|
|
|
120
125
|
continuation: "object",
|
|
121
126
|
},
|
|
122
127
|
"world.revision.cancel": { sessionId: "string" },
|
|
128
|
+
"world.revision.resume": {
|
|
129
|
+
worldId: "string",
|
|
130
|
+
sessionId: "string",
|
|
131
|
+
requestId: "string",
|
|
132
|
+
},
|
|
123
133
|
"world.revision.files.replace": {
|
|
124
134
|
worldId: "string",
|
|
125
135
|
epochId: "string",
|
|
@@ -422,6 +432,7 @@ const requestTypes = new Set([
|
|
|
422
432
|
"setting-improvement.rollback",
|
|
423
433
|
"setting-improvement.message",
|
|
424
434
|
"setting-improvement.cancel",
|
|
435
|
+
"setting-improvement.resume",
|
|
425
436
|
"play.read",
|
|
426
437
|
"play.create",
|
|
427
438
|
"play.copy",
|
|
@@ -456,6 +467,7 @@ const requestTypes = new Set([
|
|
|
456
467
|
"world.revision.session.delete",
|
|
457
468
|
"world.revision.message",
|
|
458
469
|
"world.revision.cancel",
|
|
470
|
+
"world.revision.resume",
|
|
459
471
|
"world.revision.files.replace",
|
|
460
472
|
"world.revision.rollback",
|
|
461
473
|
"world.revision.apply",
|
|
@@ -383,6 +383,8 @@ export class V1Runtime {
|
|
|
383
383
|
return this.#settingImprovements.send(request);
|
|
384
384
|
case "setting-improvement.cancel":
|
|
385
385
|
return this.#settingImprovements.cancel(request.sessionId);
|
|
386
|
+
case "setting-improvement.resume":
|
|
387
|
+
return this.#settingImprovements.resume(request);
|
|
386
388
|
case "play.read":
|
|
387
389
|
return this.#playPresets.list();
|
|
388
390
|
case "play.create":
|
|
@@ -614,6 +616,8 @@ export class V1Runtime {
|
|
|
614
616
|
return this.#worldRevisionCall(() => this.#worldRevisions.send(request));
|
|
615
617
|
case "world.revision.cancel":
|
|
616
618
|
return this.#worldRevisionCall(() => this.#worldRevisions.cancel(request.sessionId));
|
|
619
|
+
case "world.revision.resume":
|
|
620
|
+
return this.#worldRevisionCall(() => this.#worldRevisions.resume(request));
|
|
617
621
|
case "world.revision.files.replace":
|
|
618
622
|
return this.#worldRevisionCall(() => this.#worldRevisions.replaceFiles(request));
|
|
619
623
|
case "world.revision.rollback":
|
|
@@ -119,8 +119,15 @@ export class ModelToolConversation {
|
|
|
119
119
|
await input.settleToolResponse(session, session.modelItems.length - 1, toolCalls);
|
|
120
120
|
input.changed?.(true);
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// The exchange budget only bounds a runaway loop. Every tool response is
|
|
123
|
+
// already settled, so the conversation pauses and can resume unchanged.
|
|
124
|
+
session.runStatus = "paused";
|
|
125
|
+
session.activeRequestId = null;
|
|
126
|
+
session.completedRequestIds = rememberRequest(session.completedRequestIds, live.requestId);
|
|
127
|
+
session.lastFailure = null;
|
|
128
|
+
session.updatedAt = Date.now();
|
|
129
|
+
await input.save(session);
|
|
130
|
+
return input.view(session);
|
|
124
131
|
}
|
|
125
132
|
catch (error) {
|
|
126
133
|
session.runStatus = "interrupted";
|
|
@@ -154,6 +161,22 @@ export function appendAuthoringUserMessage(session, message, requestId) {
|
|
|
154
161
|
session.lastFailure = null;
|
|
155
162
|
session.updatedAt = now;
|
|
156
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Starts another exchange budget for a paused conversation without appending a
|
|
166
|
+
* user message; the next request replays the settled transcript as-is. The run
|
|
167
|
+
* still belongs to the paused user request, so that request stays active; the
|
|
168
|
+
* caller's resume request ID only deduplicates the resume itself.
|
|
169
|
+
*/
|
|
170
|
+
export function resumePausedAuthoringConversation(session) {
|
|
171
|
+
if (session.runStatus !== "paused")
|
|
172
|
+
throw new Error("Only a paused authoring conversation can be continued");
|
|
173
|
+
if (session.modelItems.at(-1)?.kind !== "tool")
|
|
174
|
+
throw new Error("The paused authoring conversation does not end with settled tool results");
|
|
175
|
+
session.runStatus = "running";
|
|
176
|
+
session.activeRequestId = session.requests?.at(-1)?.requestId ?? null;
|
|
177
|
+
session.lastFailure = null;
|
|
178
|
+
session.updatedAt = Date.now();
|
|
179
|
+
}
|
|
157
180
|
export function requiredAuthoringMessage(message) {
|
|
158
181
|
if (message.trim().length === 0)
|
|
159
182
|
throw new Error("An authoring message cannot be empty");
|
|
@@ -236,16 +236,48 @@ export function renderDocumentWritePosition(shortRef, fact, locale) {
|
|
|
236
236
|
? `最近写入位置 @${shortRef}:\n正文:${worldWritePositionText(fact.lastBodyWrite, locale)}\n元信息:${worldWritePositionText(fact.lastMetadataWrite, locale)}`
|
|
237
237
|
: `Last committed write @${shortRef}:\nBody: ${worldWritePositionText(fact.lastBodyWrite, locale)}\nMetadata: ${worldWritePositionText(fact.lastMetadataWrite, locale)}`;
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Lists only documents the current tool response changed or that exceed their
|
|
241
|
+
* advisory limit, so the report stays proportional to the edit rather than to
|
|
242
|
+
* the package size.
|
|
243
|
+
*/
|
|
244
|
+
export function renderWorldMaintenanceReport(report, changedShortRefs, locale) {
|
|
240
245
|
if (report === undefined)
|
|
241
246
|
return "";
|
|
242
247
|
const zh = locale === "zh-CN";
|
|
248
|
+
const heading = zh
|
|
249
|
+
? "# 文档整理报告(UTF-8 字节;超限仅提示)"
|
|
250
|
+
: "# Document maintenance (UTF-8 bytes; advisory only)";
|
|
251
|
+
const documents = report.documents.filter((document) => document.overAdvisory || changedShortRefs.has(document.shortRef));
|
|
252
|
+
const slots = report.slots.filter((slot) => slot.overAdvisory ||
|
|
253
|
+
slot.documents.some((shortRef) => changedShortRefs.has(shortRef)));
|
|
254
|
+
if (documents.length === 0 && slots.length === 0)
|
|
255
|
+
return `${heading}\n${zh ? "本次没有改动世界文档,也没有文档或材料槽超过建议上限。" : "No world document changed in this response, and no document or slot exceeds its advisory limit."}`;
|
|
256
|
+
const allColumns = [
|
|
257
|
+
{ label: zh ? "正文" : "body", value: (d) => d.bodyBytes },
|
|
258
|
+
{
|
|
259
|
+
label: zh ? "注入贡献" : "injection contribution",
|
|
260
|
+
value: (d) => d.injectedBytes,
|
|
261
|
+
},
|
|
262
|
+
{ label: zh ? "建议上限" : "advisory", value: (d) => d.advisoryBytes },
|
|
263
|
+
{ label: zh ? "基准" : "baseline", value: (d) => d.baselineBytes },
|
|
264
|
+
{ label: zh ? "增长" : "growth", value: (d) => d.growthBytes },
|
|
265
|
+
];
|
|
266
|
+
// Columns that are unset for every listed document carry no information.
|
|
267
|
+
const columns = allColumns.filter(({ value }) => documents.some((document) => value(document) !== null));
|
|
268
|
+
const overAdvisory = report.documents.some(({ overAdvisory }) => overAdvisory) ||
|
|
269
|
+
report.slots.some(({ overAdvisory }) => overAdvisory);
|
|
243
270
|
return [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
heading,
|
|
272
|
+
...(overAdvisory
|
|
273
|
+
? [
|
|
274
|
+
`${zh ? "世界材料 / 其中检查点历史 / 作者提示 / Runtime 提示 / 工具定义" : "World materials / checkpoint history subset / author instructions / Runtime instructions / tool definitions"}: ${report.worldMaterialsBytes} / ${report.checkpointHistoryBytes} / ${report.authorInstructionBytes} / ${report.runtimeInstructionBytes} / ${report.toolDefinitionBytes}`,
|
|
275
|
+
]
|
|
276
|
+
: []),
|
|
277
|
+
...(documents.length > 0
|
|
278
|
+
? [columns.map(({ label }) => label).join(" / ")]
|
|
279
|
+
: []),
|
|
280
|
+
...documents.map((document) => `@${document.shortRef}: ${columns.map(({ value }) => value(document) ?? "—").join(" / ")}${document.overAdvisory ? (zh ? "(超过建议)" : " (over advisory)") : ""}`),
|
|
281
|
+
...slots.map((slot) => `${zh ? "材料槽" : "Slot"} ${slot.id}: ${slot.bytes} / ${slot.advisoryBytes ?? "—"}${slot.overAdvisory ? (zh ? "(超过建议)" : " (over advisory)") : ""}`),
|
|
250
282
|
].join("\n");
|
|
251
283
|
}
|
|
@@ -487,7 +487,10 @@ function validOptionalTitle(value) {
|
|
|
487
487
|
!/[\r\n]/u.test(value)));
|
|
488
488
|
}
|
|
489
489
|
function validRunStatus(value) {
|
|
490
|
-
return value === "ready" ||
|
|
490
|
+
return (value === "ready" ||
|
|
491
|
+
value === "running" ||
|
|
492
|
+
value === "paused" ||
|
|
493
|
+
value === "interrupted");
|
|
491
494
|
}
|
|
492
495
|
function validCount(value) {
|
|
493
496
|
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
@@ -329,6 +329,7 @@ export class SettingAuthoringTransaction {
|
|
|
329
329
|
execute(calls) {
|
|
330
330
|
const normalized = calls.map(normalizeCall);
|
|
331
331
|
const results = [];
|
|
332
|
+
const responseBase = this.files();
|
|
332
333
|
let lastSuccessfulWrite = -1;
|
|
333
334
|
for (const call of normalized) {
|
|
334
335
|
const before = this.files();
|
|
@@ -344,7 +345,9 @@ export class SettingAuthoringTransaction {
|
|
|
344
345
|
if (lastSuccessfulWrite >= 0) {
|
|
345
346
|
this.#review = this.#inspect();
|
|
346
347
|
const settled = results[lastSuccessfulWrite];
|
|
347
|
-
|
|
348
|
+
// The stored review stays cumulative for the whole user request; the
|
|
349
|
+
// model only needs what this tool response changed.
|
|
350
|
+
settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#locale)}`;
|
|
348
351
|
}
|
|
349
352
|
return results;
|
|
350
353
|
}
|
|
@@ -450,9 +453,16 @@ export class SettingAuthoringTransaction {
|
|
|
450
453
|
}
|
|
451
454
|
}
|
|
452
455
|
}
|
|
453
|
-
function renderAutomaticReview(review, locale) {
|
|
454
|
-
if (review.status === "usable")
|
|
455
|
-
|
|
456
|
+
function renderAutomaticReview(review, responseDiff, snapshot, locale) {
|
|
457
|
+
if (review.status === "usable" && review.preview !== null) {
|
|
458
|
+
const changed = responseDiff.length;
|
|
459
|
+
const cumulative = review.diff.length;
|
|
460
|
+
const changedPaths = new Set(responseDiff.map(({ path }) => path));
|
|
461
|
+
const changedShortRefs = new Set(listQueryableWorldDocuments(snapshot)
|
|
462
|
+
.filter(({ logicalPath }) => changedPaths.has(logicalPath))
|
|
463
|
+
.map(({ shortRef }) => shortRef));
|
|
464
|
+
return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changed ${changed} file(s) in the content package${cumulative === changed ? "" : `; ${cumulative} file(s) changed so far in this user request`}. Use this review and the coverage below to decide whether the current user request is satisfied or further edits are needed.`, `# 当前树自动检查通过\n\n内容树校验和真实 Prompt Preview 均已通过;本次工具响应修改内容包中的 ${changed} 个文件${cumulative === changed ? "" : `,本次用户请求累计改动 ${cumulative} 个文件`}。请根据本次检查与下方覆盖报告判断当前用户要求是否已经满足,或是否还需修改。`)}\n\n${renderPlayCoverage(buildSettingAuthoringPlayCoverage(snapshot, review.preview, responseDiff), locale)}\n\n${renderWorldMaintenanceReport(review.preview.compilation.maintenance, changedShortRefs, locale)}`;
|
|
465
|
+
}
|
|
456
466
|
return [
|
|
457
467
|
localized(locale, "# Current-tree review needs repair", "# 当前树自动检查需要修复"),
|
|
458
468
|
"",
|
|
@@ -561,8 +571,6 @@ function playAccessDetail(access) {
|
|
|
561
571
|
return details[access];
|
|
562
572
|
}
|
|
563
573
|
function renderPlayCoverage(coverage, locale) {
|
|
564
|
-
if (coverage === null)
|
|
565
|
-
return localized(locale, "# Play-consumption coverage\n\nCoverage is unavailable until the real Prompt Preview succeeds.", "# 游玩读取覆盖\n\n真实 Prompt Preview 通过后才会提供覆盖报告。");
|
|
566
574
|
const labels = locale === "zh-CN"
|
|
567
575
|
? {
|
|
568
576
|
full_injected: "全文注入",
|
|
@@ -4,7 +4,7 @@ import { InvalidContentTreeError, } from "../content/ContentWorkspace.js";
|
|
|
4
4
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
5
5
|
import { equalModelHostBinding } from "../model/ModelHost.js";
|
|
6
6
|
import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
7
|
-
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, } from "../authoring/ModelToolConversation.js";
|
|
7
|
+
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
|
|
8
8
|
import { summarizeStoredSettingImprovementSession, } from "./FileNativeSettingImprovementStore.js";
|
|
9
9
|
import { SettingAuthoringTransaction, settingImprovementRuntimeContract, settingImprovementToolDefinitions, } from "./SettingAuthoringTransaction.js";
|
|
10
10
|
class RecoveryWriteConflictError extends Error {
|
|
@@ -185,33 +185,48 @@ export class SettingImprovementSession {
|
|
|
185
185
|
session = await this.#store.read(input.continuation.sessionId);
|
|
186
186
|
assertPackage(session, input.packageId);
|
|
187
187
|
}
|
|
188
|
-
session
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
throw new Error("No saved model connection matches this setting-improvement conversation");
|
|
200
|
-
const { playPreset, bootstrap } = await this.#compileCurrent(input.packageId, session.modelBinding);
|
|
201
|
-
session.schemaVersion = 3;
|
|
202
|
-
session.requests ??= [];
|
|
203
|
-
session.requests.push({
|
|
204
|
-
requestId,
|
|
205
|
-
modelItemStart: session.modelItems.length,
|
|
206
|
-
bootstrap,
|
|
207
|
-
playPreset: structuredClone(playPreset),
|
|
188
|
+
return this.#startRequest(session, requestId, async (next) => {
|
|
189
|
+
const { playPreset, bootstrap } = await this.#compileCurrent(input.packageId, next.modelBinding);
|
|
190
|
+
next.schemaVersion = 3;
|
|
191
|
+
next.requests ??= [];
|
|
192
|
+
next.requests.push({
|
|
193
|
+
requestId,
|
|
194
|
+
modelItemStart: next.modelItems.length,
|
|
195
|
+
bootstrap,
|
|
196
|
+
playPreset: structuredClone(playPreset),
|
|
197
|
+
});
|
|
198
|
+
appendAuthoringUserMessage(next, message, requestId);
|
|
208
199
|
});
|
|
209
|
-
appendAuthoringUserMessage(session, message, requestId);
|
|
210
|
-
await this.#store.save(session);
|
|
211
|
-
return { session, run: this.#startRun(session, host, requestId) };
|
|
212
200
|
});
|
|
213
201
|
return prepared.run ?? this.#view(prepared.session);
|
|
214
202
|
}
|
|
203
|
+
/** Continues a paused conversation with the frozen request it paused in. */
|
|
204
|
+
async resume(input) {
|
|
205
|
+
const requestId = requiredRequestId(input.requestId);
|
|
206
|
+
const prepared = await this.#mutate(async () => {
|
|
207
|
+
const session = await this.#store.read(input.sessionId);
|
|
208
|
+
assertPackage(session, input.packageId);
|
|
209
|
+
return this.#startRequest(session, requestId, (next) => resumePausedAuthoringConversation(next));
|
|
210
|
+
});
|
|
211
|
+
return prepared.run ?? this.#view(prepared.session);
|
|
212
|
+
}
|
|
213
|
+
async #startRequest(stored, requestId, prepare) {
|
|
214
|
+
const session = await this.#recoverIfNeeded(stored);
|
|
215
|
+
if (session.completedRequestIds.includes(requestId))
|
|
216
|
+
return { session, run: null };
|
|
217
|
+
const running = this.#conversation.running(session.sessionId);
|
|
218
|
+
if (running !== null) {
|
|
219
|
+
if (running.requestId !== requestId)
|
|
220
|
+
throw new Error("Another message in this setting-improvement conversation is still running");
|
|
221
|
+
return { session, run: running.promise };
|
|
222
|
+
}
|
|
223
|
+
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
224
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
225
|
+
throw new Error("No saved model connection matches this setting-improvement conversation");
|
|
226
|
+
await prepare(session);
|
|
227
|
+
await this.#store.save(session);
|
|
228
|
+
return { session, run: this.#startRun(session, host, requestId) };
|
|
229
|
+
}
|
|
215
230
|
async preview(packageId, sessionId) {
|
|
216
231
|
const session = sessionId === undefined ? null : await this.#store.read(sessionId);
|
|
217
232
|
if (session !== null)
|
|
@@ -307,7 +322,6 @@ export class SettingImprovementSession {
|
|
|
307
322
|
view: (next) => this.#view(next),
|
|
308
323
|
failure: {
|
|
309
324
|
emptyResponse: "The model returned an empty tool-free authoring response",
|
|
310
|
-
exchangeLimit: "Setting improvement exceeded 64 model exchanges for one user message",
|
|
311
325
|
cancelled: "The setting-improvement response was cancelled. Complete tool responses already published to the current tree were retained.",
|
|
312
326
|
interrupted: "The setting-improvement response was interrupted",
|
|
313
327
|
},
|
|
@@ -246,7 +246,7 @@ function validateSession(value) {
|
|
|
246
246
|
/[\r\n\0]/u.test(value.worldTitle) ||
|
|
247
247
|
typeof value.epochId !== "string" ||
|
|
248
248
|
(value.locale !== "en" && value.locale !== "zh-CN") ||
|
|
249
|
-
!["ready", "running", "interrupted"].includes(String(value.runStatus)) ||
|
|
249
|
+
!["ready", "running", "paused", "interrupted"].includes(String(value.runStatus)) ||
|
|
250
250
|
!finite(value.createdAt) ||
|
|
251
251
|
!finite(value.updatedAt) ||
|
|
252
252
|
value.updatedAt < value.createdAt ||
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authoringRequestPreviews } from "../authoring/AuthoringRequestSnapshot.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
|
|
4
|
-
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, } from "../authoring/ModelToolConversation.js";
|
|
4
|
+
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
|
|
5
5
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
6
6
|
import { equalModelHostBinding } from "../model/ModelHost.js";
|
|
7
7
|
import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
@@ -215,6 +215,34 @@ export class WorldRevisionSession {
|
|
|
215
215
|
});
|
|
216
216
|
return prepared.run ?? this.#view(prepared.session);
|
|
217
217
|
}
|
|
218
|
+
/** Continues a paused conversation within the revision it paused in. */
|
|
219
|
+
async resume(input) {
|
|
220
|
+
const requestId = requiredRequestId(input.requestId);
|
|
221
|
+
const prepared = await this.#mutate(async () => {
|
|
222
|
+
let session = await this.#store.readSession(input.sessionId);
|
|
223
|
+
assertWorld(session, input.worldId);
|
|
224
|
+
session = await this.#recoverIfNeeded(session);
|
|
225
|
+
if (session.completedRequestIds.includes(requestId))
|
|
226
|
+
return { session, run: null };
|
|
227
|
+
const running = this.#conversation.running(session.sessionId);
|
|
228
|
+
if (running !== null) {
|
|
229
|
+
if (running.requestId !== requestId)
|
|
230
|
+
throw new Error("Another message in this world-revision conversation is still running");
|
|
231
|
+
return { session, run: running.promise };
|
|
232
|
+
}
|
|
233
|
+
await this.#assertNoRunningConversation(input.worldId, session.sessionId);
|
|
234
|
+
const epoch = await this.#workspace.active(input.worldId);
|
|
235
|
+
if (epoch?.epochId !== session.epochId)
|
|
236
|
+
throw new WorldRevisionWorkspaceError("The paused conversation belongs to a revision that is no longer active; send a new message instead");
|
|
237
|
+
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
238
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
239
|
+
throw new Error("No saved model connection matches this world-revision conversation");
|
|
240
|
+
resumePausedAuthoringConversation(session);
|
|
241
|
+
await this.#store.saveSession(session);
|
|
242
|
+
return { session, run: this.#startRun(session, host, requestId) };
|
|
243
|
+
});
|
|
244
|
+
return prepared.run ?? this.#view(prepared.session);
|
|
245
|
+
}
|
|
218
246
|
async preview(worldId, sessionId) {
|
|
219
247
|
const session = sessionId === undefined ? null : await this.#store.readSession(sessionId);
|
|
220
248
|
if (session !== null)
|
|
@@ -317,7 +345,6 @@ export class WorldRevisionSession {
|
|
|
317
345
|
},
|
|
318
346
|
view: (next) => this.#view(next),
|
|
319
347
|
failure: {
|
|
320
|
-
exchangeLimit: "World revision exceeded 64 model exchanges for one user message",
|
|
321
348
|
cancelled: "The world-revision response was cancelled. Settled worktree changes were retained.",
|
|
322
349
|
interrupted: "The world-revision response was interrupted",
|
|
323
350
|
},
|
|
@@ -1,20 +1,52 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { inspect } from "node:util";
|
|
1
4
|
import { resolveAppPaths } from "../runtime/config/appPaths.js";
|
|
2
5
|
import { parseWebPort, startWebServer } from "./startWebServer.js";
|
|
3
6
|
// Mobile entry: started inside the Android app's Node runtime. The native shell
|
|
4
7
|
// owns the process lifecycle, so this module never registers signal handlers and
|
|
5
|
-
// never calls process.exit(); a startup failure
|
|
6
|
-
//
|
|
7
|
-
const staticRoot = process.env.NARRAEON_STATIC_ROOT?.trim();
|
|
8
|
-
if (!staticRoot)
|
|
9
|
-
throw new Error("NARRAEON_STATIC_ROOT is required for the mobile entry");
|
|
10
|
-
const port = parseWebPort(process.env.NARRAEON_PORT);
|
|
8
|
+
// never calls process.exit(); a startup failure is logged and the native side
|
|
9
|
+
// reports it.
|
|
11
10
|
const paths = resolveAppPaths(process.env);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const crashLogPath = join(paths.logRoot, "crash", "node-errors.log");
|
|
12
|
+
// Node shares the Android app process: an uncaught error would make Node call
|
|
13
|
+
// exit() and close the whole app. Record the error and keep serving instead; each
|
|
14
|
+
// Runtime write is atomic, so a failed request does not leave a half-written file.
|
|
15
|
+
process.on("uncaughtException", (error, origin) => {
|
|
16
|
+
recordCrash(origin, error);
|
|
17
17
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
process.on("unhandledRejection", (reason) => {
|
|
19
|
+
recordCrash("unhandledRejection", reason);
|
|
20
|
+
});
|
|
21
|
+
process.on("exit", (code) => {
|
|
22
|
+
recordCrash("exit", `process exiting with code ${code}`);
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
const staticRoot = process.env.NARRAEON_STATIC_ROOT?.trim();
|
|
26
|
+
if (!staticRoot)
|
|
27
|
+
throw new Error("NARRAEON_STATIC_ROOT is required for the mobile entry");
|
|
28
|
+
const port = parseWebPort(process.env.NARRAEON_PORT);
|
|
29
|
+
const server = await startWebServer({
|
|
30
|
+
paths,
|
|
31
|
+
staticRoot,
|
|
32
|
+
host: "127.0.0.1",
|
|
33
|
+
port,
|
|
34
|
+
});
|
|
35
|
+
const address = server.server.address();
|
|
36
|
+
const listeningPort = typeof address === "object" && address !== null ? address.port : port;
|
|
37
|
+
process.stdout.write(`NARRAEON_READY ${listeningPort}\n`);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
recordCrash("startup", error);
|
|
41
|
+
}
|
|
42
|
+
function recordCrash(kind, value) {
|
|
43
|
+
const text = `${new Date().toISOString()} [${kind}] ${typeof value === "string" ? value : inspect(value, { depth: 4 })}\n`;
|
|
44
|
+
process.stderr.write(text);
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(join(paths.logRoot, "crash"), { recursive: true, mode: 0o700 });
|
|
47
|
+
appendFileSync(crashLogPath, text, { encoding: "utf8", mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// The stderr copy above still reaches logcat and the native stderr log.
|
|
51
|
+
}
|
|
52
|
+
}
|