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.
Files changed (49) hide show
  1. package/dist/node/protocol/modelConnections.js +15 -0
  2. package/dist/node/protocol/modelUsage.js +4 -0
  3. package/dist/node/protocol/sillyTavernContent.js +1 -0
  4. package/dist/node/protocol/v1.js +97 -0
  5. package/dist/node/runtime/V1Runtime.js +105 -3
  6. package/dist/node/runtime/authoring/AuthoringEditPlan.js +79 -0
  7. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  8. package/dist/node/runtime/authoring/AuthoringRetry.js +59 -0
  9. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  10. package/dist/node/runtime/authoring/ModelToolConversation.js +66 -25
  11. package/dist/node/runtime/content/ContentWorkspace.js +10 -2
  12. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +14 -8
  13. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  14. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  15. package/dist/node/runtime/model/DispatchFailure.js +8 -0
  16. package/dist/node/runtime/model/FileNativeModelAdapters.js +627 -18
  17. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  18. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  19. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  20. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  21. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  22. package/dist/node/runtime/model/ProviderStream.js +4 -2
  23. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  24. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  25. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +199 -10
  26. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  27. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +51 -23
  28. package/dist/node/runtime/play/PlayCallChain.js +92 -8
  29. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  30. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  31. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +321 -15
  32. package/dist/node/runtime/setting/SettingImprovementSession.js +285 -21
  33. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +236 -15
  34. package/dist/node/runtime/world-revision/WorldRevisionSession.js +324 -50
  35. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +47 -1
  36. package/dist/node/shared/application-preset-files.js +29 -0
  37. package/dist/node/shared/default-preset-host.js +50 -38
  38. package/dist/node/shared/first-party-action-choices.js +7 -11
  39. package/dist/node/shared/first-party-generic-panels.js +7 -11
  40. package/dist/node/shared/first-party-player-view.js +7 -11
  41. package/dist/node/shared/ordered-author-prompts.js +6 -4
  42. package/dist/node/shared/ordered-play-prompts.js +4 -2
  43. package/dist/node/shared/prompt-arrangement.js +205 -0
  44. package/dist/node/shared/sillytavern-preset.js +454 -0
  45. package/dist/web/assets/index-BthiDIQu.js +1432 -0
  46. package/dist/web/assets/{index-DH2BVsYd.css → index-wO9Y4HT1.css} +1 -1
  47. package/dist/web/index.html +2 -2
  48. package/package.json +1 -1
  49. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -1,21 +1,44 @@
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";
5
9
  import { equalModelHostBinding } from "../model/ModelHost.js";
6
10
  import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
7
11
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
8
- import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
12
+ import { appendAuthoringUserMessage, authoringExchange, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
9
13
  import { summarizeStoredSettingImprovementSession, } from "./FileNativeSettingImprovementStore.js";
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 = new ModelToolConversation();
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;
@@ -62,7 +96,7 @@ export class SettingImprovementSession {
62
96
  async overview(packageId) {
63
97
  return this.#mutate(async () => {
64
98
  let summaries = await this.#store.listSummariesByPackage(packageId);
65
- const latestSummary = summaries[0];
99
+ const latestSummary = summaries.find((summary) => !summary.damagedContinuation);
66
100
  const latest = latestSummary === undefined
67
101
  ? null
68
102
  : await this.#recoverIfNeeded(await this.#store.read(latestSummary.sessionId));
@@ -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.#store.deleteSession(packageId, sessionId);
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,9 +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) => {
190
- const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.packageId, next.modelBinding);
191
- next.schemaVersion = 3;
229
+ return this.#startRequest(session, requestId, async (next, lease) => {
230
+ if (next.runStatus === "paused")
231
+ throw new Error("Resume the paused authoring request before sending another message");
232
+ const { playPreset, bootstrap, preload } = await this.#compilePackage(lease.package, next.modelBinding);
233
+ if (next.schemaVersion !== 5)
234
+ next.schemaVersion = 4;
192
235
  next.requests ??= [];
193
236
  next.requests.push({
194
237
  requestId,
@@ -198,18 +241,136 @@ export class SettingImprovementSession {
198
241
  preload: structuredClone(preload.record),
199
242
  });
200
243
  next.authorization = authoringPreloadAuthorization(next.authorization, preload.record);
244
+ delete next.retryDispatch;
201
245
  appendAuthoringUserMessage(next, message, requestId);
246
+ checkpointNextAuthoringDispatch(next, "dispatching");
247
+ if (next.modelBinding.provider === "google_interactions")
248
+ next.googleOutcomeUnknown = false;
202
249
  });
203
250
  });
204
251
  return prepared.run ?? this.#view(prepared.session);
205
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
+ }
206
358
  /** Continues a paused conversation with the frozen request it paused in. */
