narraeon 0.8.0 → 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/v1.js +22 -0
- package/dist/node/runtime/V1Runtime.js +34 -2
- package/dist/node/runtime/authoring/AuthoringEditPlan.js +79 -0
- package/dist/node/runtime/authoring/AuthoringRetry.js +59 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +22 -6
- package/dist/node/runtime/content/ContentWorkspace.js +7 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +14 -8
- package/dist/node/runtime/model/DispatchFailure.js +8 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +17 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +49 -0
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +18 -14
- package/dist/node/runtime/play/PlayCallChain.js +16 -4
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +210 -8
- package/dist/node/runtime/setting/SettingImprovementSession.js +247 -24
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +154 -9
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +199 -33
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +47 -1
- package/dist/web/assets/index-BthiDIQu.js +1432 -0
- package/dist/web/assets/{index-BKA1D0Rz.css → index-wO9Y4HT1.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-C_Qta8UL.js +0 -1432
|
@@ -1,3 +1,5 @@
|
|
|
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";
|
|
3
5
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -8,9 +10,28 @@ import { equalModelHostBinding } from "../model/ModelHost.js";
|
|
|
8
10
|
import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
9
11
|
import { authoringPreloadAuthorization, buildAuthoringPreload, worldRevisionAuthoringSurface, } from "../setting/SettingAuthoringTransaction.js";
|
|
10
12
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
11
|
-
import { WorldRevisionAuthoringTransaction, worldRevisionRuntimeContract, worldRevisionToolDefinitions, } from "./WorldRevisionAuthoringTransaction.js";
|
|
13
|
+
import { WorldRevisionAuthoringTransaction, inspectWorldRevisionFiles, worldRevisionRuntimeContract, worldRevisionToolDefinitions, } from "./WorldRevisionAuthoringTransaction.js";
|
|
12
14
|
import { WorldRevisionWorkspaceError } from "./WorldRevisionWorkspace.js";
|
|
13
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
|
+
});
|
|
14
35
|
/** Conversation orchestration over the shared locked revision worktree. */
|
|
15
36
|
export class WorldRevisionSession {
|
|
16
37
|
#store;
|
|
@@ -22,9 +43,9 @@ export class WorldRevisionSession {
|
|
|
22
43
|
#bindExistingModelHost;
|
|
23
44
|
#bindPlayPreset;
|
|
24
45
|
#preview;
|
|
25
|
-
#
|
|
46
|
+
#live;
|
|
47
|
+
#conversation;
|
|
26
48
|
#changed;
|
|
27
|
-
#mutationTail = Promise.resolve();
|
|
28
49
|
constructor(input) {
|
|
29
50
|
this.#changed =
|
|
30
51
|
input.changed ??
|
|
@@ -32,6 +53,21 @@ export class WorldRevisionSession {
|
|
|
32
53
|
/* No observers attached. */
|
|
33
54
|
});
|
|
34
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;
|
|
35
71
|
this.#workspace = input.workspace;
|
|
36
72
|
this.#worlds = input.worlds;
|
|
37
73
|
this.#compiler = input.compiler;
|
|
@@ -56,9 +92,9 @@ export class WorldRevisionSession {
|
|
|
56
92
|
}
|
|
57
93
|
async overview(worldId) {
|
|
58
94
|
return this.#mutate(async () => {
|
|
59
|
-
const
|
|
60
|
-
const [
|
|
61
|
-
this.#
|
|
95
|
+
const sessions = await this.#recoverWorldSessions(worldId);
|
|
96
|
+
const [activeEpoch, sealedEpochs] = await Promise.all([
|
|
97
|
+
this.#workspace.active(worldId),
|
|
62
98
|
this.#workspace.sealed(worldId),
|
|
63
99
|
]);
|
|
64
100
|
const latest = sessions.find((session) => !session.damagedContinuation) ?? null;
|
|
@@ -68,7 +104,9 @@ export class WorldRevisionSession {
|
|
|
68
104
|
? null
|
|
69
105
|
: this.#workspace.view(shownEpoch, activeEpoch?.epochId === shownEpoch.epochId),
|
|
70
106
|
sealedEpochs,
|
|
71
|
-
latest: latest === null
|
|
107
|
+
latest: latest === null
|
|
108
|
+
? null
|
|
109
|
+
: this.#view(latest, activeEpoch?.epochId ?? null),
|
|
72
110
|
history: sessions.map(historyItem),
|
|
73
111
|
};
|
|
74
112
|
});
|
|
@@ -90,6 +128,7 @@ export class WorldRevisionSession {
|
|
|
90
128
|
}
|
|
91
129
|
async status(worldId, sessionId) {
|
|
92
130
|
return this.#mutate(async () => {
|
|
131
|
+
await this.#workspace.active(worldId);
|
|
93
132
|
const sessions = await this.#store.listSessions(worldId);
|
|
94
133
|
const selectedIndex = sessionId === undefined
|
|
95
134
|
? sessions.length === 0
|
|
@@ -116,7 +155,8 @@ export class WorldRevisionSession {
|
|
|
116
155
|
return this.#mutate(async () => {
|
|
117
156
|
const session = await this.#store.readSession(sessionId);
|
|
118
157
|
assertWorld(session, worldId);
|
|
119
|
-
|
|
158
|
+
const recovered = await this.#recoverIfNeeded(session);
|
|
159
|
+
return this.#view(recovered, (await this.#workspace.active(worldId))?.epochId ?? null);
|
|
120
160
|
});
|
|
121
161
|
}
|
|
122
162
|
async deleteSession(worldId, sessionId) {
|
|
@@ -211,7 +251,8 @@ export class WorldRevisionSession {
|
|
|
211
251
|
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
212
252
|
throw new Error("No saved model connection matches this world-revision conversation");
|
|
213
253
|
const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.worldId, session.modelBinding);
|
|
214
|
-
session.schemaVersion =
|
|
254
|
+
session.schemaVersion = 3;
|
|
255
|
+
delete session.retryDispatch;
|
|
215
256
|
session.requests ??= [];
|
|
216
257
|
session.requests.push({
|
|
217
258
|
requestId,
|
|
@@ -225,6 +266,7 @@ export class WorldRevisionSession {
|
|
|
225
266
|
if (preload !== null)
|
|
226
267
|
session.authorization = authoringPreloadAuthorization(session.authorization, preload.record);
|
|
227
268
|
appendAuthoringUserMessage(session, message, requestId);
|
|
269
|
+
checkpointNextAuthoringDispatch(session, "dispatching");
|
|
228
270
|
if (session.modelBinding.provider === "google_interactions")
|
|
229
271
|
session.googleOutcomeUnknown = false;
|
|
230
272
|
const boundaries = host.binding().provider === "google_interactions"
|
|
@@ -239,7 +281,112 @@ export class WorldRevisionSession {
|
|
|
239
281
|
run: this.#startRun(session, host, requestId, boundaries),
|
|
240
282
|
};
|
|
241
283
|
});
|
|
242
|
-
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));
|
|
243
390
|
}
|
|
244
391
|
/** Continues a paused conversation within the revision it paused in. */
|
|
245
392
|
async resume(input) {
|
|
@@ -263,7 +410,14 @@ export class WorldRevisionSession {
|
|
|
263
410
|
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
264
411
|
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
265
412
|
throw new Error("No saved model connection matches this world-revision conversation");
|
|
266
|
-
|
|
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);
|
|
267
421
|
const boundaries = host.binding().provider === "google_interactions"
|
|
268
422
|
? await this.#validatedEpochBoundaries(session)
|
|
269
423
|
: undefined;
|
|
@@ -273,7 +427,8 @@ export class WorldRevisionSession {
|
|
|
273
427
|
run: this.#startRun(session, host, requestId, boundaries),
|
|
274
428
|
};
|
|
275
429
|
});
|
|
276
|
-
return prepared.run ??
|
|
430
|
+
return (prepared.run ??
|
|
431
|
+
this.#view(prepared.session, (await this.#workspace.active(input.worldId))?.epochId ?? null));
|
|
277
432
|
}
|
|
278
433
|
async preview(worldId, sessionId) {
|
|
279
434
|
const session = sessionId === undefined ? null : await this.#store.readSession(sessionId);
|
|
@@ -288,8 +443,8 @@ export class WorldRevisionSession {
|
|
|
288
443
|
const { bootstrap } = await this.#compileCurrent(worldId, host.binding());
|
|
289
444
|
return bootstrap;
|
|
290
445
|
}
|
|
291
|
-
async #compileCurrent(worldId, modelBinding) {
|
|
292
|
-
const epoch = await this.#workspace.active(worldId);
|
|
446
|
+
async #compileCurrent(worldId, modelBinding, overrideEpoch) {
|
|
447
|
+
const epoch = overrideEpoch ?? (await this.#workspace.active(worldId));
|
|
293
448
|
const worlds = await this.#worlds.listWorlds();
|
|
294
449
|
const playPreset = await this.#bindPlayPreset();
|
|
295
450
|
const locale = this.#locale();
|
|
@@ -386,7 +541,8 @@ export class WorldRevisionSession {
|
|
|
386
541
|
});
|
|
387
542
|
const now = Date.now();
|
|
388
543
|
const session = {
|
|
389
|
-
schemaVersion:
|
|
544
|
+
schemaVersion: 3,
|
|
545
|
+
requests: [],
|
|
390
546
|
sessionId: this.#store.createSessionId(),
|
|
391
547
|
worldId,
|
|
392
548
|
worldTitle,
|
|
@@ -410,7 +566,6 @@ export class WorldRevisionSession {
|
|
|
410
566
|
authorization: null,
|
|
411
567
|
pendingSettlement: null,
|
|
412
568
|
};
|
|
413
|
-
await this.#store.saveSession(session);
|
|
414
569
|
return session;
|
|
415
570
|
}
|
|
416
571
|
#startRun(session, host, requestId, worldRevisionBoundaries) {
|
|
@@ -420,6 +575,7 @@ export class WorldRevisionSession {
|
|
|
420
575
|
session,
|
|
421
576
|
host,
|
|
422
577
|
requestId,
|
|
578
|
+
exchangeRequestId: session.activeRequestId ?? requestId,
|
|
423
579
|
...(worldRevisionBoundaries === undefined
|
|
424
580
|
? {}
|
|
425
581
|
: { worldRevisionBoundaries }),
|
|
@@ -429,6 +585,7 @@ export class WorldRevisionSession {
|
|
|
429
585
|
activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, calls, activeTransaction);
|
|
430
586
|
},
|
|
431
587
|
view: (next) => this.#view(next),
|
|
588
|
+
retrySupported: session.schemaVersion === 3,
|
|
432
589
|
failure: {
|
|
433
590
|
cancelled: "The world-revision response was cancelled. Settled worktree changes were retained.",
|
|
434
591
|
interrupted: "The world-revision response was interrupted",
|
|
@@ -510,6 +667,8 @@ export class WorldRevisionSession {
|
|
|
510
667
|
toolResults,
|
|
511
668
|
});
|
|
512
669
|
finalizeSettlement(session, prepared);
|
|
670
|
+
if (session.schemaVersion === 3)
|
|
671
|
+
checkpointNextAuthoringDispatch(session, "dispatching");
|
|
513
672
|
session.updatedAt = Date.now();
|
|
514
673
|
await this.#store.saveSession(session);
|
|
515
674
|
return { revision: afterRevision, transaction };
|
|
@@ -571,6 +730,8 @@ export class WorldRevisionSession {
|
|
|
571
730
|
return session;
|
|
572
731
|
if (this.#conversation.isRunning(session.sessionId))
|
|
573
732
|
return session;
|
|
733
|
+
await this.#workspace.active(session.worldId);
|
|
734
|
+
session = await this.#store.readSession(session.sessionId);
|
|
574
735
|
const lastItem = session.modelItems.at(-1);
|
|
575
736
|
if (session.pendingSettlement === null &&
|
|
576
737
|
session.modelBinding.provider === "google_interactions" &&
|
|
@@ -607,6 +768,7 @@ export class WorldRevisionSession {
|
|
|
607
768
|
session.googleOutcomeUnknown !== false)
|
|
608
769
|
session.googleOutcomeUnknown = true;
|
|
609
770
|
const recoveringSettlement = session.pendingSettlement !== null;
|
|
771
|
+
let settlementPublished = false;
|
|
610
772
|
if (session.pendingSettlement !== null) {
|
|
611
773
|
if (session.modelBinding.provider === "google_interactions")
|
|
612
774
|
session.googleOutcomeUnknown = false;
|
|
@@ -614,20 +776,16 @@ export class WorldRevisionSession {
|
|
|
614
776
|
if (pending.phase === "publication_prepared") {
|
|
615
777
|
const active = await this.#workspace.active(session.worldId);
|
|
616
778
|
if (active?.epochId === pending.epochId) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
finalizeUnconfirmed(session, pending.assistantItemIndex, error);
|
|
630
|
-
}
|
|
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;
|
|
631
789
|
}
|
|
632
790
|
else
|
|
633
791
|
finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("The revision epoch ended before tool publication recovered"));
|
|
@@ -638,6 +796,13 @@ export class WorldRevisionSession {
|
|
|
638
796
|
if (session.runStatus !== "running" && !recoveringSettlement)
|
|
639
797
|
return session;
|
|
640
798
|
const requestId = session.activeRequestId;
|
|
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);
|
|
641
806
|
session.runStatus =
|
|
642
807
|
session.modelBinding.provider === "google_interactions" &&
|
|
643
808
|
session.googleOutcomeUnknown === false
|
|
@@ -671,7 +836,7 @@ export class WorldRevisionSession {
|
|
|
671
836
|
this.#conversation.isRunning(sessionId)))
|
|
672
837
|
throw new WorldRevisionWorkspaceError("Wait for or cancel the running AI response before changing the revision");
|
|
673
838
|
}
|
|
674
|
-
#view(session) {
|
|
839
|
+
#view(session, activeEpochId = session.epochId) {
|
|
675
840
|
const streaming = session.runStatus === "running"
|
|
676
841
|
? this.#conversation.streaming(session.sessionId)
|
|
677
842
|
: null;
|
|
@@ -691,6 +856,7 @@ export class WorldRevisionSession {
|
|
|
691
856
|
updatedAt: session.updatedAt,
|
|
692
857
|
},
|
|
693
858
|
lastFailure: session.lastFailure,
|
|
859
|
+
canRetry: activeEpochId === session.epochId && canRetryAuthoringDispatch(session),
|
|
694
860
|
};
|
|
695
861
|
}
|
|
696
862
|
#statusView(session) {
|
|
@@ -709,9 +875,9 @@ export class WorldRevisionSession {
|
|
|
709
875
|
};
|
|
710
876
|
}
|
|
711
877
|
async #mutate(operation) {
|
|
712
|
-
const previous = this.#mutationTail;
|
|
878
|
+
const previous = this.#live.mutationTail;
|
|
713
879
|
let release = () => undefined;
|
|
714
|
-
this.#mutationTail = new Promise((resolveMutation) => {
|
|
880
|
+
this.#live.mutationTail = new Promise((resolveMutation) => {
|
|
715
881
|
release = resolveMutation;
|
|
716
882
|
});
|
|
717
883
|
await previous;
|
|
@@ -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)
|