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,4 +1,8 @@
|
|
|
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";
|
|
4
|
+
import { readGameplay } from "../../protocol/gameplay.js";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
6
|
import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
|
|
3
7
|
import { InvalidContentTreeError, } from "../content/ContentWorkspace.js";
|
|
4
8
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
@@ -10,12 +14,31 @@ import { summarizeStoredSettingImprovementSession, } from "./FileNativeSettingIm
|
|
|
10
14
|
import { SettingAuthoringTransaction, settingImprovementRuntimeContract, settingImprovementToolDefinitions, authoringPreloadAuthorization, buildAuthoringPreload, contentPackageAuthoringSurface, } from "./SettingAuthoringTransaction.js";
|
|
11
15
|
class RecoveryWriteConflictError extends Error {
|
|
12
16
|
}
|
|
17
|
+
const liveConversations = new Map();
|
|
18
|
+
const releaseConversation = new FinalizationRegistry(({ scope, reference }) => {
|
|
19
|
+
if (liveConversations.get(scope) === reference)
|
|
20
|
+
liveConversations.delete(scope);
|
|
21
|
+
});
|
|
13
22
|
export class SettingImprovementRollbackError extends Error {
|
|
14
23
|
constructor(message, options) {
|
|
15
24
|
super(message, options);
|
|
16
25
|
this.name = "SettingImprovementRollbackError";
|
|
17
26
|
}
|
|
18
27
|
}
|
|
28
|
+
export class SettingMessageEditError extends Error {
|
|
29
|
+
reason;
|
|
30
|
+
paths;
|
|
31
|
+
constructor(reason, paths) {
|
|
32
|
+
super(reason === "conflict"
|
|
33
|
+
? `Cannot revert changed files: ${paths.join(", ")}`
|
|
34
|
+
: reason === "invalid_tree"
|
|
35
|
+
? `Reverting would make these files invalid: ${paths.join(", ")}`
|
|
36
|
+
: `Cannot attribute older file changes to this message: ${paths.join(", ")}`);
|
|
37
|
+
this.name = "SettingMessageEditError";
|
|
38
|
+
this.reason = reason;
|
|
39
|
+
this.paths = paths;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
export class SettingImprovementSession {
|
|
20
43
|
#store;
|
|
21
44
|
#content;
|
|
@@ -25,7 +48,7 @@ export class SettingImprovementSession {
|
|
|
25
48
|
#bindExistingModelHost;
|
|
26
49
|
#bindPlayPreset;
|
|
27
50
|
#preview;
|
|
28
|
-
#conversation
|
|
51
|
+
#conversation;
|
|
29
52
|
#changed;
|
|
30
53
|
#mutationTail = Promise.resolve();
|
|
31
54
|
constructor(input) {
|
|
@@ -35,6 +58,17 @@ export class SettingImprovementSession {
|
|
|
35
58
|
/* No observers attached. */
|
|
36
59
|
});
|
|
37
60
|
this.#store = input.store;
|
|
61
|
+
let shared = liveConversations.get(input.store.sessionScope)?.deref();
|
|
62
|
+
if (shared === undefined) {
|
|
63
|
+
shared = new ModelToolConversation();
|
|
64
|
+
const reference = new WeakRef(shared);
|
|
65
|
+
liveConversations.set(input.store.sessionScope, reference);
|
|
66
|
+
releaseConversation.register(shared, {
|
|
67
|
+
scope: input.store.sessionScope,
|
|
68
|
+
reference,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
this.#conversation = shared;
|
|
38
72
|
this.#content = input.content;
|
|
39
73
|
this.#compiler = input.compiler;
|
|
40
74
|
this.#locale = input.locale;
|
|
@@ -123,7 +157,13 @@ export class SettingImprovementSession {
|
|
|
123
157
|
const stored = await this.#store.read(sessionId);
|
|
124
158
|
assertPackage(stored, packageId);
|
|
125
159
|
await this.#recoverIfNeeded(stored);
|
|
126
|
-
await this.#
|
|
160
|
+
const lease = await this.#content.beginCurrentTreeContentPackageOperation(packageId);
|
|
161
|
+
try {
|
|
162
|
+
await this.#store.deleteSession(packageId, sessionId);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
lease.release();
|
|
166
|
+
}
|
|
127
167
|
});
|
|
128
168
|
return this.overview(packageId);
|
|
129
169
|
}
|
|
@@ -186,11 +226,12 @@ export class SettingImprovementSession {
|
|
|
186
226
|
session = await this.#store.read(input.continuation.sessionId);
|
|
187
227
|
assertPackage(session, input.packageId);
|
|
188
228
|
}
|
|
189
|
-
return this.#startRequest(session, requestId, async (next) => {
|
|
229
|
+
return this.#startRequest(session, requestId, async (next, lease) => {
|
|
190
230
|
if (next.runStatus === "paused")
|
|
191
231
|
throw new Error("Resume the paused authoring request before sending another message");
|
|
192
|
-
const { playPreset, bootstrap, preload } = await this.#
|
|
193
|
-
next.schemaVersion
|
|
232
|
+
const { playPreset, bootstrap, preload } = await this.#compilePackage(lease.package, next.modelBinding);
|
|
233
|
+
if (next.schemaVersion !== 5)
|
|
234
|
+
next.schemaVersion = 4;
|
|
194
235
|
next.requests ??= [];
|
|
195
236
|
next.requests.push({
|
|
196
237
|
requestId,
|
|
@@ -200,20 +241,136 @@ export class SettingImprovementSession {
|
|
|
200
241
|
preload: structuredClone(preload.record),
|
|
201
242
|
});
|
|
202
243
|
next.authorization = authoringPreloadAuthorization(next.authorization, preload.record);
|
|
244
|
+
delete next.retryDispatch;
|
|
203
245
|
appendAuthoringUserMessage(next, message, requestId);
|
|
246
|
+
checkpointNextAuthoringDispatch(next, "dispatching");
|
|
204
247
|
if (next.modelBinding.provider === "google_interactions")
|
|
205
248
|
next.googleOutcomeUnknown = false;
|
|
206
249
|
});
|
|
207
250
|
});
|
|
208
251
|
return prepared.run ?? this.#view(prepared.session);
|
|
209
252
|
}
|
|
253
|
+
async editMessage(input) {
|
|
254
|
+
const message = requiredMessage(input.message);
|
|
255
|
+
const requestId = requiredRequestId(input.requestId);
|
|
256
|
+
if (input.filePolicy !== "keep" && input.filePolicy !== "revert")
|
|
257
|
+
throw new Error("Invalid file policy");
|
|
258
|
+
const prepared = await this.#mutate(async () => {
|
|
259
|
+
let session = await this.#store.read(input.sessionId);
|
|
260
|
+
assertPackage(session, input.packageId);
|
|
261
|
+
session = await this.#recoverIfNeeded(session);
|
|
262
|
+
const edit = {
|
|
263
|
+
requestId,
|
|
264
|
+
messageId: input.messageId,
|
|
265
|
+
message,
|
|
266
|
+
filePolicy: input.filePolicy,
|
|
267
|
+
};
|
|
268
|
+
if (completedAuthoringEdit(session, edit))
|
|
269
|
+
return { session, run: null };
|
|
270
|
+
const running = this.#conversation.running(session.sessionId);
|
|
271
|
+
if (running !== null) {
|
|
272
|
+
if (running.requestId !== requestId)
|
|
273
|
+
throw new Error("Another message in this setting-improvement conversation is still running");
|
|
274
|
+
return { session, run: running.promise };
|
|
275
|
+
}
|
|
276
|
+
if (session.googleOutcomeUnknown)
|
|
277
|
+
throw new Error("The Google request outcome is unknown; start a fresh model context.");
|
|
278
|
+
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
279
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
280
|
+
throw new Error("No saved model connection matches this setting-improvement conversation");
|
|
281
|
+
const { itemIndex, tail, next } = planAuthoringEdit(session, edit);
|
|
282
|
+
const lease = await this.#content.beginCurrentTreeContentPackageOperation(input.packageId);
|
|
283
|
+
try {
|
|
284
|
+
if (!isDeepStrictEqual(await this.#store.read(session.sessionId), session))
|
|
285
|
+
throw new Error("The authoring conversation changed; reload it before editing");
|
|
286
|
+
if (input.filePolicy === "revert" &&
|
|
287
|
+
session.legacyDraft !== null &&
|
|
288
|
+
(session.legacyDraft.outcome === "applied" ||
|
|
289
|
+
session.legacyDraft.outcome === "apply_outcome_unknown") &&
|
|
290
|
+
session.legacyDraft.changes.length > 0 &&
|
|
291
|
+
!afterLegacyBoundary(session, itemIndex))
|
|
292
|
+
throw new SettingMessageEditError("unattributed", [
|
|
293
|
+
...new Set(session.legacyDraft.changes.map(({ path }) => path)),
|
|
294
|
+
].sort());
|
|
295
|
+
const beforeFiles = lease.package.files;
|
|
296
|
+
const reverted = input.filePolicy === "revert"
|
|
297
|
+
? revertAuthoringChanges(beforeFiles, tail)
|
|
298
|
+
: { files: beforeFiles, conflicts: [] };
|
|
299
|
+
if (reverted.conflicts.length > 0)
|
|
300
|
+
throw new SettingMessageEditError("conflict", reverted.conflicts);
|
|
301
|
+
const afterFiles = reverted.files;
|
|
302
|
+
this.#content.validateCurrentTreeContentPackage(afterFiles);
|
|
303
|
+
if (input.filePolicy === "revert") {
|
|
304
|
+
const existingIssues = new Set(this.#content
|
|
305
|
+
.inspectCurrentTreeContentPackage(beforeFiles)
|
|
306
|
+
.issues.map((issue) => JSON.stringify(issue)));
|
|
307
|
+
const newIssues = this.#content
|
|
308
|
+
.inspectCurrentTreeContentPackage(afterFiles)
|
|
309
|
+
.issues.filter((issue) => !existingIssues.has(JSON.stringify(issue)));
|
|
310
|
+
if (newIssues.length > 0)
|
|
311
|
+
throw new SettingMessageEditError("invalid_tree", [...new Set(newIssues.map(({ path }) => path))].sort());
|
|
312
|
+
}
|
|
313
|
+
const { playPreset, bootstrap, preload } = await this.#compilePackage({
|
|
314
|
+
...lease.package,
|
|
315
|
+
files: afterFiles,
|
|
316
|
+
gameplay: readGameplay(afterFiles),
|
|
317
|
+
}, session.modelBinding);
|
|
318
|
+
next.schemaVersion = 5;
|
|
319
|
+
delete next.pendingEdit;
|
|
320
|
+
next.requests.push({
|
|
321
|
+
requestId,
|
|
322
|
+
modelItemStart: itemIndex,
|
|
323
|
+
bootstrap,
|
|
324
|
+
playPreset: structuredClone(playPreset),
|
|
325
|
+
preload: structuredClone(preload.record),
|
|
326
|
+
});
|
|
327
|
+
next.authorization = authoringPreloadAuthorization(null, preload.record);
|
|
328
|
+
appendAuthoringUserMessage(next, message, requestId);
|
|
329
|
+
checkpointNextAuthoringDispatch(next, "dispatching");
|
|
330
|
+
if (next.modelBinding.provider === "google_interactions")
|
|
331
|
+
next.googleOutcomeUnknown = false;
|
|
332
|
+
if (next.modelBinding.provider === "google_interactions" &&
|
|
333
|
+
host.previewAuthoringRequest !== undefined)
|
|
334
|
+
next.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(next, requestId));
|
|
335
|
+
session.schemaVersion = 5;
|
|
336
|
+
session.requests ??= [];
|
|
337
|
+
session.pendingEdit = {
|
|
338
|
+
beforeFingerprint: contentTreeFingerprint(beforeFiles),
|
|
339
|
+
afterFingerprint: contentTreeFingerprint(afterFiles),
|
|
340
|
+
afterFiles: structuredClone(afterFiles),
|
|
341
|
+
next,
|
|
342
|
+
};
|
|
343
|
+
await this.#store.preparePendingEdit(session);
|
|
344
|
+
this.#changed(input.packageId, true);
|
|
345
|
+
const committed = await this.#store.finishPendingEdit(session, lease);
|
|
346
|
+
this.#changed(input.packageId, true);
|
|
347
|
+
return {
|
|
348
|
+
session: committed,
|
|
349
|
+
run: this.#startRun(committed, host, requestId),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
lease.release();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return prepared.run ?? this.#view(prepared.session);
|
|
357
|
+
}
|
|
210
358
|
/** Continues a paused conversation with the frozen request it paused in. */
|
|
211
359
|
async resume(input) {
|
|
212
360
|
const requestId = requiredRequestId(input.requestId);
|
|
213
361
|
const prepared = await this.#mutate(async () => {
|
|
214
362
|
const session = await this.#store.read(input.sessionId);
|
|
215
363
|
assertPackage(session, input.packageId);
|
|
216
|
-
return this.#startRequest(session, requestId, (next) =>
|
|
364
|
+
return this.#startRequest(session, requestId, (next) => {
|
|
365
|
+
if (canRetryAuthoringDispatch(next)) {
|
|
366
|
+
next.runStatus = "running";
|
|
367
|
+
next.activeRequestId = next.requests?.at(-1)?.requestId ?? null;
|
|
368
|
+
next.lastFailure = null;
|
|
369
|
+
next.updatedAt = Date.now();
|
|
370
|
+
}
|
|
371
|
+
else
|
|
372
|
+
resumePausedAuthoringConversation(next);
|
|
373
|
+
});
|
|
217
374
|
});
|
|
218
375
|
return prepared.run ?? this.#view(prepared.session);
|
|
219
376
|
}
|
|
@@ -223,8 +380,11 @@ export class SettingImprovementSession {
|
|
|
223
380
|
return { session, run: null };
|
|
224
381
|
const running = this.#conversation.running(session.sessionId);
|
|
225
382
|
if (running !== null) {
|
|
226
|
-
if (running.requestId !== requestId)
|
|
383
|
+
if (running.requestId !== requestId) {
|
|
384
|
+
if (session.googleOutcomeUnknown)
|
|
385
|
+
throw new Error("The Google request outcome is unknown; start a fresh model context.");
|
|
227
386
|
throw new Error("Another message in this setting-improvement conversation is still running");
|
|
387
|
+
}
|
|
228
388
|
return { session, run: running.promise };
|
|
229
389
|
}
|
|
230
390
|
if (session.googleOutcomeUnknown)
|
|
@@ -232,13 +392,23 @@ export class SettingImprovementSession {
|
|
|
232
392
|
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
233
393
|
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
234
394
|
throw new Error("No saved model connection matches this setting-improvement conversation");
|
|
235
|
-
await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
session.
|
|
240
|
-
|
|
241
|
-
|
|
395
|
+
const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
|
|
396
|
+
try {
|
|
397
|
+
if (!isDeepStrictEqual(await this.#store.read(session.sessionId), session))
|
|
398
|
+
throw new Error("The authoring conversation changed; reload it before continuing");
|
|
399
|
+
if (session.schemaVersion === 3)
|
|
400
|
+
session.schemaVersion = 4;
|
|
401
|
+
await prepare(session, lease);
|
|
402
|
+
if (session.modelBinding.provider === "google_interactions" &&
|
|
403
|
+
host.previewAuthoringRequest !== undefined &&
|
|
404
|
+
session.requests?.at(-1)?.requestId === requestId)
|
|
405
|
+
session.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(session, requestId));
|
|
406
|
+
await this.#store.save(session);
|
|
407
|
+
return { session, run: this.#startRun(session, host, requestId) };
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
lease.release();
|
|
411
|
+
}
|
|
242
412
|
}
|
|
243
413
|
async preview(packageId, sessionId) {
|
|
244
414
|
const session = sessionId === undefined ? null : await this.#store.read(sessionId);
|
|
@@ -254,9 +424,12 @@ export class SettingImprovementSession {
|
|
|
254
424
|
return bootstrap;
|
|
255
425
|
}
|
|
256
426
|
async #compileCurrent(packageId, modelBinding) {
|
|
427
|
+
const package_ = await this.#content.readCurrentTreeContentPackage(packageId);
|
|
428
|
+
return this.#compilePackage(package_, modelBinding);
|
|
429
|
+
}
|
|
430
|
+
async #compilePackage(package_, modelBinding) {
|
|
257
431
|
const playPreset = await this.#bindPlayPreset();
|
|
258
432
|
const locale = this.#locale();
|
|
259
|
-
const package_ = await this.#content.readCurrentTreeContentPackage(packageId);
|
|
260
433
|
const preload = this.#preload(package_.files, modelBinding, playPreset, locale);
|
|
261
434
|
const bootstrap = this.#compiler.compileSettingImprovement({
|
|
262
435
|
contentPackageTitle: package_.title,
|
|
@@ -329,7 +502,8 @@ export class SettingImprovementSession {
|
|
|
329
502
|
const now = Date.now();
|
|
330
503
|
const sessionId = this.#store.createId();
|
|
331
504
|
const session = {
|
|
332
|
-
schemaVersion:
|
|
505
|
+
schemaVersion: 4,
|
|
506
|
+
requests: [],
|
|
333
507
|
sessionId,
|
|
334
508
|
packageId,
|
|
335
509
|
contentPackageTitle: package_.title,
|
|
@@ -366,6 +540,7 @@ export class SettingImprovementSession {
|
|
|
366
540
|
session,
|
|
367
541
|
host,
|
|
368
542
|
requestId,
|
|
543
|
+
exchangeRequestId: session.activeRequestId ?? requestId,
|
|
369
544
|
save: (next) => this.#store.save(next),
|
|
370
545
|
recordGoogleHop: (id, hop) => this.#store.recordGoogleHop(session.sessionId, id, hop),
|
|
371
546
|
settleToolResponse: async (next, assistantItemIndex, toolCalls) => {
|
|
@@ -377,10 +552,11 @@ export class SettingImprovementSession {
|
|
|
377
552
|
cancelled: "The setting-improvement response was cancelled. Complete tool responses already published to the current tree were retained.",
|
|
378
553
|
interrupted: "The setting-improvement response was interrupted",
|
|
379
554
|
},
|
|
555
|
+
retrySupported: session.schemaVersion === 4 || session.schemaVersion === 5,
|
|
380
556
|
});
|
|
381
557
|
}
|
|
382
558
|
async #settleToolResponse(session, assistantItemIndex, calls, active) {
|
|
383
|
-
const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
|
|
559
|
+
const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId, { wait: true });
|
|
384
560
|
try {
|
|
385
561
|
const beforeFingerprint = contentTreeFingerprint(lease.package.files);
|
|
386
562
|
session.pendingSettlement = {
|
|
@@ -420,6 +596,8 @@ export class SettingImprovementSession {
|
|
|
420
596
|
}
|
|
421
597
|
}
|
|
422
598
|
this.#finalizeSettlement(session, prepared);
|
|
599
|
+
if (session.schemaVersion === 4 || session.schemaVersion === 5)
|
|
600
|
+
checkpointNextAuthoringDispatch(session, "dispatching");
|
|
423
601
|
session.updatedAt = Date.now();
|
|
424
602
|
await this.#store.save(session);
|
|
425
603
|
return { revision: afterFingerprint, transaction };
|
|
@@ -454,9 +632,43 @@ export class SettingImprovementSession {
|
|
|
454
632
|
async #recoverIfNeeded(original) {
|
|
455
633
|
if (this.#conversation.isRunning(original.sessionId))
|
|
456
634
|
return original;
|
|
635
|
+
if (original.pendingEdit !== undefined) {
|
|
636
|
+
const lease = await this.#content.beginCurrentTreeContentPackageOperation(original.packageId);
|
|
637
|
+
lease.release();
|
|
638
|
+
const recovered = await this.#store.read(original.sessionId);
|
|
639
|
+
if (recovered.pendingEdit !== undefined)
|
|
640
|
+
throw new Error("The pending authoring edit has no valid package recovery marker");
|
|
641
|
+
return this.#recoverIfNeeded(recovered);
|
|
642
|
+
}
|
|
457
643
|
let expected = original;
|
|
458
644
|
const session = structuredClone(original);
|
|
459
645
|
const pendingAssistant = lastPendingAssistant(session);
|
|
646
|
+
if (session.runStatus === "running" &&
|
|
647
|
+
session.pendingSettlement === null &&
|
|
648
|
+
pendingAssistant === null) {
|
|
649
|
+
const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
|
|
650
|
+
let latest;
|
|
651
|
+
try {
|
|
652
|
+
latest = await this.#store.read(session.sessionId);
|
|
653
|
+
if (this.#conversation.isRunning(session.sessionId))
|
|
654
|
+
return latest;
|
|
655
|
+
if (isDeepStrictEqual(latest, original)) {
|
|
656
|
+
if (session.modelBinding.provider === "google_interactions" &&
|
|
657
|
+
session.googleOutcomeUnknown !== false)
|
|
658
|
+
session.googleOutcomeUnknown = true;
|
|
659
|
+
markInterruptedAfterRestart(session);
|
|
660
|
+
recoverAuthoringDispatch(session);
|
|
661
|
+
session.updatedAt = Date.now();
|
|
662
|
+
if (await this.#store.saveIfUnchanged(original, session))
|
|
663
|
+
return session;
|
|
664
|
+
latest = await this.#store.read(session.sessionId);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
finally {
|
|
668
|
+
lease.release();
|
|
669
|
+
}
|
|
670
|
+
return this.#recoverIfNeeded(latest);
|
|
671
|
+
}
|
|
460
672
|
if (session.pendingSettlement === null && pendingAssistant !== null) {
|
|
461
673
|
this.#finalizeUnconfirmedToolResponse(session, pendingAssistant.itemIndex, "Runtime found a complete tool response without the persisted current-tree revision that authorized it. The calls were not replayed against a newer tree; read the current tree again before editing.");
|
|
462
674
|
markInterruptedAfterRestart(session);
|
|
@@ -475,6 +687,7 @@ export class SettingImprovementSession {
|
|
|
475
687
|
let fingerprint = contentTreeFingerprint(lease.package.files);
|
|
476
688
|
const pending = session.pendingSettlement;
|
|
477
689
|
let prepared = pending.phase === "publication_prepared" ? pending : null;
|
|
690
|
+
let settled = false;
|
|
478
691
|
if (fingerprint === pending.beforeFingerprint) {
|
|
479
692
|
const assistant = assistantAt(session, pending.assistantItemIndex);
|
|
480
693
|
const transaction = this.#openTransaction(session, lease.package.files, fingerprint);
|
|
@@ -504,11 +717,20 @@ export class SettingImprovementSession {
|
|
|
504
717
|
}
|
|
505
718
|
else if (fingerprint === prepared.afterFingerprint) {
|
|
506
719
|
this.#finalizeSettlement(session, prepared);
|
|
720
|
+
settled = true;
|
|
507
721
|
}
|
|
508
722
|
else {
|
|
509
723
|
this.#finalizeUnconfirmedToolResponse(session, prepared.assistantItemIndex, "The content package no longer matches either the original or prepared revision for this tool response. No claimed change is being treated as authoritative; read the current tree again before editing.");
|
|
510
724
|
}
|
|
511
725
|
markInterruptedAfterRestart(session);
|
|
726
|
+
const request = session.requests?.at(-1);
|
|
727
|
+
if (settled &&
|
|
728
|
+
request !== undefined &&
|
|
729
|
+
session.modelBinding.provider !== "google_interactions") {
|
|
730
|
+
if (session.schemaVersion !== 5)
|
|
731
|
+
session.schemaVersion = 4;
|
|
732
|
+
checkpointNextAuthoringDispatch(session, "retryable");
|
|
733
|
+
}
|
|
512
734
|
session.updatedAt = Date.now();
|
|
513
735
|
if (!(await this.#store.saveIfUnchanged(expected, session)))
|
|
514
736
|
throw new RecoveryWriteConflictError();
|
|
@@ -538,13 +760,6 @@ export class SettingImprovementSession {
|
|
|
538
760
|
"The Google request outcome is unknown; start a fresh model context.";
|
|
539
761
|
session.updatedAt = Date.now();
|
|
540
762
|
}
|
|
541
|
-
else if (session.runStatus === "running") {
|
|
542
|
-
if (session.modelBinding.provider === "google_interactions" &&
|
|
543
|
-
session.googleOutcomeUnknown !== false)
|
|
544
|
-
session.googleOutcomeUnknown = true;
|
|
545
|
-
markInterruptedAfterRestart(session);
|
|
546
|
-
session.updatedAt = Date.now();
|
|
547
|
-
}
|
|
548
763
|
else {
|
|
549
764
|
return session;
|
|
550
765
|
}
|
|
@@ -601,6 +816,7 @@ export class SettingImprovementSession {
|
|
|
601
816
|
updatedAt: session.updatedAt,
|
|
602
817
|
},
|
|
603
818
|
lastFailure: session.lastFailure,
|
|
819
|
+
canRetry: canRetryAuthoringDispatch(session),
|
|
604
820
|
};
|
|
605
821
|
}
|
|
606
822
|
async #mutate(operation) {
|
|
@@ -764,6 +980,13 @@ function treeMatchesChangeBefore(files, change) {
|
|
|
764
980
|
current.encoding === undefined &&
|
|
765
981
|
current.contents === expected);
|
|
766
982
|
}
|
|
983
|
+
function afterLegacyBoundary(session, itemIndex) {
|
|
984
|
+
const first = session.requests?.[0];
|
|
985
|
+
return (first !== undefined &&
|
|
986
|
+
itemIndex >= first.modelItemStart &&
|
|
987
|
+
(first.modelItemStart > 0 ||
|
|
988
|
+
session.editReceipts?.some((receipt) => receipt.requestId === first.requestId) === true));
|
|
989
|
+
}
|
|
767
990
|
function restoreChangeBefore(files, change) {
|
|
768
991
|
const restored = new Map(files.map((file) => [file.path, structuredClone(file)]));
|
|
769
992
|
if (change.before === null)
|
|
@@ -4,7 +4,7 @@ import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
|
|
|
4
4
|
import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { readGoogleHops, writeGoogleHop } from "../model/GoogleHopJournal.js";
|
|
7
|
-
import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
7
|
+
import { link, mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { gameplayPath } from "../../protocol/gameplay.js";
|
|
10
10
|
import { validGoogleInteractionsSteps } from "../model/ProviderExchangeState.js";
|
|
@@ -43,6 +43,9 @@ export class FileNativeWorldRevisionStore {
|
|
|
43
43
|
this.#epochsRoot = join(root, "epochs");
|
|
44
44
|
this.#sessionsRoot = join(root, "sessions");
|
|
45
45
|
}
|
|
46
|
+
get sessionScope() {
|
|
47
|
+
return this.#sessionsRoot;
|
|
48
|
+
}
|
|
46
49
|
createEpochId() {
|
|
47
50
|
return `revision-${randomUUID()}`;
|
|
48
51
|
}
|
|
@@ -114,9 +117,90 @@ export class FileNativeWorldRevisionStore {
|
|
|
114
117
|
saveSession(session) {
|
|
115
118
|
return this.#mutate(async () => {
|
|
116
119
|
validateSession(session);
|
|
117
|
-
|
|
120
|
+
const path = this.#sessionPath(session.sessionId);
|
|
121
|
+
{
|
|
122
|
+
const source = await readFile(path).catch((error) => {
|
|
123
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
124
|
+
return null;
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
127
|
+
if (source !== null) {
|
|
128
|
+
const old = validateSession(JSON.parse(source.toString("utf8")));
|
|
129
|
+
if (old.schemaVersion === 3 && session.schemaVersion !== 3)
|
|
130
|
+
throw new Error("Cannot downgrade a migrated world-revision session");
|
|
131
|
+
if (old.schemaVersion !== 3) {
|
|
132
|
+
const backup = join(this.#sessionsRoot, ".legacy", `${session.sessionId}.json`);
|
|
133
|
+
await mkdir(dirname(backup), { recursive: true, mode: 0o700 });
|
|
134
|
+
const temporary = `${backup}.${randomUUID()}.tmp`;
|
|
135
|
+
try {
|
|
136
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
137
|
+
try {
|
|
138
|
+
await handle.writeFile(source);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await handle.close();
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await link(temporary, backup);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
if (!(await readFile(backup)).equals(source))
|
|
151
|
+
throw new Error("World-revision legacy source backup does not match the original bytes");
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await rm(temporary, { force: true });
|
|
155
|
+
}
|
|
156
|
+
session.schemaVersion = 3;
|
|
157
|
+
validateSession(session);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await publishJson(path, session);
|
|
118
162
|
});
|
|
119
163
|
}
|
|
164
|
+
async prepareEdit(input) {
|
|
165
|
+
validateEpoch(input.epoch);
|
|
166
|
+
validateSession(input.session);
|
|
167
|
+
if (input.epoch.worldId !== input.session.worldId ||
|
|
168
|
+
input.epoch.epochId !== input.session.epochId ||
|
|
169
|
+
!treeRevisionPattern.test(input.beforeRevision))
|
|
170
|
+
throw new Error("Invalid world-revision edit intent");
|
|
171
|
+
await publishJson(this.#editPath(input.epoch.epochId), input);
|
|
172
|
+
}
|
|
173
|
+
async readEdit(epochId) {
|
|
174
|
+
let value;
|
|
175
|
+
try {
|
|
176
|
+
value = JSON.parse(await readFile(this.#editPath(epochId), "utf8"));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
180
|
+
return null;
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
if (!isRecord(value) ||
|
|
184
|
+
!hasExactKeys(value, ["beforeRevision", "epoch", "session"]) ||
|
|
185
|
+
typeof value.beforeRevision !== "string" ||
|
|
186
|
+
!treeRevisionPattern.test(value.beforeRevision))
|
|
187
|
+
throw new Error("Invalid world-revision edit intent");
|
|
188
|
+
const epoch = validateEpoch(value.epoch);
|
|
189
|
+
const session = validateSession(value.session);
|
|
190
|
+
if (epoch.epochId !== epochId ||
|
|
191
|
+
epoch.worldId !== session.worldId ||
|
|
192
|
+
epoch.epochId !== session.epochId ||
|
|
193
|
+
epoch.lifecycle !== "active")
|
|
194
|
+
throw new Error("Invalid world-revision edit identity");
|
|
195
|
+
return { beforeRevision: value.beforeRevision, epoch, session };
|
|
196
|
+
}
|
|
197
|
+
async removeEdit(epochId) {
|
|
198
|
+
await rm(this.#editPath(epochId), { force: true });
|
|
199
|
+
}
|
|
200
|
+
#editPath(epochId) {
|
|
201
|
+
assertEpochId(epochId);
|
|
202
|
+
return join(this.#epochsRoot, `.edit-${epochId}.json`);
|
|
203
|
+
}
|
|
120
204
|
async listSessions(worldId) {
|
|
121
205
|
return this.#afterMutations(async () => {
|
|
122
206
|
const entries = await readdir(this.#sessionsRoot, {
|
|
@@ -339,15 +423,51 @@ function validateSession(value) {
|
|
|
339
423
|
"lastFailure",
|
|
340
424
|
"authorization",
|
|
341
425
|
"pendingSettlement",
|
|
342
|
-
], [
|
|
343
|
-
|
|
426
|
+
], [
|
|
427
|
+
"requests",
|
|
428
|
+
"googleOutcomeUnknown",
|
|
429
|
+
...(value.schemaVersion === 3 ? ["retryDispatch", "editReceipts"] : []),
|
|
430
|
+
]) ||
|
|
431
|
+
(value.schemaVersion !== 1 &&
|
|
432
|
+
value.schemaVersion !== 2 &&
|
|
433
|
+
value.schemaVersion !== 3) ||
|
|
434
|
+
(value.editReceipts !== undefined &&
|
|
435
|
+
!validEditReceipts(value.editReceipts)) ||
|
|
436
|
+
(value.retryDispatch !== undefined &&
|
|
437
|
+
(!isRecord(value.retryDispatch) ||
|
|
438
|
+
!hasExactKeys(value.retryDispatch, [
|
|
439
|
+
"requestId",
|
|
440
|
+
"exchange",
|
|
441
|
+
"modelItemCount",
|
|
442
|
+
"state",
|
|
443
|
+
]) ||
|
|
444
|
+
!nonEmptyString(value.retryDispatch.requestId) ||
|
|
445
|
+
!count(value.retryDispatch.exchange) ||
|
|
446
|
+
!count(value.retryDispatch.modelItemCount) ||
|
|
447
|
+
!Array.isArray(value.requests) ||
|
|
448
|
+
!isRecord(value.requests.at(-1)) ||
|
|
449
|
+
value.requests.at(-1).requestId !==
|
|
450
|
+
value.retryDispatch.requestId ||
|
|
451
|
+
(value.runStatus !== "running" && value.runStatus !== "interrupted") ||
|
|
452
|
+
(value.retryDispatch.state === "retryable" &&
|
|
453
|
+
(!isRecord(value.modelBinding) ||
|
|
454
|
+
value.modelBinding.provider === "google_interactions")) ||
|
|
455
|
+
value.pendingSettlement !== null ||
|
|
456
|
+
!count(value.exchange) ||
|
|
457
|
+
(value.retryDispatch.exchange !== value.exchange &&
|
|
458
|
+
value.retryDispatch.exchange !== value.exchange + 1) ||
|
|
459
|
+
value.retryDispatch.modelItemCount !==
|
|
460
|
+
(Array.isArray(value.modelItems) ? value.modelItems.length : -1) ||
|
|
461
|
+
!["dispatching", "retryable", "blocked"].includes(String(value.retryDispatch.state)))) ||
|
|
344
462
|
(value.schemaVersion === 1
|
|
345
463
|
? value.requests !== undefined
|
|
346
|
-
:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
464
|
+
: value.schemaVersion === 3 && value.requests === undefined
|
|
465
|
+
? value.retryDispatch !== undefined
|
|
466
|
+
: !validAuthoringRequests(value.requests, validPromptCompilation, {
|
|
467
|
+
modelItems: value.modelItems,
|
|
468
|
+
activeRequestId: value.activeRequestId,
|
|
469
|
+
completedRequestIds: value.completedRequestIds,
|
|
470
|
+
})) ||
|
|
351
471
|
typeof value.sessionId !== "string" ||
|
|
352
472
|
!nonEmptyString(value.worldId) ||
|
|
353
473
|
typeof value.worldTitle !== "string" ||
|
|
@@ -389,6 +509,31 @@ function validateSession(value) {
|
|
|
389
509
|
assertEpochId(value.epochId);
|
|
390
510
|
return structuredClone(value);
|
|
391
511
|
}
|
|
512
|
+
function validEditReceipts(value) {
|
|
513
|
+
if (!Array.isArray(value))
|
|
514
|
+
return false;
|
|
515
|
+
const ids = new Set();
|
|
516
|
+
return value.every((entry) => {
|
|
517
|
+
if (!isRecord(entry) ||
|
|
518
|
+
!hasExactKeys(entry, [
|
|
519
|
+
"requestId",
|
|
520
|
+
"messageId",
|
|
521
|
+
"message",
|
|
522
|
+
"filePolicy",
|
|
523
|
+
]) ||
|
|
524
|
+
typeof entry.requestId !== "string" ||
|
|
525
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(entry.requestId) ||
|
|
526
|
+
ids.has(entry.requestId) ||
|
|
527
|
+
!nonEmptyString(entry.messageId) ||
|
|
528
|
+
typeof entry.message !== "string" ||
|
|
529
|
+
entry.message.trim().length === 0 ||
|
|
530
|
+
entry.message.length > 64 * 1024 ||
|
|
531
|
+
(entry.filePolicy !== "keep" && entry.filePolicy !== "revert"))
|
|
532
|
+
return false;
|
|
533
|
+
ids.add(entry.requestId);
|
|
534
|
+
return true;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
392
537
|
function validFiles(value) {
|
|
393
538
|
if (!Array.isArray(value))
|
|
394
539
|
return false;
|