207
359
  async resume(input) {
208
360
  const requestId = requiredRequestId(input.requestId);
209
361
  const prepared = await this.#mutate(async () => {
210
362
  const session = await this.#store.read(input.sessionId);
211
363
  assertPackage(session, input.packageId);
212
- return this.#startRequest(session, requestId, (next) => resumePausedAuthoringConversation(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
+ });
213
374
  });
214
375
  return prepared.run ?? this.#view(prepared.session);
215
376
  }
@@ -219,16 +380,35 @@ export class SettingImprovementSession {
219
380
  return { session, run: null };
220
381
  const running = this.#conversation.running(session.sessionId);
221
382
  if (running !== null) {
222
- 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.");
223
386
  throw new Error("Another message in this setting-improvement conversation is still running");
387
+ }
224
388
  return { session, run: running.promise };
225
389
  }
390
+ if (session.googleOutcomeUnknown)
391
+ throw new Error("The Google request outcome is unknown; start a fresh model context.");
226
392
  const host = await this.#bindExistingModelHost(session.modelBinding);
227
393
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
228
394
  throw new Error("No saved model connection matches this setting-improvement conversation");
229
- await prepare(session);
230
- await this.#store.save(session);
231
- return { session, run: this.#startRun(session, host, requestId) };
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
+ }
232
412
  }
233
413
  async preview(packageId, sessionId) {
234
414
  const session = sessionId === undefined ? null : await this.#store.read(sessionId);
@@ -244,9 +424,12 @@ export class SettingImprovementSession {
244
424
  return bootstrap;
245
425
  }
246
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) {
247
431
  const playPreset = await this.#bindPlayPreset();
248
432
  const locale = this.#locale();
249
- const package_ = await this.#content.readCurrentTreeContentPackage(packageId);
250
433
  const preload = this.#preload(package_.files, modelBinding, playPreset, locale);
251
434
  const bootstrap = this.#compiler.compileSettingImprovement({
252
435
  contentPackageTitle: package_.title,
@@ -317,9 +500,11 @@ export class SettingImprovementSession {
317
500
  tools,
318
501
  });
319
502
  const now = Date.now();
503
+ const sessionId = this.#store.createId();
320
504
  const session = {
321
- schemaVersion: 2,
322
- sessionId: this.#store.createId(),
505
+ schemaVersion: 4,
506
+ requests: [],
507
+ sessionId,
323
508
  packageId,
324
509
  contentPackageTitle: package_.title,
325
510
  locale,
@@ -327,6 +512,9 @@ export class SettingImprovementSession {
327
512
  createdAt: now,
328
513
  updatedAt: now,
329
514
  creationRequestId,
515
+ ...(modelBinding.provider === "google_interactions"
516
+ ? { googleIdentity: { sessionId, packageId, creationRequestId } }
517
+ : {}),
330
518
  bootstrap,
331
519
  modelBinding,
332
520
  playPreset: structuredClone(playPreset),
@@ -352,7 +540,9 @@ export class SettingImprovementSession {
352
540
  session,
353
541
  host,
354
542
  requestId,
543
+ exchangeRequestId: session.activeRequestId ?? requestId,
355
544
  save: (next) => this.#store.save(next),
545
+ recordGoogleHop: (id, hop) => this.#store.recordGoogleHop(session.sessionId, id, hop),
356
546
  settleToolResponse: async (next, assistantItemIndex, toolCalls) => {
357
547
  activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, toolCalls, activeTransaction);
358
548
  },
@@ -362,10 +552,11 @@ export class SettingImprovementSession {
362
552
  cancelled: "The setting-improvement response was cancelled. Complete tool responses already published to the current tree were retained.",
363
553
  interrupted: "The setting-improvement response was interrupted",
364
554
  },
555
+ retrySupported: session.schemaVersion === 4 || session.schemaVersion === 5,
365
556
  });
366
557
  }
367
558
  async #settleToolResponse(session, assistantItemIndex, calls, active) {
