narraeon 0.7.10 → 0.8.0

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 (44) 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 +75 -0
  5. package/dist/node/runtime/V1Runtime.js +71 -1
  6. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
  7. package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
  8. package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
  9. package/dist/node/runtime/content/ContentWorkspace.js +3 -0
  10. package/dist/node/runtime/content/SillyTavernContent.js +796 -0
  11. package/dist/node/runtime/model/AiFailureLog.js +8 -1
  12. package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
  13. package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
  14. package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
  15. package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
  16. package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
  17. package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
  18. package/dist/node/runtime/model/ProviderStream.js +4 -2
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
  20. package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
  21. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
  22. package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
  23. package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
  24. package/dist/node/runtime/play/PlayCallChain.js +76 -4
  25. package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
  26. package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
  27. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
  28. package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
  29. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
  30. package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
  31. package/dist/node/shared/application-preset-files.js +29 -0
  32. package/dist/node/shared/default-preset-host.js +50 -38
  33. package/dist/node/shared/first-party-action-choices.js +7 -11
  34. package/dist/node/shared/first-party-generic-panels.js +7 -11
  35. package/dist/node/shared/first-party-player-view.js +7 -11
  36. package/dist/node/shared/ordered-author-prompts.js +6 -4
  37. package/dist/node/shared/ordered-play-prompts.js +4 -2
  38. package/dist/node/shared/prompt-arrangement.js +205 -0
  39. package/dist/node/shared/sillytavern-preset.js +454 -0
  40. package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
  41. package/dist/web/assets/index-C_Qta8UL.js +1432 -0
  42. package/dist/web/index.html +2 -2
  43. package/package.json +1 -1
  44. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -49,6 +49,8 @@ export async function runPlayFollowupRequests(input) {
49
49
  }
50
50
  if (signalWasAborted(input.signal)) {
51
51
  failure ??= "The follow-up request was cancelled.";
52
+ outcomes.push(outcome);
53
+ input.observer?.onOutcome?.(outcome);
52
54
  break;
53
55
  }
54
56
  outcomes.push(outcome);
