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.
- package/dist/node/protocol/modelConnections.js +15 -0
- package/dist/node/protocol/modelUsage.js +4 -0
- package/dist/node/protocol/sillyTavernContent.js +1 -0
- package/dist/node/protocol/v1.js +97 -0
- package/dist/node/runtime/V1Runtime.js +105 -3
- package/dist/node/runtime/authoring/AuthoringEditPlan.js +79 -0
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
- package/dist/node/runtime/authoring/AuthoringRetry.js +59 -0
- package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +66 -25
- package/dist/node/runtime/content/ContentWorkspace.js +10 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +14 -8
- package/dist/node/runtime/content/SillyTavernContent.js +796 -0
- package/dist/node/runtime/model/AiFailureLog.js +8 -1
- package/dist/node/runtime/model/DispatchFailure.js +8 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +627 -18
- package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
- package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
- package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
- package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
- package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
- package/dist/node/runtime/model/ProviderStream.js +4 -2
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +199 -10
- package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +51 -23
- package/dist/node/runtime/play/PlayCallChain.js +92 -8
- package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +321 -15
- package/dist/node/runtime/setting/SettingImprovementSession.js +285 -21
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +236 -15
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +324 -50
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +47 -1
- package/dist/node/shared/application-preset-files.js +29 -0
- package/dist/node/shared/default-preset-host.js +50 -38
- package/dist/node/shared/first-party-action-choices.js +7 -11
- package/dist/node/shared/first-party-generic-panels.js +7 -11
- package/dist/node/shared/first-party-player-view.js +7 -11
- package/dist/node/shared/ordered-author-prompts.js +6 -4
- package/dist/node/shared/ordered-play-prompts.js +4 -2
- package/dist/node/shared/prompt-arrangement.js +205 -0
- package/dist/node/shared/sillytavern-preset.js +454 -0
- package/dist/web/assets/index-BthiDIQu.js +1432 -0
- package/dist/web/assets/{index-DH2BVsYd.css → index-wO9Y4HT1.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B2kbaOGe.js +0 -1408
|
@@ -1,15 +1,37 @@
|
|
|
1
|
+
import { completedAuthoringEdit, planAuthoringEdit, revertAuthoringChanges, } from "../authoring/AuthoringEditPlan.js";
|
|
2
|
+
import { canRetryAuthoringDispatch, checkpointNextAuthoringDispatch, recoverAuthoringDispatch, } from "../authoring/AuthoringRetry.js";
|
|
1
3
|
import { authoringRequestPreviews } from "../authoring/AuthoringRequestSnapshot.js";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
6
|
import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
|
|
4
|
-
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
|
|
7
|
+
import { appendAuthoringUserMessage, authoringExchange, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
|
|
5
8
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
6
9
|
import { equalModelHostBinding } from "../model/ModelHost.js";
|
|
7
10
|
import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
8
11
|
import { authoringPreloadAuthorization, buildAuthoringPreload, worldRevisionAuthoringSurface, } from "../setting/SettingAuthoringTransaction.js";
|
|
9
12
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
10
|
-
import { WorldRevisionAuthoringTransaction, worldRevisionRuntimeContract, worldRevisionToolDefinitions, } from "./WorldRevisionAuthoringTransaction.js";
|
|
13
|
+
import { WorldRevisionAuthoringTransaction, inspectWorldRevisionFiles, worldRevisionRuntimeContract, worldRevisionToolDefinitions, } from "./WorldRevisionAuthoringTransaction.js";
|
|
11
14
|
import { WorldRevisionWorkspaceError } from "./WorldRevisionWorkspace.js";
|
|
12
15
|
import { historyEntries } from "../history/HistoryTools.js";
|
|
16
|
+
export class WorldRevisionMessageEditError extends Error {
|
|
17
|
+
reason;
|
|
18
|
+
paths;
|
|
19
|
+
constructor(reason, paths = []) {
|
|
20
|
+
super(reason === "sealed"
|
|
21
|
+
? "Changes in a sealed revision cannot be reverted; keep current files to continue"
|
|
22
|
+
: reason === "conflict"
|
|
23
|
+
? `Cannot revert changed files: ${paths.join(", ")}`
|
|
24
|
+
: `Reverting would make the revision invalid: ${paths.join(", ")}`);
|
|
25
|
+
this.name = "WorldRevisionMessageEditError";
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.paths = paths;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const liveScopes = new Map();
|
|
31
|
+
const releaseScope = new FinalizationRegistry(({ scope, reference }) => {
|
|
32
|
+
if (liveScopes.get(scope) === reference)
|
|
33
|
+
liveScopes.delete(scope);
|
|
34
|
+
});
|
|
13
35
|
/** Conversation orchestration over the shared locked revision worktree. */
|
|
14
36
|
export class WorldRevisionSession {
|
|
15
37
|
#store;
|
|
@@ -21,9 +43,9 @@ export class WorldRevisionSession {
|
|
|
21
43
|
#bindExistingModelHost;
|
|
22
44
|
#bindPlayPreset;
|
|
23
45
|
#preview;
|
|
24
|
-
#
|
|
46
|
+
#live;
|
|
47
|
+
#conversation;
|
|
25
48
|
#changed;
|
|
26
|
-
#mutationTail = Promise.resolve();
|
|
27
49
|
constructor(input) {
|
|
28
50
|
this.#changed =
|
|
29
51
|
input.changed ??
|
|
@@ -31,6 +53,21 @@ export class WorldRevisionSession {
|
|
|
31
53
|
/* No observers attached. */
|
|
32
54
|
});
|
|
33
55
|
this.#store = input.store;
|
|
56
|
+
let live = liveScopes.get(input.store.sessionScope)?.deref();
|
|
57
|
+
if (live === undefined) {
|
|
58
|
+
live = {
|
|
59
|
+
conversation: new ModelToolConversation(),
|
|
60
|
+
mutationTail: Promise.resolve(),
|
|
61
|
+
};
|
|
62
|
+
const reference = new WeakRef(live);
|
|
63
|
+
liveScopes.set(input.store.sessionScope, reference);
|
|
64
|
+
releaseScope.register(live, {
|
|
65
|
+
scope: input.store.sessionScope,
|
|
66
|
+
reference,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
this.#live = live;
|
|
70
|
+
this.#conversation = live.conversation;
|
|
34
71
|
this.#workspace = input.workspace;
|
|
35
72
|
this.#worlds = input.worlds;
|
|
36
73
|
this.#compiler = input.compiler;
|
|
@@ -55,19 +92,21 @@ export class WorldRevisionSession {
|
|
|
55
92
|
}
|
|
56
93
|
async overview(worldId) {
|
|
57
94
|
return this.#mutate(async () => {
|
|
58
|
-
const
|
|
59
|
-
const [
|
|
60
|
-
this.#
|
|
95
|
+
const sessions = await this.#recoverWorldSessions(worldId);
|
|
96
|
+
const [activeEpoch, sealedEpochs] = await Promise.all([
|
|
97
|
+
this.#workspace.active(worldId),
|
|
61
98
|
this.#workspace.sealed(worldId),
|
|
62
99
|
]);
|
|
63
|
-
const latest = sessions
|
|
100
|
+
const latest = sessions.find((session) => !session.damagedContinuation) ?? null;
|
|
64
101
|
const shownEpoch = activeEpoch ?? (await this.#workspace.latest(worldId));
|
|
65
102
|
return {
|
|
66
103
|
epoch: shownEpoch === null
|
|
67
104
|
? null
|
|
68
105
|
: this.#workspace.view(shownEpoch, activeEpoch?.epochId === shownEpoch.epochId),
|
|
69
106
|
sealedEpochs,
|
|
70
|
-
latest: latest === null
|
|
107
|
+
latest: latest === null
|
|
108
|
+
? null
|
|
109
|
+
: this.#view(latest, activeEpoch?.epochId ?? null),
|
|
71
110
|
history: sessions.map(historyItem),
|
|
72
111
|
};
|
|
73
112
|
});
|
|
@@ -89,6 +128,7 @@ export class WorldRevisionSession {
|
|
|
89
128
|
}
|
|
90
129
|
async status(worldId, sessionId) {
|
|
91
130
|
return this.#mutate(async () => {
|
|
131
|
+
await this.#workspace.active(worldId);
|
|
92
132
|
const sessions = await this.#store.listSessions(worldId);
|
|
93
133
|
const selectedIndex = sessionId === undefined
|
|
94
134
|
? sessions.length === 0
|
|
@@ -115,7 +155,8 @@ export class WorldRevisionSession {
|
|
|
115
155
|
return this.#mutate(async () => {
|
|
116
156
|
const session = await this.#store.readSession(sessionId);
|
|
117
157
|
assertWorld(session, worldId);
|
|
118
|
-
|
|
158
|
+
const recovered = await this.#recoverIfNeeded(session);
|
|
159
|
+
return this.#view(recovered, (await this.#workspace.active(worldId))?.epochId ?? null);
|
|
119
160
|
});
|
|
120
161
|
}
|
|
121
162
|
async deleteSession(worldId, sessionId) {
|
|
@@ -188,19 +229,30 @@ export class WorldRevisionSession {
|
|
|
188
229
|
throw new Error("Another message in this world-revision conversation is still running");
|
|
189
230
|
return { session, run: running.promise };
|
|
190
231
|
}
|
|
232
|
+
if (session.googleOutcomeUnknown)
|
|
233
|
+
throw new Error("The Google request outcome is unknown; start a fresh model context.");
|
|
234
|
+
if (session.runStatus === "paused")
|
|
235
|
+
throw new Error("Resume the paused authoring request before sending another message");
|
|
191
236
|
}
|
|
192
237
|
await this.#assertNoRunningConversation(input.worldId, session?.sessionId);
|
|
238
|
+
let host;
|
|
239
|
+
if (session !== null) {
|
|
240
|
+
host = await this.#bindExistingModelHost(session.modelBinding);
|
|
241
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
242
|
+
throw new Error("No saved model connection matches this world-revision conversation");
|
|
243
|
+
}
|
|
193
244
|
const epoch = await this.#workspace.open(input.worldId);
|
|
194
245
|
session ??= await this.#create(input.worldId, epoch, requestId);
|
|
195
246
|
if (session.epochId !== epoch.epochId) {
|
|
196
247
|
retargetSession(session, epoch, this.#locale());
|
|
197
248
|
await this.#store.saveSession(session);
|
|
198
249
|
}
|
|
199
|
-
|
|
250
|
+
host ??= await this.#bindExistingModelHost(session.modelBinding);
|
|
200
251
|
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
201
252
|
throw new Error("No saved model connection matches this world-revision conversation");
|
|
202
253
|
const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.worldId, session.modelBinding);
|
|
203
|
-
session.schemaVersion =
|
|
254
|
+
session.schemaVersion = 3;
|
|
255
|
+
delete session.retryDispatch;
|
|
204
256
|
session.requests ??= [];
|
|
205
257
|
session.requests.push({
|
|
206
258
|
requestId,
|
|
@@ -214,13 +266,127 @@ export class WorldRevisionSession {
|
|
|
214
266
|
if (preload !== null)
|
|
215
267
|
session.authorization = authoringPreloadAuthorization(session.authorization, preload.record);
|
|
216
268
|
appendAuthoringUserMessage(session, message, requestId);
|
|
269
|
+
checkpointNextAuthoringDispatch(session, "dispatching");
|
|
270
|
+
if (session.modelBinding.provider === "google_interactions")
|
|
271
|
+
session.googleOutcomeUnknown = false;
|
|
272
|
+
const boundaries = host.binding().provider === "google_interactions"
|
|
273
|
+
? await this.#validatedEpochBoundaries(session)
|
|
274
|
+
: undefined;
|
|
275
|
+
if (boundaries !== undefined &&
|
|
276
|
+
host.previewAuthoringRequest !== undefined)
|
|
277
|
+
session.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(session, requestId, boundaries));
|
|
217
278
|
await this.#store.saveSession(session);
|
|
218
279
|
return {
|
|
219
280
|
session,
|
|
220
|
-
run: this.#startRun(session, host, requestId),
|
|
281
|
+
run: this.#startRun(session, host, requestId, boundaries),
|
|
221
282
|
};
|
|
222
283
|
});
|
|
223
|
-
return prepared.run ??
|
|
284
|
+
return (prepared.run ??
|
|
285
|
+
this.#view(prepared.session, (await this.#workspace.active(input.worldId))?.epochId ?? null));
|
|
286
|
+
}
|
|
287
|
+
async editMessage(input) {
|
|
288
|
+
const message = requiredMessage(input.message);
|
|
289
|
+
const requestId = requiredRequestId(input.requestId);
|
|
290
|
+
if (input.filePolicy !== "keep" && input.filePolicy !== "revert")
|
|
291
|
+
throw new Error("Invalid file policy");
|
|
292
|
+
const prepared = await this.#mutate(async () => {
|
|
293
|
+
// Recover the epoch intent before reading the conversation it may replace.
|
|
294
|
+
let epoch = await this.#workspace.active(input.worldId);
|
|
295
|
+
let session = await this.#store.readSession(input.sessionId);
|
|
296
|
+
assertWorld(session, input.worldId);
|
|
297
|
+
session = await this.#recoverIfNeeded(session);
|
|
298
|
+
const edit = {
|
|
299
|
+
requestId,
|
|
300
|
+
messageId: input.messageId,
|
|
301
|
+
message,
|
|
302
|
+
filePolicy: input.filePolicy,
|
|
303
|
+
};
|
|
304
|
+
if (completedAuthoringEdit(session, edit))
|
|
305
|
+
return { session, run: null };
|
|
306
|
+
const running = this.#conversation.running(session.sessionId);
|
|
307
|
+
if (running !== null) {
|
|
308
|
+
if (running.requestId !== requestId)
|
|
309
|
+
throw new Error("Another message in this world-revision conversation is still running");
|
|
310
|
+
return { session, run: running.promise };
|
|
311
|
+
}
|
|
312
|
+
if (session.googleOutcomeUnknown)
|
|
313
|
+
throw new Error("The Google request outcome is unknown; start a fresh model context.");
|
|
314
|
+
await this.#assertNoRunningConversation(input.worldId, session.sessionId);
|
|
315
|
+
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
316
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
317
|
+
throw new Error("No saved model connection matches this world-revision conversation");
|
|
318
|
+
const { tail, next } = planAuthoringEdit(session, edit);
|
|
319
|
+
if (input.filePolicy === "revert") {
|
|
320
|
+
const activeChangeIds = new Set(epoch?.changes.map(({ changeSetId }) => changeSetId) ?? []);
|
|
321
|
+
if (tail.some((item) => item.kind === "tool" &&
|
|
322
|
+
!item.isError &&
|
|
323
|
+
item.changes.length > 0 &&
|
|
324
|
+
(item.changeSetId === null ||
|
|
325
|
+
!activeChangeIds.has(item.changeSetId))))
|
|
326
|
+
throw new WorldRevisionMessageEditError("sealed");
|
|
327
|
+
}
|
|
328
|
+
epoch ??= await this.#workspace.open(input.worldId);
|
|
329
|
+
const reverted = input.filePolicy === "revert"
|
|
330
|
+
? revertAuthoringChanges(epoch.files, tail)
|
|
331
|
+
: { files: epoch.files, conflicts: [] };
|
|
332
|
+
if (reverted.conflicts.length > 0)
|
|
333
|
+
throw new WorldRevisionMessageEditError("conflict", reverted.conflicts);
|
|
334
|
+
const afterFiles = reverted.files;
|
|
335
|
+
if (input.filePolicy === "revert") {
|
|
336
|
+
const inspection = inspectWorldRevisionFiles(afterFiles, epoch.baseFiles);
|
|
337
|
+
if (inspection.status !== "usable")
|
|
338
|
+
throw new WorldRevisionMessageEditError("invalid_tree", [...new Set(inspection.diagnostics.map(({ path }) => path))].sort());
|
|
339
|
+
}
|
|
340
|
+
const candidate = {
|
|
341
|
+
...epoch,
|
|
342
|
+
files: afterFiles,
|
|
343
|
+
revision: contentTreeFingerprint(afterFiles),
|
|
344
|
+
};
|
|
345
|
+
const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.worldId, session.modelBinding, candidate);
|
|
346
|
+
next.schemaVersion = 3;
|
|
347
|
+
if (session.epochId !== epoch.epochId ||
|
|
348
|
+
tail.some((item) => item.kind === "prompt_delta" &&
|
|
349
|
+
item.logicalMessages.some((entry) => entry.blocks.some((block) => block.source.startsWith("runtime:world-revision-epoch:")))))
|
|
350
|
+
retargetSession(next, epoch, this.#locale());
|
|
351
|
+
next.epochId = epoch.epochId;
|
|
352
|
+
next.requests.push({
|
|
353
|
+
requestId,
|
|
354
|
+
modelItemStart: next.modelItems.length,
|
|
355
|
+
bootstrap,
|
|
356
|
+
playPreset: structuredClone(playPreset),
|
|
357
|
+
...(preload === null
|
|
358
|
+
? {}
|
|
359
|
+
: { preload: structuredClone(preload.record) }),
|
|
360
|
+
});
|
|
361
|
+
next.authorization =
|
|
362
|
+
preload === null
|
|
363
|
+
? null
|
|
364
|
+
: authoringPreloadAuthorization(null, preload.record);
|
|
365
|
+
appendAuthoringUserMessage(next, message, requestId);
|
|
366
|
+
checkpointNextAuthoringDispatch(next, "dispatching");
|
|
367
|
+
if (next.modelBinding.provider === "google_interactions")
|
|
368
|
+
next.googleOutcomeUnknown = false;
|
|
369
|
+
const boundaries = host.binding().provider === "google_interactions"
|
|
370
|
+
? await this.#validatedEpochBoundaries(next)
|
|
371
|
+
: undefined;
|
|
372
|
+
if (boundaries !== undefined &&
|
|
373
|
+
host.previewAuthoringRequest !== undefined)
|
|
374
|
+
next.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(next, requestId, boundaries));
|
|
375
|
+
await this.#workspace.publishEdit({
|
|
376
|
+
worldId: input.worldId,
|
|
377
|
+
epochId: epoch.epochId,
|
|
378
|
+
expectedRevision: epoch.revision,
|
|
379
|
+
files: afterFiles,
|
|
380
|
+
session: next,
|
|
381
|
+
});
|
|
382
|
+
this.#changed(input.worldId, true);
|
|
383
|
+
return {
|
|
384
|
+
session: next,
|
|
385
|
+
run: this.#startRun(next, host, requestId, boundaries),
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
return (prepared.run ??
|
|
389
|
+
this.#view(prepared.session, (await this.#workspace.active(input.worldId))?.epochId ?? null));
|
|
224
390
|
}
|
|
225
391
|
/** Continues a paused conversation within the revision it paused in. */
|
|
226
392
|
async resume(input) {
|
|
@@ -244,11 +410,25 @@ export class WorldRevisionSession {
|
|
|
244
410
|
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
245
411
|
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
246
412
|
throw new Error("No saved model connection matches this world-revision conversation");
|
|
247
|
-
|
|
413
|
+
if (canRetryAuthoringDispatch(session)) {
|
|
414
|
+
session.runStatus = "running";
|
|
415
|
+
session.activeRequestId = session.requests?.at(-1)?.requestId ?? null;
|
|
416
|
+
session.lastFailure = null;
|
|
417
|
+
session.updatedAt = Date.now();
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
resumePausedAuthoringConversation(session);
|
|
421
|
+
const boundaries = host.binding().provider === "google_interactions"
|
|
422
|
+
? await this.#validatedEpochBoundaries(session)
|
|
423
|
+
: undefined;
|
|
248
424
|
await this.#store.saveSession(session);
|
|
249
|
-
return {
|
|
425
|
+
return {
|
|
426
|
+
session,
|
|
427
|
+
run: this.#startRun(session, host, requestId, boundaries),
|
|
428
|
+
};
|
|
250
429
|
});
|
|
251
|
-
return prepared.run ??
|
|
430
|
+
return (prepared.run ??
|
|
431
|
+
this.#view(prepared.session, (await this.#workspace.active(input.worldId))?.epochId ?? null));
|
|
252
432
|
}
|
|
253
433
|
async preview(worldId, sessionId) {
|
|
254
434
|
const session = sessionId === undefined ? null : await this.#store.readSession(sessionId);
|
|
@@ -263,8 +443,8 @@ export class WorldRevisionSession {
|
|
|
263
443
|
const { bootstrap } = await this.#compileCurrent(worldId, host.binding());
|
|
264
444
|
return bootstrap;
|
|
265
445
|
}
|
|
266
|
-
async #compileCurrent(worldId, modelBinding) {
|
|
267
|
-
const epoch = await this.#workspace.active(worldId);
|
|
446
|
+
async #compileCurrent(worldId, modelBinding, overrideEpoch) {
|
|
447
|
+
const epoch = overrideEpoch ?? (await this.#workspace.active(worldId));
|
|
268
448
|
const worlds = await this.#worlds.listWorlds();
|
|
269
449
|
const playPreset = await this.#bindPlayPreset();
|
|
270
450
|
const locale = this.#locale();
|
|
@@ -361,7 +541,8 @@ export class WorldRevisionSession {
|
|
|
361
541
|
});
|
|
362
542
|
const now = Date.now();
|
|
363
543
|
const session = {
|
|
364
|
-
schemaVersion:
|
|
544
|
+
schemaVersion: 3,
|
|
545
|
+
requests: [],
|
|
365
546
|
sessionId: this.#store.createSessionId(),
|
|
366
547
|
worldId,
|
|
367
548
|
worldTitle,
|
|
@@ -385,27 +566,58 @@ export class WorldRevisionSession {
|
|
|
385
566
|
authorization: null,
|
|
386
567
|
pendingSettlement: null,
|
|
387
568
|
};
|
|
388
|
-
await this.#store.saveSession(session);
|
|
389
569
|
return session;
|
|
390
570
|
}
|
|
391
|
-
#startRun(session, host, requestId) {
|
|
571
|
+
#startRun(session, host, requestId, worldRevisionBoundaries) {
|
|
392
572
|
let activeTransaction = null;
|
|
393
573
|
return this.#conversation.run({
|
|
394
574
|
changed: (durable) => this.#changed(session.worldId, durable),
|
|
395
575
|
session,
|
|
396
576
|
host,
|
|
397
577
|
requestId,
|
|
578
|
+
exchangeRequestId: session.activeRequestId ?? requestId,
|
|
579
|
+
...(worldRevisionBoundaries === undefined
|
|
580
|
+
? {}
|
|
581
|
+
: { worldRevisionBoundaries }),
|
|
398
582
|
save: (next) => this.#store.saveSession(next),
|
|
583
|
+
recordGoogleHop: (id, hop) => this.#store.recordGoogleHop(session.sessionId, id, hop),
|
|
399
584
|
settleToolResponse: async (next, assistantItemIndex, calls) => {
|
|
400
585
|
activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, calls, activeTransaction);
|
|
401
586
|
},
|
|
402
587
|
view: (next) => this.#view(next),
|
|
588
|
+
retrySupported: session.schemaVersion === 3,
|
|
403
589
|
failure: {
|
|
404
590
|
cancelled: "The world-revision response was cancelled. Settled worktree changes were retained.",
|
|
405
591
|
interrupted: "The world-revision response was interrupted",
|
|
406
592
|
},
|
|
407
593
|
});
|
|
408
594
|
}
|
|
595
|
+
async #validatedEpochBoundaries(session) {
|
|
596
|
+
const entries = [];
|
|
597
|
+
for (const [itemIndex, item] of session.modelItems.entries()) {
|
|
598
|
+
if (item.kind !== "prompt_delta")
|
|
599
|
+
continue;
|
|
600
|
+
const source = item.logicalMessages[0]?.blocks[0]?.source;
|
|
601
|
+
if (source?.startsWith("runtime:world-revision-epoch:") !== true)
|
|
602
|
+
continue;
|
|
603
|
+
const epochId = source.slice("runtime:world-revision-epoch:".length);
|
|
604
|
+
const epoch = await this.#store.readEpoch(epochId);
|
|
605
|
+
if (epoch.worldId !== session.worldId)
|
|
606
|
+
throw new Error("World-revision epoch boundary belongs to another world");
|
|
607
|
+
const valid = ["en", "zh-CN"].some((locale) => isDeepStrictEqual(item, epochBoundaryItem(epoch, locale)));
|
|
608
|
+
if (!valid)
|
|
609
|
+
throw new Error("World-revision epoch boundary is damaged");
|
|
610
|
+
entries.push({
|
|
611
|
+
itemIndex,
|
|
612
|
+
epochId,
|
|
613
|
+
baseHead: epoch.baseHead,
|
|
614
|
+
markdown: item.logicalMessages[0].markdown,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (entries.length > 0 && entries.at(-1)?.epochId !== session.epochId)
|
|
618
|
+
throw new Error("World-revision epoch boundary does not match the current epoch");
|
|
619
|
+
return { currentEpochId: session.epochId, entries };
|
|
620
|
+
}
|
|
409
621
|
async #settleToolResponse(session, assistantItemIndex, calls, active) {
|
|
410
622
|
const epoch = await this.#requiredActiveEpoch(session);
|
|
411
623
|
const beforeRevision = epoch.revision;
|
|
@@ -455,6 +667,8 @@ export class WorldRevisionSession {
|
|
|
455
667
|
toolResults,
|
|
456
668
|
});
|
|
457
669
|
finalizeSettlement(session, prepared);
|
|
670
|
+
if (session.schemaVersion === 3)
|
|
671
|
+
checkpointNextAuthoringDispatch(session, "dispatching");
|
|
458
672
|
session.updatedAt = Date.now();
|
|
459
673
|
await this.#store.saveSession(session);
|
|
460
674
|
return { revision: afterRevision, transaction };
|
|
@@ -512,27 +726,66 @@ export class WorldRevisionSession {
|
|
|
512
726
|
}
|
|
513
727
|
}
|
|
514
728
|
async #recoverIfNeeded(session) {
|
|
729
|
+
if (session.damagedContinuation)
|
|
730
|
+
return session;
|
|
515
731
|
if (this.#conversation.isRunning(session.sessionId))
|
|
516
732
|
return session;
|
|
733
|
+
await this.#workspace.active(session.worldId);
|
|
734
|
+
session = await this.#store.readSession(session.sessionId);
|
|
735
|
+
const lastItem = session.modelItems.at(-1);
|
|
736
|
+
if (session.pendingSettlement === null &&
|
|
737
|
+
session.modelBinding.provider === "google_interactions" &&
|
|
738
|
+
session.runStatus === "interrupted" &&
|
|
739
|
+
session.googleOutcomeUnknown === undefined &&
|
|
740
|
+
(lastItem?.kind === "user" || lastItem?.kind === "tool")) {
|
|
741
|
+
session.googleOutcomeUnknown = true;
|
|
742
|
+
session.lastFailure =
|
|
743
|
+
"The Google request outcome is unknown; start a fresh model context.";
|
|
744
|
+
session.updatedAt = Date.now();
|
|
745
|
+
await this.#store.saveSession(session);
|
|
746
|
+
return session;
|
|
747
|
+
}
|
|
748
|
+
if (session.pendingSettlement === null &&
|
|
749
|
+
lastItem?.kind === "assistant" &&
|
|
750
|
+
lastItem.toolCalls.length > 0) {
|
|
751
|
+
finalizeUnconfirmed(session, session.modelItems.length - 1, new Error("Runtime found a complete tool response without a persisted worktree settlement intent"));
|
|
752
|
+
if (session.modelBinding.provider === "google_interactions")
|
|
753
|
+
session.googleOutcomeUnknown = false;
|
|
754
|
+
const requestId = session.activeRequestId;
|
|
755
|
+
session.runStatus = "interrupted";
|
|
756
|
+
session.activeRequestId = null;
|
|
757
|
+
if (requestId !== null)
|
|
758
|
+
session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
|
|
759
|
+
session.lastFailure =
|
|
760
|
+
"The complete tool response could not be settled; its calls received failure receipts. Re-read the worktree before editing.";
|
|
761
|
+
session.updatedAt = Date.now();
|
|
762
|
+
await this.#store.saveSession(session);
|
|
763
|
+
return session;
|
|
764
|
+
}
|
|
765
|
+
if (session.runStatus === "running" &&
|
|
766
|
+
session.pendingSettlement === null &&
|
|
767
|
+
session.modelBinding.provider === "google_interactions" &&
|
|
768
|
+
session.googleOutcomeUnknown !== false)
|
|
769
|
+
session.googleOutcomeUnknown = true;
|
|
770
|
+
const recoveringSettlement = session.pendingSettlement !== null;
|
|
771
|
+
let settlementPublished = false;
|
|
517
772
|
if (session.pendingSettlement !== null) {
|
|
773
|
+
if (session.modelBinding.provider === "google_interactions")
|
|
774
|
+
session.googleOutcomeUnknown = false;
|
|
518
775
|
const pending = session.pendingSettlement;
|
|
519
776
|
if (pending.phase === "publication_prepared") {
|
|
520
777
|
const active = await this.#workspace.active(session.worldId);
|
|
521
778
|
if (active?.epochId === pending.epochId) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
catch (error) {
|
|
534
|
-
finalizeUnconfirmed(session, pending.assistantItemIndex, error);
|
|
535
|
-
}
|
|
779
|
+
await this.#workspace.publishAi({
|
|
780
|
+
worldId: session.worldId,
|
|
781
|
+
epochId: pending.epochId,
|
|
782
|
+
expectedRevision: pending.beforeRevision,
|
|
783
|
+
afterRevision: pending.afterRevision,
|
|
784
|
+
files: pending.afterFiles,
|
|
785
|
+
toolResults: pending.toolResults,
|
|
786
|
+
});
|
|
787
|
+
finalizeSettlement(session, pending);
|
|
788
|
+
settlementPublished = true;
|
|
536
789
|
}
|
|
537
790
|
else
|
|
538
791
|
finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("The revision epoch ended before tool publication recovered"));
|
|
@@ -540,15 +793,27 @@ export class WorldRevisionSession {
|
|
|
540
793
|
else
|
|
541
794
|
finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("Runtime restarted before the complete tool response was prepared"));
|
|
542
795
|
}
|
|
543
|
-
if (session.runStatus !== "running" &&
|
|
796
|
+
if (session.runStatus !== "running" && !recoveringSettlement)
|
|
544
797
|
return session;
|
|
545
798
|
const requestId = session.activeRequestId;
|
|
546
|
-
|
|
799
|
+
if (settlementPublished &&
|
|
800
|
+
session.modelBinding.provider !== "google_interactions" &&
|
|
801
|
+
session.requests?.at(-1) !== undefined) {
|
|
802
|
+
session.schemaVersion = 3;
|
|
803
|
+
checkpointNextAuthoringDispatch(session, "retryable");
|
|
804
|
+
}
|
|
805
|
+
recoverAuthoringDispatch(session);
|
|
806
|
+
session.runStatus =
|
|
807
|
+
session.modelBinding.provider === "google_interactions" &&
|
|
808
|
+
session.googleOutcomeUnknown === false
|
|
809
|
+
? "paused"
|
|
810
|
+
: "interrupted";
|
|
547
811
|
session.activeRequestId = null;
|
|
548
812
|
if (requestId !== null)
|
|
549
813
|
session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
|
|
550
|
-
session.lastFailure =
|
|
551
|
-
"The
|
|
814
|
+
session.lastFailure = session.googleOutcomeUnknown
|
|
815
|
+
? "The Google request outcome is unknown; start a fresh model context."
|
|
816
|
+
: "The previous model request stopped after its last settled worktree change. Continue this conversation when ready.";
|
|
552
817
|
session.updatedAt = Date.now();
|
|
553
818
|
await this.#store.saveSession(session);
|
|
554
819
|
return session;
|
|
@@ -571,7 +836,7 @@ export class WorldRevisionSession {
|
|
|
571
836
|
this.#conversation.isRunning(sessionId)))
|
|
572
837
|
throw new WorldRevisionWorkspaceError("Wait for or cancel the running AI response before changing the revision");
|
|
573
838
|
}
|
|
574
|
-
#view(session) {
|
|
839
|
+
#view(session, activeEpochId = session.epochId) {
|
|
575
840
|
const streaming = session.runStatus === "running"
|
|
576
841
|
? this.#conversation.streaming(session.sessionId)
|
|
577
842
|
: null;
|
|
@@ -591,6 +856,7 @@ export class WorldRevisionSession {
|
|
|
591
856
|
updatedAt: session.updatedAt,
|
|
592
857
|
},
|
|
593
858
|
lastFailure: session.lastFailure,
|
|
859
|
+
canRetry: activeEpochId === session.epochId && canRetryAuthoringDispatch(session),
|
|
594
860
|
};
|
|
595
861
|
}
|
|
596
862
|
#statusView(session) {
|
|
@@ -609,9 +875,9 @@ export class WorldRevisionSession {
|
|
|
609
875
|
};
|
|
610
876
|
}
|
|
611
877
|
async #mutate(operation) {
|
|
612
|
-
const previous = this.#mutationTail;
|
|
878
|
+
const previous = this.#live.mutationTail;
|
|
613
879
|
let release = () => undefined;
|
|
614
|
-
this.#mutationTail = new Promise((resolveMutation) => {
|
|
880
|
+
this.#live.mutationTail = new Promise((resolveMutation) => {
|
|
615
881
|
release = resolveMutation;
|
|
616
882
|
});
|
|
617
883
|
await previous;
|
|
@@ -624,10 +890,18 @@ export class WorldRevisionSession {
|
|
|
624
890
|
}
|
|
625
891
|
}
|
|
626
892
|
function retargetSession(session, epoch, locale) {
|
|
893
|
+
session.modelItems.push(epochBoundaryItem(epoch, locale));
|
|
894
|
+
session.epochId = epoch.epochId;
|
|
895
|
+
session.authorization = null;
|
|
896
|
+
session.pendingSettlement = null;
|
|
897
|
+
session.lastFailure = null;
|
|
898
|
+
session.updatedAt = Date.now();
|
|
899
|
+
}
|
|
900
|
+
function epochBoundaryItem(epoch, locale) {
|
|
627
901
|
const markdown = locale === "zh-CN"
|
|
628
902
|
? `# 新的世界修订 epoch\n\n上一轮修订已经应用或放弃。当前工作树是 ${epoch.epochId},从世界端点 ${epoch.baseHead} 重新建立。此前全部 search cursor 和 read 写授权已失效;修改前必须重新读取。`
|
|
629
903
|
: `# New world-revision epoch\n\nThe previous revision was applied or discarded. Worktree ${epoch.epochId} was rebuilt from world endpoint ${epoch.baseHead}. Every prior search cursor and read authorization is invalid; re-read before writing.`;
|
|
630
|
-
|
|
904
|
+
return {
|
|
631
905
|
kind: "prompt_delta",
|
|
632
906
|
logicalMessages: [
|
|
633
907
|
{
|
|
@@ -641,12 +915,7 @@ function retargetSession(session, epoch, locale) {
|
|
|
641
915
|
],
|
|
642
916
|
},
|
|
643
917
|
],
|
|
644
|
-
}
|
|
645
|
-
session.epochId = epoch.epochId;
|
|
646
|
-
session.authorization = null;
|
|
647
|
-
session.pendingSettlement = null;
|
|
648
|
-
session.lastFailure = null;
|
|
649
|
-
session.updatedAt = Date.now();
|
|
918
|
+
};
|
|
650
919
|
}
|
|
651
920
|
function finalizeSettlement(session, pending) {
|
|
652
921
|
for (const result of pending.toolResults)
|
|
@@ -745,6 +1014,11 @@ function historyItem(session) {
|
|
|
745
1014
|
const changed = new Set(session.modelItems.flatMap((item) => item.kind === "tool" ? item.changes.map(({ path }) => path) : []));
|
|
746
1015
|
return {
|
|
747
1016
|
sessionId: session.sessionId,
|
|
1017
|
+
...(session.damagedContinuation
|
|
1018
|
+
? {
|
|
1019
|
+
unavailableReason: "Google continuation is damaged; start a fresh model context.",
|
|
1020
|
+
}
|
|
1021
|
+
: {}),
|
|
748
1022
|
epochId: session.epochId,
|
|
749
1023
|
runStatus: session.runStatus,
|
|
750
1024
|
createdAt: session.createdAt,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
3
4
|
import { readDeclaredWorldClock } from "../prompt/WorldMaintenanceReport.js";
|
|
4
5
|
import { WorldOperationClaimLostError } from "../world/WorldOperationCoordinator.js";
|
|
@@ -125,6 +126,47 @@ export class WorldRevisionWorkspace {
|
|
|
125
126
|
return epoch;
|
|
126
127
|
});
|
|
127
128
|
}
|
|
129
|
+
async publishEdit(input) {
|
|
130
|
+
const handle = revisionHandle(input);
|
|
131
|
+
await this.#withMutableEpoch(handle, async (epoch) => {
|
|
132
|
+
assertExpectedRevision(epoch, input.expectedRevision);
|
|
133
|
+
const files = cloneFiles(input.files);
|
|
134
|
+
assertUsableWorldRevisionFiles(files, epoch.baseFiles);
|
|
135
|
+
const changes = fileDiff(epoch.files, files);
|
|
136
|
+
if (changes.length > 0)
|
|
137
|
+
epoch.changes.push({
|
|
138
|
+
changeSetId: this.#store.createChangeSetId(),
|
|
139
|
+
source: "rollback",
|
|
140
|
+
createdAt: Date.now(),
|
|
141
|
+
changes,
|
|
142
|
+
});
|
|
143
|
+
epoch.files = files;
|
|
144
|
+
epoch.revision = contentTreeFingerprint(files);
|
|
145
|
+
epoch.updatedAt = Date.now();
|
|
146
|
+
await this.#store.prepareEdit({
|
|
147
|
+
beforeRevision: input.expectedRevision,
|
|
148
|
+
epoch,
|
|
149
|
+
session: input.session,
|
|
150
|
+
});
|
|
151
|
+
await this.#finishEdit(handle);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async #finishEdit(handle) {
|
|
155
|
+
const pending = await this.#store.readEdit(handle.epochId);
|
|
156
|
+
if (pending === null)
|
|
157
|
+
return;
|
|
158
|
+
if (pending.epoch.worldId !== handle.worldId)
|
|
159
|
+
throw new WorldRevisionWorkspaceError("Pending edit belongs to another world");
|
|
160
|
+
const current = await this.#store.readEpoch(handle.epochId);
|
|
161
|
+
if (!isDeepStrictEqual(current, pending.epoch)) {
|
|
162
|
+
if (current.lifecycle !== "active" ||
|
|
163
|
+
current.revision !== pending.beforeRevision)
|
|
164
|
+
throw new WorldRevisionWorkspaceError("Pending world-revision edit conflicts with the worktree");
|
|
165
|
+
await this.#store.saveEpoch(pending.epoch);
|
|
166
|
+
}
|
|
167
|
+
await this.#store.saveSession(pending.session);
|
|
168
|
+
await this.#store.removeEdit(handle.epochId);
|
|
169
|
+
}
|
|
128
170
|
async rollback(input) {
|
|
129
171
|
const handle = revisionHandle(input);
|
|
130
172
|
return this.#withMutableEpoch(handle, async (epoch) => {
|
|
@@ -227,6 +269,7 @@ export class WorldRevisionWorkspace {
|
|
|
227
269
|
};
|
|
228
270
|
}
|
|
229
271
|
async #resumeLocked(handle) {
|
|
272
|
+
await this.#withRevisionOwnership(handle, () => Promise.resolve());
|
|
230
273
|
let epoch = await this.#store.readEpochIfPresent(handle.epochId);
|
|
231
274
|
if (epoch === null) {
|
|
232
275
|
const binding = await this.#worlds.bindWorldRevision(handle);
|
|
@@ -336,7 +379,10 @@ export class WorldRevisionWorkspace {
|
|
|
336
379
|
}
|
|
337
380
|
async #withRevisionOwnership(handle, action) {
|
|
338
381
|
try {
|
|
339
|
-
return await this.#worlds.operations.withWorldRevisionLock(handle,
|
|
382
|
+
return await this.#worlds.operations.withWorldRevisionLock(handle, async () => {
|
|
383
|
+
await this.#finishEdit(handle);
|
|
384
|
+
return action();
|
|
385
|
+
});
|
|
340
386
|
}
|
|
341
387
|
catch (error) {
|
|
342
388
|
if (error instanceof WorldOperationClaimLostError)
|