368
- const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
559
+ const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId, { wait: true });
369
560
  try {
370
561
  const beforeFingerprint = contentTreeFingerprint(lease.package.files);
371
562
  session.pendingSettlement = {
@@ -405,6 +596,8 @@ export class SettingImprovementSession {
405
596
  }
406
597
  }
407
598
  this.#finalizeSettlement(session, prepared);
599
+ if (session.schemaVersion === 4 || session.schemaVersion === 5)
600
+ checkpointNextAuthoringDispatch(session, "dispatching");
408
601
  session.updatedAt = Date.now();
409
602
  await this.#store.save(session);
410
603
  return { revision: afterFingerprint, transaction };
@@ -439,9 +632,43 @@ export class SettingImprovementSession {
439
632
  async #recoverIfNeeded(original) {
440
633
  if (this.#conversation.isRunning(original.sessionId))
441
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
+ }
442
643
  let expected = original;
443
644
  const session = structuredClone(original);
444
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
+ }
445
672
  if (session.pendingSettlement === null && pendingAssistant !== null) {
446
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.");
447
674
  markInterruptedAfterRestart(session);
@@ -451,6 +678,8 @@ export class SettingImprovementSession {
451
678
  return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
452
679
  }
453
680
  if (session.pendingSettlement !== null) {
681
+ if (session.modelBinding.provider === "google_interactions")
682
+ session.googleOutcomeUnknown = false;
454
683
  const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
455
684
  let retry = false;
456
685
  try {
@@ -458,6 +687,7 @@ export class SettingImprovementSession {
458
687
  let fingerprint = contentTreeFingerprint(lease.package.files);
459
688
  const pending = session.pendingSettlement;
460
689
  let prepared = pending.phase === "publication_prepared" ? pending : null;
690
+ let settled = false;
461
691
  if (fingerprint === pending.beforeFingerprint) {
462
692
  const assistant = assistantAt(session, pending.assistantItemIndex);
463
693
  const transaction = this.#openTransaction(session, lease.package.files, fingerprint);
@@ -487,11 +717,20 @@ export class SettingImprovementSession {
487
717
  }
488
718
  else if (fingerprint === prepared.afterFingerprint) {
489
719
  this.#finalizeSettlement(session, prepared);
720
+ settled = true;
490
721
  }
491
722
  else {
492
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.");
493
724
  }
494
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
+ }
495
734
  session.updatedAt = Date.now();
496
735
  if (!(await this.#store.saveIfUnchanged(expected, session)))
497
736
  throw new RecoveryWriteConflictError();
@@ -510,8 +749,15 @@ export class SettingImprovementSession {
510
749
  return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
511
750
  return session;
512
751
  }
513
- if (session.runStatus === "running") {
514
- markInterruptedAfterRestart(session);
752
+ const lastItem = session.modelItems.at(-1);
753
+ const legacyUnknown = session.modelBinding.provider === "google_interactions" &&
754
+ session.runStatus === "interrupted" &&
755
+ session.googleOutcomeUnknown === undefined &&
756
+ (lastItem?.kind === "user" || lastItem?.kind === "tool");
757
+ if (legacyUnknown) {
758
+ session.googleOutcomeUnknown = true;
759
+ session.lastFailure =
760
+ "The Google request outcome is unknown; start a fresh model context.";
515
761
  session.updatedAt = Date.now();
516
762
  }
517
763
  else {
@@ -570,6 +816,7 @@ export class SettingImprovementSession {
570
816
  updatedAt: session.updatedAt,
571
817
  },
572
818
  lastFailure: session.lastFailure,
819
+ canRetry: canRetryAuthoringDispatch(session),
573
820
  };
574
821
  }
575
822
  async #mutate(operation) {
@@ -606,12 +853,17 @@ function lastPendingAssistant(session) {
606
853
  }
607
854
  function markInterruptedAfterRestart(session) {
608
855
  const requestId = session.activeRequestId;
609
- session.runStatus = "interrupted";
856
+ session.runStatus =
857
+ session.modelBinding.provider === "google_interactions" &&
858
+ session.googleOutcomeUnknown === false
859
+ ? "paused"
860
+ : "interrupted";
610
861
  session.activeRequestId = null;
611
862
  if (requestId !== null)
612
863
  session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
613
- session.lastFailure =
614
- "The previous model request stopped before its next complete response. Every settled current-tree change and complete tool result was retained; continue this conversation when ready.";
864
+ session.lastFailure = session.googleOutcomeUnknown
865
+ ? "The Google request outcome is unknown; start a fresh model context."
866
+ : "The previous model request stopped before its next complete response. Every settled current-tree change and complete tool result was retained; continue this conversation when ready.";
615
867
  }
616
868
  function requiredMessage(message) {
617
869
  if (message.trim().length === 0)
@@ -728,6 +980,13 @@ function treeMatchesChangeBefore(files, change) {
728
980
  current.encoding === undefined &&
729
981
  current.contents === expected);
730
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
+ }
731
990
  function restoreChangeBefore(files, change) {
732
991
  const restored = new Map(files.map((file) => [file.path, structuredClone(file)]));
733
992
  if (change.before === null)
@@ -750,6 +1009,11 @@ function diffForRollback(files, change) {
750
1009
  function settingImprovementHistoryItem(summary) {
751
1010
  return {
752
1011
  sessionId: summary.sessionId,
1012
+ ...(summary.damagedContinuation
1013
+ ? {
1014
+ unavailableReason: "Google continuation is damaged; start a fresh model context.",
1015
+ }
1016
+ : {}),
753
1017
  runStatus: summary.runStatus,
754
1018
  createdAt: summary.createdAt,
755
1019
  updatedAt: summary.updatedAt,