@@ -91,10 +93,9 @@ async function runOne(input, followup, prefix, requestSignal) {
91
93
  try {
92
94
  await input.artifacts.beginRequestAttempt(requestContext);
93
95
  requestSignal?.throwIfAborted();
94
- input.observer?.onProviderDispatch?.();
95
96
  let response;
96
97
  try {
97
- response = await input.modelHost.exchange({
98
+ const request = {
98
99
  bootstrap: structuredClone(input.bootstrap),
99
100
  tools: structuredClone(followup.tools),
100
101
  toolUniverse: structuredClone(followup.tools),
@@ -114,8 +115,23 @@ async function runOne(input, followup, prefix, requestSignal) {
114
115
  requestAttempt: 1,
115
116
  exchange: 1,
116
117
  maxOutputTokens: input.maxOutputTokens,
117
- }, {
118
+ };
119
+ try {
120
+ outcome.requestPreviewRecorded =
121
+ (await input.observer?.onRequest?.(followup.id, request)) ?? false;
122
+ }
123
+ catch {
124
+ // Diagnostic recording must not change the provider request.
125
+ }
126
+ requestSignal?.throwIfAborted();
127
+ outcome.requestPreviewStatus = "attempted";
128
+ input.observer?.onProviderDispatch?.();
129
+ response = await input.modelHost.exchange(request, {
118
130
  ...(requestSignal === undefined ? {} : { signal: requestSignal }),
131
+ onGoogleHop: (evidence) => input.observer?.onGoogleHop?.(followup.id, evidence),
132
+ ...(input.controls !== undefined
133
+ ? { retainLateExtensionResponse: true }
134
+ : {}),
119
135
  onDelta: (delta) => input.observer?.onProviderDelta?.(delta),
120
136
  });
121
137
  if (input.signal?.aborted === true)
@@ -124,6 +140,7 @@ async function runOne(input, followup, prefix, requestSignal) {
124
140
  finally {
125
141
  input.observer?.onProviderSettled?.();
126
142
  }
143
+ outcome.requestPreviewStatus = "completed";
127
144
  responseDiagnostics = response.diagnostics;
128
145
  if (response.diagnostics !== undefined)
129
146
  await input.failureLog?.recordExchangeIfActive(response.diagnostics);
@@ -182,6 +199,9 @@ async function runOne(input, followup, prefix, requestSignal) {
182
199
  });
183
200
  }
184
201
  catch (error) {
202
+ if (outcome.requestPreviewStatus !== undefined &&
203
+ error instanceof ModelHostCancelledError)
204
+ outcome.requestPreviewStatus = "cancelled";
185
205
  outcome.failure =
186
206
  error instanceof Error
187
207
  ? error.message
@@ -315,7 +315,8 @@ function validPromptCache(value) {
315
315
  export function validProviderKind(value) {
316
316
  return (value === "chat_completions" ||
317
317
  value === "openai_responses" ||
318
- value === "anthropic_messages");
318
+ value === "anthropic_messages" ||
319
+ value === "google_interactions");
319
320
  }
320
321
  export function validCacheStrategy(value) {
321
322
  return (value === "explicit_anthropic_blocks" ||
@@ -1,11 +1,14 @@
1
1
  import { validPromptCompilation, validLogicalMessages, validPromptTools, validPlayFollowup, validProviderKind, validCacheStrategy, validToolStrategy, } from "../prompt/PromptCompilationCodec.js";
2
2
  import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { readGoogleHops, writeGoogleHop } from "../model/GoogleHopJournal.js";
4
5
  import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
5
6
  import { linkExclusive } from "../FileNativeExclusiveLink.js";
6
7
  import { dirname, join } from "node:path";
7
8
  import { isDeepStrictEqual } from "node:util";
8
9
  import { fictionGameplay } from "../../protocol/gameplay.js";
10
+ import { validGoogleInteractionsSteps } from "../model/ProviderExchangeState.js";
11
+ import { hasDamagedGoogleSteps, withoutDamagedGoogleSteps, } from "../authoring/DamagedGoogleContinuation.js";
9
12
  import { ContentWorkspace } from "../content/ContentWorkspace.js";
10
13
  import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
11
14
  import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
@@ -28,19 +31,35 @@ export class FileNativeSettingImprovementStore {
28
31
  createId() {
29
32
  return `setting-${randomUUID()}`;
30
33
  }
34
+ recordGoogleHop(sessionId, requestId, hop) {
35
+ assertSessionId(sessionId);
36
+ return writeGoogleHop(join(this.#sessionsRoot, ".google-hops", sessionId), requestId, hop, () => stat(this.#path(sessionId)).then(() => true, () => false));
37
+ }
31
38
  async read(sessionId) {
32
39
  await this.#mutationTail;
33
- return this.#read(sessionId);
40
+ try {
41
+ const session = await this.#read(sessionId);
42
+ if (hasDamagedGoogleSteps(session) &&
43
+ (await this.#damagedSummary(sessionId)))
44
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context");
45
+ return session;
46
+ }
47
+ catch (error) {
48
+ if (await this.#damagedSummary(sessionId))
49
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context", { cause: error });
50
+ throw error;
51
+ }
34
52
  }
35
53
  async findLatestByPackage(packageId) {
36
54
  const summaries = await this.listSummariesByPackage(packageId);
37
- return summaries[0] === undefined
38
- ? null
39
- : this.read(summaries[0].sessionId);
55
+ const latest = summaries.find((summary) => !summary.damagedContinuation);
56
+ return latest === undefined ? null : this.read(latest.sessionId);
40
57
  }
41
58
  async findByCreationRequest(packageId, requestId) {
42
59
  const summaries = await this.listSummariesByPackage(packageId);
43
60
  const match = summaries.find(({ creationRequestId }) => creationRequestId === requestId);
61
+ if (match?.damagedContinuation)
62
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context with a new request ID");
44
63
  return match === undefined ? null : this.read(match.sessionId);
45
64
  }
46
65
  async listSummariesByPackage(packageId) {
@@ -88,6 +107,10 @@ export class FileNativeSettingImprovementStore {
88
107
  if (session.packageId !== packageId)
89
108
  throw new Error("The setting-improvement conversation does not belong to this content package");
90
109
  await rm(this.#path(sessionId));
110
+ await rm(join(this.#sessionsRoot, ".google-hops", sessionId), {
111
+ recursive: true,
112
+ force: true,
113
+ });
91
114
  await rm(join(this.#sessionsRoot, `${sessionId}.schema-v1.json`), {
92
115
  force: true,
93
116
  });
@@ -117,8 +140,20 @@ export class FileNativeSettingImprovementStore {
117
140
  for (const entry of entries) {
118
141
  if (!entry.isFile() || !sessionFilePattern.test(entry.name))
119
142
  continue;
120
- const session = await this.#read(entry.name.slice(0, -5));
121
- summaries.set(session.sessionId, summarizeStoredSettingImprovementSession(session));
143
+ const sessionId = entry.name.slice(0, -5);
144
+ try {
145
+ const session = await this.#read(sessionId);
146
+ summaries.set(session.sessionId, (!hasDamagedGoogleSteps(session)
147
+ ? null
148
+ : await this.#damagedSummary(sessionId)) ??
149
+ summarizeStoredSettingImprovementSession(session));
150
+ }
151
+ catch (error) {
152
+ const summary = await this.#damagedSummary(sessionId);
153
+ if (summary === null)
154
+ throw error;
155
+ summaries.set(sessionId, summary);
156
+ }
122
157
  }
123
158
  const completedRevision = await directoryRevision(this.#sessionsRoot);
124
159
  if (completedRevision !== revision)
@@ -127,6 +162,39 @@ export class FileNativeSettingImprovementStore {
127
162
  return;
128
163
  }
129
164
  }
165
+ async #damagedSummary(sessionId) {
166
+ let session;
167
+ try {
168
+ const raw = JSON.parse(await readFile(this.#path(sessionId), "utf8"));
169
+ const copy = withoutDamagedGoogleSteps(raw);
170
+ if (copy === null) {
171
+ if (hasDamagedGoogleSteps(raw))
172
+ throw new Error("Cannot safely identify the damaged historical conversation; settlement or identity is unverified");
173
+ return null;
174
+ }
175
+ session = validateStoredSession(copy);
176
+ }
177
+ catch (error) {
178
+ if (error instanceof Error &&
179
+ error.message.startsWith("Cannot safely identify"))
180
+ throw error;
181
+ return null;
182
+ }
183
+ if (session.sessionId !== sessionId ||
184
+ session.googleIdentity?.sessionId !== sessionId ||
185
+ session.googleIdentity.packageId !== session.packageId ||
186
+ session.googleIdentity.creationRequestId !== session.creationRequestId ||
187
+ session.requests?.[0]?.requestId !== session.creationRequestId ||
188
+ !/^package-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(session.packageId) ||
189
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(session.creationRequestId) ||
190
+ hasPendingToolCalls(session))
191
+ throw new Error("Cannot safely identify the damaged historical conversation; its owner or creation identity is unverified");
192
+ return {
193
+ ...summarizeStoredSettingImprovementSession(session),
194
+ runStatus: "interrupted",
195
+ damagedContinuation: true,
196
+ };
197
+ }
130
198
  async #recordSavedSession(session) {
131
199
  if (this.#summaryIndex !== null)
132
200
  this.#cacheSummary(summarizeStoredSettingImprovementSession(session));
@@ -219,6 +287,9 @@ export class FileNativeSettingImprovementStore {
219
287
  const session = validateStoredSession(value);
220
288
  if (session.sessionId !== sessionId)
221
289
  throw new Error("Setting-improvement conversation identity does not match its file");
290
+ for (const request of session.requests ?? [])
291
+ if (request.wireRequest !== undefined)
292
+ request.wireRequest = await readGoogleHops(join(this.#sessionsRoot, ".google-hops", sessionId), request.requestId, request.wireRequest);
222
293
  return session;
223
294
  }
224
295
  async #migrateLegacy(sessionId, source, value) {
@@ -362,7 +433,13 @@ function validateStoredSession(value) {
362
433
  "authorization",
363
434
  "pendingSettlement",
364
435
  "legacyDraft",
365
- ], ["contentPackageTitle", "playPreset", "requests"]) ||
436
+ ], [
437
+ "contentPackageTitle",
438
+ "playPreset",
439
+ "requests",
440
+ "googleOutcomeUnknown",
441
+ "googleIdentity",
442
+ ]) ||
366
443
  (value.schemaVersion !== 2 && value.schemaVersion !== 3) ||
367
444
  (value.schemaVersion === 2
368
445
  ? value.requests !== undefined
@@ -392,12 +469,33 @@ function validateStoredSession(value) {
392
469
  typeof value.activeRequestId !== "string") ||
393
470
  !Array.isArray(value.completedRequestIds) ||
394
471
  (value.lastFailure !== null && typeof value.lastFailure !== "string") ||
472
+ (value.googleOutcomeUnknown !== undefined &&
473
+ (typeof value.googleOutcomeUnknown !== "boolean" ||
474
+ !isRecord(value.modelBinding) ||
475
+ value.modelBinding.provider !== "google_interactions")) ||
476
+ (value.googleIdentity !== undefined &&
477
+ (!isRecord(value.googleIdentity) ||
478
+ !hasExactKeys(value.googleIdentity, [
479
+ "sessionId",
480
+ "packageId",
481
+ "creationRequestId",
482
+ ]) ||
483
+ typeof value.googleIdentity.sessionId !== "string" ||
484
+ typeof value.googleIdentity.packageId !== "string" ||
485
+ typeof value.googleIdentity.creationRequestId !== "string" ||
486
+ !isRecord(value.modelBinding) ||
487
+ value.modelBinding.provider !== "google_interactions")) ||
395
488
  !validAuthorization(value.authorization) ||
396
489
  !validPendingSettlement(value.pendingSettlement) ||
397
490
  !validPendingSettlementForTranscript(value.pendingSettlement, value.modelItems, value.runStatus) ||
398
491
  !validLegacyDraft(value.legacyDraft))
399
492
  throw new Error("Setting-improvement conversation does not match its durable schema");
400
493
  assertSessionId(value.sessionId);
494
+ if (isRecord(value.googleIdentity) &&
495
+ (value.googleIdentity.sessionId !== value.sessionId ||
496
+ value.googleIdentity.packageId !== value.packageId ||
497
+ value.googleIdentity.creationRequestId !== value.creationRequestId))
498
+ throw new Error("Cannot safely identify the historical conversation; its owner or creation identity is inconsistent");
401
499
  if (!value.completedRequestIds.every((id) => typeof id === "string") ||
402
500
  !value.messages.every(validMessage) ||
403
501
  !value.modelItems.every(validModelItem))
@@ -823,6 +921,9 @@ function validModelToolCall(value) {
823
921
  function validProviderState(value) {
824
922
  if (!isRecord(value))
825
923
  return false;
924
+ if (value.protocol === "google_interactions")
925
+ return (hasExactKeys(value, ["protocol", "steps"]) &&
926
+ validGoogleInteractionsSteps(value.steps));
826
927
  if (value.protocol === "chat_completions")
827
928
  return (hasExactKeys(value, ["protocol", "assistantMessage"]) &&
828
929
  Object.hasOwn(value, "assistantMessage"));
@@ -850,7 +951,10 @@ function validModelUsage(value) {
850
951
  "totalTokens",
851
952
  ];
852
953
  if (!isRecord(value) ||
853
- !hasExactKeys(value, [...fields, "provenance"]) ||
954
+ !hasExactKeys(value, [...fields, "provenance"], ["providerRaw", "accounting"]) ||
955
+ (value.providerRaw !== undefined && !isRecord(value.providerRaw)) ||
956
+ (value.accounting !== undefined &&
957
+ value.accounting !== "google_interactions") ||
854
958
  !fields.every((field) => validNullableCount(value[field])) ||
855
959
  !isRecord(value.provenance) ||
856
960
  !hasExactKeys(value.provenance, fields))
@@ -5,7 +5,7 @@ import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
5
5
  import { equalModelHostBinding } from "../model/ModelHost.js";
6
6
  import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
7
7
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
8
- import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
8
+ import { appendAuthoringUserMessage, authoringExchange, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
9
9
  import { summarizeStoredSettingImprovementSession, } from "./FileNativeSettingImprovementStore.js";
10
10
  import { SettingAuthoringTransaction, settingImprovementRuntimeContract, settingImprovementToolDefinitions, authoringPreloadAuthorization, buildAuthoringPreload, contentPackageAuthoringSurface, } from "./SettingAuthoringTransaction.js";
11
11
  class RecoveryWriteConflictError extends Error {
@@ -62,7 +62,7 @@ export class SettingImprovementSession {
62
62
  async overview(packageId) {
63
63
  return this.#mutate(async () => {
64
64
  let summaries = await this.#store.listSummariesByPackage(packageId);
65
- const latestSummary = summaries[0];
65
+ const latestSummary = summaries.find((summary) => !summary.damagedContinuation);
66
66
  const latest = latestSummary === undefined
67
67
  ? null
68
68
  : await this.#recoverIfNeeded(await this.#store.read(latestSummary.sessionId));
@@ -187,6 +187,8 @@ export class SettingImprovementSession {
187
187
  assertPackage(session, input.packageId);
188
188
  }
189
189
  return this.#startRequest(session, requestId, async (next) => {
190
+ if (next.runStatus === "paused")
191
+ throw new Error("Resume the paused authoring request before sending another message");
190
192
  const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.packageId, next.modelBinding);
191
193
  next.schemaVersion = 3;
192
194
  next.requests ??= [];
@@ -199,6 +201,8 @@ export class SettingImprovementSession {
199
201
  });
200
202
  next.authorization = authoringPreloadAuthorization(next.authorization, preload.record);
201
203
  appendAuthoringUserMessage(next, message, requestId);
204
+ if (next.modelBinding.provider === "google_interactions")
205
+ next.googleOutcomeUnknown = false;
202
206
  });
203
207
  });
204
208
  return prepared.run ?? this.#view(prepared.session);
@@ -223,10 +227,16 @@ export class SettingImprovementSession {
223
227
  throw new Error("Another message in this setting-improvement conversation is still running");
224
228
  return { session, run: running.promise };
225
229
  }
230
+ if (session.googleOutcomeUnknown)
231
+ throw new Error("The Google request outcome is unknown; start a fresh model context.");
226
232
  const host = await this.#bindExistingModelHost(session.modelBinding);
227
233
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
228
234
  throw new Error("No saved model connection matches this setting-improvement conversation");
229
235
  await prepare(session);
236
+ if (session.modelBinding.provider === "google_interactions" &&
237
+ host.previewAuthoringRequest !== undefined &&
238
+ session.requests?.at(-1)?.requestId === requestId)
239
+ session.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(session, requestId));
230
240
  await this.#store.save(session);
231
241
  return { session, run: this.#startRun(session, host, requestId) };
232
242
  }
@@ -317,9 +327,10 @@ export class SettingImprovementSession {
317
327
  tools,
318
328
  });
319
329
  const now = Date.now();
330
+ const sessionId = this.#store.createId();
320
331
  const session = {
321
332
  schemaVersion: 2,
322
- sessionId: this.#store.createId(),
333
+ sessionId,
323
334
  packageId,
324
335
  contentPackageTitle: package_.title,
325
336
  locale,
@@ -327,6 +338,9 @@ export class SettingImprovementSession {
327
338
  createdAt: now,
328
339
  updatedAt: now,
329
340
  creationRequestId,
341
+ ...(modelBinding.provider === "google_interactions"
342
+ ? { googleIdentity: { sessionId, packageId, creationRequestId } }
343
+ : {}),
330
344
  bootstrap,
331
345
  modelBinding,
332
346
  playPreset: structuredClone(playPreset),
@@ -353,6 +367,7 @@ export class SettingImprovementSession {
353
367
  host,
354
368
  requestId,
355
369
  save: (next) => this.#store.save(next),
370
+ recordGoogleHop: (id, hop) => this.#store.recordGoogleHop(session.sessionId, id, hop),
356
371
  settleToolResponse: async (next, assistantItemIndex, toolCalls) => {
357
372
  activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, toolCalls, activeTransaction);
358
373
  },
@@ -451,6 +466,8 @@ export class SettingImprovementSession {
451
466
  return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
452
467
  }
453
468
  if (session.pendingSettlement !== null) {
469
+ if (session.modelBinding.provider === "google_interactions")
470
+ session.googleOutcomeUnknown = false;
454
471
  const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
455
472
  let retry = false;
456
473
  try {
@@ -510,7 +527,21 @@ export class SettingImprovementSession {
510
527
  return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
511
528
  return session;
512
529
  }
513
- if (session.runStatus === "running") {
530
+ const lastItem = session.modelItems.at(-1);
531
+ const legacyUnknown = session.modelBinding.provider === "google_interactions" &&
532
+ session.runStatus === "interrupted" &&
533
+ session.googleOutcomeUnknown === undefined &&
534
+ (lastItem?.kind === "user" || lastItem?.kind === "tool");
535
+ if (legacyUnknown) {
536
+ session.googleOutcomeUnknown = true;
537
+ session.lastFailure =
538
+ "The Google request outcome is unknown; start a fresh model context.";
539
+ session.updatedAt = Date.now();
540
+ }
541
+ else if (session.runStatus === "running") {
542
+ if (session.modelBinding.provider === "google_interactions" &&
543
+ session.googleOutcomeUnknown !== false)
544
+ session.googleOutcomeUnknown = true;
514
545
  markInterruptedAfterRestart(session);
515
546
  session.updatedAt = Date.now();
516
547
  }
@@ -606,12 +637,17 @@ function lastPendingAssistant(session) {
606
637
  }
607
638
  function markInterruptedAfterRestart(session) {
608
639
  const requestId = session.activeRequestId;
609
- session.runStatus = "interrupted";
640
+ session.runStatus =
641
+ session.modelBinding.provider === "google_interactions" &&
642
+ session.googleOutcomeUnknown === false
643
+ ? "paused"
644
+ : "interrupted";
610
645
  session.activeRequestId = null;
611
646
  if (requestId !== null)
612
647
  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.";
648
+ session.lastFailure = session.googleOutcomeUnknown
649
+ ? "The Google request outcome is unknown; start a fresh model context."
650
+ : "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
651
  }
616
652
  function requiredMessage(message) {
617
653
  if (message.trim().length === 0)
@@ -750,6 +786,11 @@ function diffForRollback(files, change) {
750
786
  function settingImprovementHistoryItem(summary) {
751
787
  return {
752
788
  sessionId: summary.sessionId,
789
+ ...(summary.damagedContinuation
790
+ ? {
791
+ unavailableReason: "Google continuation is damaged; start a fresh model context.",
792
+ }
793
+ : {}),
753
794
  runStatus: summary.runStatus,
754
795
  createdAt: summary.createdAt,
755
796
  updatedAt: summary.updatedAt,
@@ -3,9 +3,12 @@ import { upgradeLegacyEpochs } from "./LegacyWorldRevisionEpochUpgrade.js";
3
3
  import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
4
4
  import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
- import { mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
6
+ import { readGoogleHops, writeGoogleHop } from "../model/GoogleHopJournal.js";
7
+ import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
7
8
  import { dirname, join, resolve } from "node:path";
8
9
  import { gameplayPath } from "../../protocol/gameplay.js";
10
+ import { validGoogleInteractionsSteps } from "../model/ProviderExchangeState.js";
11
+ import { hasDamagedGoogleSteps, withoutDamagedGoogleSteps, } from "../authoring/DamagedGoogleContinuation.js";
9
12
  import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
10
13
  import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
11
14
  const uuidV4Pattern = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
@@ -46,6 +49,10 @@ export class FileNativeWorldRevisionStore {
46
49
  createSessionId() {
47
50
  return `world-session-${randomUUID()}`;
48
51
  }
52
+ recordGoogleHop(sessionId, requestId, hop) {
53
+ assertSessionId(sessionId);
54
+ return writeGoogleHop(join(this.#sessionsRoot, ".google-hops", sessionId), requestId, hop, () => stat(this.#sessionPath(sessionId)).then(() => true, () => false));
55
+ }
49
56
  createChangeSetId() {
50
57
  return `change-set:${randomUUID()}`;
51
58
  }
@@ -89,7 +96,20 @@ export class FileNativeWorldRevisionStore {
89
96
  });
90
97
  }
91
98
  readSession(sessionId) {
92
- return this.#afterMutations(() => this.#readSession(sessionId));
99
+ return this.#afterMutations(async () => {
100
+ try {
101
+ const session = await this.#readSession(sessionId);
102
+ if (hasDamagedGoogleSteps(session) &&
103
+ (await this.#damagedSession(sessionId)))
104
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context");
105
+ return session;
106
+ }
107
+ catch (error) {
108
+ if (await this.#damagedSession(sessionId))
109
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context", { cause: error });
110
+ throw error;
111
+ }
112
+ });
93
113
  }
94
114
  saveSession(session) {
95
115
  return this.#mutate(async () => {
@@ -110,7 +130,19 @@ export class FileNativeWorldRevisionStore {
110
130
  for (const entry of entries) {
111
131
  if (!entry.isFile() || !sessionName.test(entry.name))
112
132
  continue;
113
- const session = await this.#readSession(entry.name.slice(0, -5));
133
+ const sessionId = entry.name.slice(0, -5);
134
+ let session;
135
+ try {
136
+ session = await this.#readSession(sessionId);
137
+ if (hasDamagedGoogleSteps(session))
138
+ session = (await this.#damagedSession(sessionId)) ?? session;
139
+ }
140
+ catch (error) {
141
+ const damaged = await this.#damagedSession(sessionId);
142
+ if (damaged === null)
143
+ throw error;
144
+ session = damaged;
145
+ }
114
146
  if (session.worldId === worldId)
115
147
  sessions.push(session);
116
148
  }
@@ -118,7 +150,10 @@ export class FileNativeWorldRevisionStore {
118
150
  });
119
151
  }
120
152
  async findByCreationRequest(worldId, requestId) {
121
- return ((await this.listSessions(worldId)).find(({ creationRequestId }) => creationRequestId === requestId) ?? null);
153
+ const match = (await this.listSessions(worldId)).find(({ creationRequestId }) => creationRequestId === requestId);
154
+ if (match?.damagedContinuation)
155
+ throw new Error("The historical conversation has damaged Google continuation; start a fresh model context with a new request ID");
156
+ return match ?? null;
122
157
  }
123
158
  deleteSession(worldId, sessionId) {
124
159
  return this.#mutate(async () => {
@@ -126,6 +161,10 @@ export class FileNativeWorldRevisionStore {
126
161
  if (session.worldId !== worldId)
127
162
  throw new Error("World-revision conversation belongs to another world");
128
163
  await rm(this.#sessionPath(sessionId));
164
+ await rm(join(this.#sessionsRoot, ".google-hops", sessionId), {
165
+ recursive: true,
166
+ force: true,
167
+ });
129
168
  });
130
169
  }
131
170
  async #readEpoch(epochId) {
@@ -155,8 +194,34 @@ export class FileNativeWorldRevisionStore {
155
194
  const session = validateSession(value);
156
195
  if (session.sessionId !== sessionId)
157
196
  throw new Error("World-revision conversation identity does not match its file");
197
+ for (const request of session.requests ?? [])
198
+ if (request.wireRequest !== undefined)
199
+ request.wireRequest = await readGoogleHops(join(this.#sessionsRoot, ".google-hops", sessionId), request.requestId, request.wireRequest);
158
200
  return session;
159
201
  }
202
+ async #damagedSession(sessionId) {
203
+ const raw = JSON.parse(await readFile(this.#sessionPath(sessionId), "utf8"));
204
+ const copy = withoutDamagedGoogleSteps(raw);
205
+ if (copy === null) {
206
+ if (hasDamagedGoogleSteps(raw))
207
+ throw new Error("Cannot safely identify the damaged historical conversation; settlement or identity is unverified");
208
+ return null;
209
+ }
210
+ const session = validateSession(copy);
211
+ if (session.sessionId !== sessionId ||
212
+ session.requests?.[0]?.requestId !== session.creationRequestId ||
213
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(session.creationRequestId))
214
+ throw new Error("Cannot safely identify the damaged historical conversation; its creation identity is unverified");
215
+ const epoch = await this.#readEpoch(session.epochId);
216
+ if (epoch.worldId !== session.worldId)
217
+ throw new Error("Cannot safely identify the damaged historical conversation; its world is inconsistent");
218
+ return {
219
+ ...session,
220
+ damagedContinuation: true,
221
+ runStatus: "interrupted",
222
+ lastFailure: "Google continuation is damaged; start a fresh model context.",
223
+ };
224
+ }
160
225
  async #afterMutations(read) {
161
226
  await this.#mutationTail;
162
227
  return read();
@@ -274,7 +339,7 @@ function validateSession(value) {
274
339
  "lastFailure",
275
340
  "authorization",
276
341
  "pendingSettlement",
277
- ], ["requests"]) ||
342
+ ], ["requests", "googleOutcomeUnknown"]) ||
278
343
  (value.schemaVersion !== 1 && value.schemaVersion !== 2) ||
279
344
  (value.schemaVersion === 1
280
345
  ? value.requests !== undefined
@@ -311,6 +376,10 @@ function validateSession(value) {
311
376
  !Array.isArray(value.completedRequestIds) ||
312
377
  !value.completedRequestIds.every((item) => typeof item === "string") ||
313
378
  (value.lastFailure !== null && typeof value.lastFailure !== "string") ||
379
+ (value.googleOutcomeUnknown !== undefined &&
380
+ (typeof value.googleOutcomeUnknown !== "boolean" ||
381
+ !isRecord(value.modelBinding) ||
382
+ value.modelBinding.provider !== "google_interactions")) ||
314
383
  !validAuthorization(value.authorization) ||
315
384
  !validPendingSettlement(value.pendingSettlement) ||
316
385
  !validPendingForSession(value.pendingSettlement, value.epochId, value.runStatus) ||
@@ -452,6 +521,9 @@ function validModelToolCall(value) {
452
521
  function validProviderState(value) {
453
522
  if (!isRecord(value))
454
523
  return false;
524
+ if (value.protocol === "google_interactions")
525
+ return (hasExactKeys(value, ["protocol", "steps"]) &&
526
+ validGoogleInteractionsSteps(value.steps));
455
527
  if (value.protocol === "chat_completions")
456
528
  return hasExactKeys(value, ["protocol", "assistantMessage"]);
457
529
  if (value.protocol === "openai_responses")
@@ -509,7 +581,8 @@ function validModelBinding(value) {
509
581
  ], ["cacheStrategy"]) &&
510
582
  (value.provider === "chat_completions" ||
511
583
  value.provider === "openai_responses" ||
512
- value.provider === "anthropic_messages") &&
584
+ value.provider === "anthropic_messages" ||
585
+ value.provider === "google_interactions") &&
513
586
  typeof value.endpointFingerprint === "string" &&
514
587
  typeof value.modelId === "string" &&
515
588
  count(value.contextWindowTokens) &&
@@ -531,7 +604,10 @@ function validModelUsage(value) {
531
604
  "totalTokens",
532
605
  ];
533
606
  if (!isRecord(value) ||
534
- !hasExactKeys(value, [...fields, "provenance"]) ||
607
+ !hasExactKeys(value, [...fields, "provenance"], ["providerRaw", "accounting"]) ||
608
+ (value.providerRaw !== undefined && !isRecord(value.providerRaw)) ||
609
+ (value.accounting !== undefined &&
610
+ value.accounting !== "google_interactions") ||
535
611
  !fields.every((field) => value[field] === null || count(value[field])) ||
536
612
  !isRecord(value.provenance) ||
537
613
  !hasExactKeys(value.provenance, fields))
@@ -594,7 +670,7 @@ function validPendingSettlement(value) {
594
670
  function validPendingForSession(pending, epochId, runStatus) {
595
671
  return (pending === null ||
596
672
  (isRecord(pending) &&
597
- runStatus === "running" &&
673
+ (runStatus === "running" || runStatus === "interrupted") &&
598
674
  pending.epochId === epochId));
599
675
  }
600
676
  function validToolResult(value) {