narraeon 0.7.9 → 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 (45) 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/SettingAuthoringTransaction.js +100 -41
  29. package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
  30. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
  31. package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
  32. package/dist/node/shared/application-preset-files.js +29 -0
  33. package/dist/node/shared/default-preset-host.js +50 -38
  34. package/dist/node/shared/first-party-action-choices.js +7 -11
  35. package/dist/node/shared/first-party-generic-panels.js +7 -11
  36. package/dist/node/shared/first-party-player-view.js +7 -11
  37. package/dist/node/shared/ordered-author-prompts.js +6 -4
  38. package/dist/node/shared/ordered-play-prompts.js +4 -2
  39. package/dist/node/shared/prompt-arrangement.js +205 -0
  40. package/dist/node/shared/sillytavern-preset.js +454 -0
  41. package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
  42. package/dist/web/assets/index-C_Qta8UL.js +1432 -0
  43. package/dist/web/index.html +2 -2
  44. package/package.json +1 -1
  45. package/dist/web/assets/index-B2kbaOGe.js +0 -1408
@@ -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) {
@@ -1,7 +1,8 @@
1
1
  import { authoringRequestPreviews } from "../authoring/AuthoringRequestSnapshot.js";
2
2
  import { createHash } from "node:crypto";
3
+ import { isDeepStrictEqual } from "node:util";
3
4
  import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
4
- import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
5
+ import { appendAuthoringUserMessage, authoringExchange, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
5
6
  import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
6
7
  import { equalModelHostBinding } from "../model/ModelHost.js";
7
8
  import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
@@ -60,7 +61,7 @@ export class WorldRevisionSession {
60
61
  this.#recoverWorldSessions(worldId),
61
62
  this.#workspace.sealed(worldId),
62
63
  ]);
63
- const latest = sessions[0] ?? null;
64
+ const latest = sessions.find((session) => !session.damagedContinuation) ?? null;
64
65
  const shownEpoch = activeEpoch ?? (await this.#workspace.latest(worldId));
65
66
  return {
66
67
  epoch: shownEpoch === null
@@ -188,15 +189,25 @@ export class WorldRevisionSession {
188
189
  throw new Error("Another message in this world-revision conversation is still running");
189
190
  return { session, run: running.promise };
190
191
  }
192
+ if (session.googleOutcomeUnknown)
193
+ throw new Error("The Google request outcome is unknown; start a fresh model context.");
194
+ if (session.runStatus === "paused")
195
+ throw new Error("Resume the paused authoring request before sending another message");
191
196
  }
192
197
  await this.#assertNoRunningConversation(input.worldId, session?.sessionId);
198
+ let host;
199
+ if (session !== null) {
200
+ host = await this.#bindExistingModelHost(session.modelBinding);
201
+ if (!equalModelHostBinding(session.modelBinding, host.binding()))
202
+ throw new Error("No saved model connection matches this world-revision conversation");
203
+ }
193
204
  const epoch = await this.#workspace.open(input.worldId);
194
205
  session ??= await this.#create(input.worldId, epoch, requestId);
195
206
  if (session.epochId !== epoch.epochId) {
196
207
  retargetSession(session, epoch, this.#locale());
197
208
  await this.#store.saveSession(session);
198
209
  }
199
- const host = await this.#bindExistingModelHost(session.modelBinding);
210
+ host ??= await this.#bindExistingModelHost(session.modelBinding);
200
211
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
201
212
  throw new Error("No saved model connection matches this world-revision conversation");
202
213
  const { playPreset, bootstrap, preload } = await this.#compileCurrent(input.worldId, session.modelBinding);
@@ -214,10 +225,18 @@ export class WorldRevisionSession {
214
225
  if (preload !== null)
215
226
  session.authorization = authoringPreloadAuthorization(session.authorization, preload.record);
216
227
  appendAuthoringUserMessage(session, message, requestId);
228
+ if (session.modelBinding.provider === "google_interactions")
229
+ session.googleOutcomeUnknown = false;
230
+ const boundaries = host.binding().provider === "google_interactions"
231
+ ? await this.#validatedEpochBoundaries(session)
232
+ : undefined;
233
+ if (boundaries !== undefined &&
234
+ host.previewAuthoringRequest !== undefined)
235
+ session.requests.at(-1).wireRequest = host.previewAuthoringRequest(authoringExchange(session, requestId, boundaries));
217
236
  await this.#store.saveSession(session);
218
237
  return {
219
238
  session,
220
- run: this.#startRun(session, host, requestId),
239
+ run: this.#startRun(session, host, requestId, boundaries),
221
240
  };
222
241
  });
223
242
  return prepared.run ?? this.#view(prepared.session);
@@ -245,8 +264,14 @@ export class WorldRevisionSession {
245
264
  if (!equalModelHostBinding(session.modelBinding, host.binding()))
246
265
  throw new Error("No saved model connection matches this world-revision conversation");
247
266
  resumePausedAuthoringConversation(session);
267
+ const boundaries = host.binding().provider === "google_interactions"
268
+ ? await this.#validatedEpochBoundaries(session)
269
+ : undefined;
248
270
  await this.#store.saveSession(session);
249
- return { session, run: this.#startRun(session, host, requestId) };
271
+ return {
272
+ session,
273
+ run: this.#startRun(session, host, requestId, boundaries),
274
+ };
250
275
  });
251
276
  return prepared.run ?? this.#view(prepared.session);
252
277
  }
@@ -388,14 +413,18 @@ export class WorldRevisionSession {
388
413
  await this.#store.saveSession(session);
389
414
  return session;
390
415
  }
391
- #startRun(session, host, requestId) {
416
+ #startRun(session, host, requestId, worldRevisionBoundaries) {
392
417
  let activeTransaction = null;
393
418
  return this.#conversation.run({
394
419
  changed: (durable) => this.#changed(session.worldId, durable),
395
420
  session,
396
421
  host,
397
422
  requestId,
423
+ ...(worldRevisionBoundaries === undefined
424
+ ? {}
425
+ : { worldRevisionBoundaries }),
398
426
  save: (next) => this.#store.saveSession(next),
427
+ recordGoogleHop: (id, hop) => this.#store.recordGoogleHop(session.sessionId, id, hop),
399
428
  settleToolResponse: async (next, assistantItemIndex, calls) => {
400
429
  activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, calls, activeTransaction);
401
430
  },
@@ -406,6 +435,32 @@ export class WorldRevisionSession {
406
435
  },
407
436
  });
408
437
  }
438
+ async #validatedEpochBoundaries(session) {
439
+ const entries = [];
440
+ for (const [itemIndex, item] of session.modelItems.entries()) {
441
+ if (item.kind !== "prompt_delta")
442
+ continue;
443
+ const source = item.logicalMessages[0]?.blocks[0]?.source;
444
+ if (source?.startsWith("runtime:world-revision-epoch:") !== true)
445
+ continue;
446
+ const epochId = source.slice("runtime:world-revision-epoch:".length);
447
+ const epoch = await this.#store.readEpoch(epochId);
448
+ if (epoch.worldId !== session.worldId)
449
+ throw new Error("World-revision epoch boundary belongs to another world");
450
+ const valid = ["en", "zh-CN"].some((locale) => isDeepStrictEqual(item, epochBoundaryItem(epoch, locale)));
451
+ if (!valid)
452
+ throw new Error("World-revision epoch boundary is damaged");
453
+ entries.push({
454
+ itemIndex,
455
+ epochId,
456
+ baseHead: epoch.baseHead,
457
+ markdown: item.logicalMessages[0].markdown,
458
+ });
459
+ }
460
+ if (entries.length > 0 && entries.at(-1)?.epochId !== session.epochId)
461
+ throw new Error("World-revision epoch boundary does not match the current epoch");
462
+ return { currentEpochId: session.epochId, entries };
463
+ }
409
464
  async #settleToolResponse(session, assistantItemIndex, calls, active) {
410
465
  const epoch = await this.#requiredActiveEpoch(session);
411
466
  const beforeRevision = epoch.revision;
@@ -512,9 +567,49 @@ export class WorldRevisionSession {
512
567
  }
513
568
  }
514
569
  async #recoverIfNeeded(session) {
570
+ if (session.damagedContinuation)
571
+ return session;
515
572
  if (this.#conversation.isRunning(session.sessionId))
516
573
  return session;
574
+ const lastItem = session.modelItems.at(-1);
575
+ if (session.pendingSettlement === null &&
576
+ session.modelBinding.provider === "google_interactions" &&
577
+ session.runStatus === "interrupted" &&
578
+ session.googleOutcomeUnknown === undefined &&
579
+ (lastItem?.kind === "user" || lastItem?.kind === "tool")) {
580
+ session.googleOutcomeUnknown = true;
581
+ session.lastFailure =
582
+ "The Google request outcome is unknown; start a fresh model context.";
583
+ session.updatedAt = Date.now();
584
+ await this.#store.saveSession(session);
585
+ return session;
586
+ }
587
+ if (session.pendingSettlement === null &&
588
+ lastItem?.kind === "assistant" &&
589
+ lastItem.toolCalls.length > 0) {
590
+ finalizeUnconfirmed(session, session.modelItems.length - 1, new Error("Runtime found a complete tool response without a persisted worktree settlement intent"));
591
+ if (session.modelBinding.provider === "google_interactions")
592
+ session.googleOutcomeUnknown = false;
593
+ const requestId = session.activeRequestId;
594
+ session.runStatus = "interrupted";
595
+ session.activeRequestId = null;
596
+ if (requestId !== null)
597
+ session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
598
+ session.lastFailure =
599
+ "The complete tool response could not be settled; its calls received failure receipts. Re-read the worktree before editing.";
600
+ session.updatedAt = Date.now();
601
+ await this.#store.saveSession(session);
602
+ return session;
603
+ }
604
+ if (session.runStatus === "running" &&
605
+ session.pendingSettlement === null &&
606
+ session.modelBinding.provider === "google_interactions" &&
607
+ session.googleOutcomeUnknown !== false)
608
+ session.googleOutcomeUnknown = true;
609
+ const recoveringSettlement = session.pendingSettlement !== null;
517
610
  if (session.pendingSettlement !== null) {
611
+ if (session.modelBinding.provider === "google_interactions")
612
+ session.googleOutcomeUnknown = false;
518
613
  const pending = session.pendingSettlement;
519
614
  if (pending.phase === "publication_prepared") {
520
615
  const active = await this.#workspace.active(session.worldId);
@@ -540,15 +635,20 @@ export class WorldRevisionSession {
540
635
  else
541
636
  finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("Runtime restarted before the complete tool response was prepared"));
542
637
  }
543
- if (session.runStatus !== "running" && session.pendingSettlement === null)
638
+ if (session.runStatus !== "running" && !recoveringSettlement)
544
639
  return session;
545
640
  const requestId = session.activeRequestId;
546
- session.runStatus = "interrupted";
641
+ session.runStatus =
642
+ session.modelBinding.provider === "google_interactions" &&
643
+ session.googleOutcomeUnknown === false
644
+ ? "paused"
645
+ : "interrupted";
547
646
  session.activeRequestId = null;
548
647
  if (requestId !== null)
549
648
  session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
550
- session.lastFailure =
551
- "The previous model request stopped after its last settled worktree change. Continue this conversation when ready.";
649
+ session.lastFailure = session.googleOutcomeUnknown
650
+ ? "The Google request outcome is unknown; start a fresh model context."
651
+ : "The previous model request stopped after its last settled worktree change. Continue this conversation when ready.";
552
652
  session.updatedAt = Date.now();
553
653
  await this.#store.saveSession(session);
554
654
  return session;
@@ -624,10 +724,18 @@ export class WorldRevisionSession {
624
724
  }
625
725
  }
626
726
  function retargetSession(session, epoch, locale) {
727
+ session.modelItems.push(epochBoundaryItem(epoch, locale));
728
+ session.epochId = epoch.epochId;
729
+ session.authorization = null;
730
+ session.pendingSettlement = null;
731
+ session.lastFailure = null;
732
+ session.updatedAt = Date.now();
733
+ }
734
+ function epochBoundaryItem(epoch, locale) {
627
735
  const markdown = locale === "zh-CN"
628
736
  ? `# 新的世界修订 epoch\n\n上一轮修订已经应用或放弃。当前工作树是 ${epoch.epochId},从世界端点 ${epoch.baseHead} 重新建立。此前全部 search cursor 和 read 写授权已失效;修改前必须重新读取。`
629
737
  : `# New world-revision epoch\n\nThe previous revision was applied or discarded. Worktree ${epoch.epochId} was rebuilt from world endpoint ${epoch.baseHead}. Every prior search cursor and read authorization is invalid; re-read before writing.`;
630
- session.modelItems.push({
738
+ return {
631
739
  kind: "prompt_delta",
632
740
  logicalMessages: [
633
741
  {
@@ -641,12 +749,7 @@ function retargetSession(session, epoch, locale) {
641
749
  ],
642
750
  },
643
751
  ],
644
- });
645
- session.epochId = epoch.epochId;
646
- session.authorization = null;
647
- session.pendingSettlement = null;
648
- session.lastFailure = null;
649
- session.updatedAt = Date.now();
752
+ };
650
753
  }
651
754
  function finalizeSettlement(session, pending) {
652
755
  for (const result of pending.toolResults)
@@ -745,6 +848,11 @@ function historyItem(session) {
745
848
  const changed = new Set(session.modelItems.flatMap((item) => item.kind === "tool" ? item.changes.map(({ path }) => path) : []));
746
849
  return {
747
850
  sessionId: session.sessionId,
851
+ ...(session.damagedContinuation
852
+ ? {
853
+ unavailableReason: "Google continuation is damaged; start a fresh model context.",
854
+ }
855
+ : {}),
748
856
  epochId: session.epochId,
749
857
  runStatus: session.runStatus,
750
858
  createdAt: session.createdAt,
@@ -0,0 +1,29 @@
1
+ import { stringify } from "yaml";
2
+ /** App-owned defaults contain declarations, never copies of the prompt catalog. */
3
+ export function applicationPresetFilesForLocale(locale) {
4
+ return {
5
+ "preset.yaml": stringify({
6
+ format: "narraeon.play-preset/v3",
7
+ name: locale === "zh-CN" ? "系统推荐" : "System recommended",
8
+ callChain: "call-chain.yaml",
9
+ playPrompts: {},
10
+ authorPrompts: {},
11
+ mergeConsecutiveMessages: false,
12
+ mounts: [],
13
+ extensions: [],
14
+ }),
15
+ "call-chain.yaml": "format: narraeon.play-call-chain/v1\nnarrative: []\nfollowups: []\n",
16
+ "frame.yaml": `format: narraeon.host-frame/v1
17
+ roles:
18
+ runtime_system:
19
+ - builtin: runtime.play-contract
20
+ - builtin: runtime.tool-contract
21
+ - builtin: runtime.operation-contract
22
+ author_instruction:
23
+ - include: world.instructions
24
+ world_context:
25
+ - include: world.context
26
+ - builtin: runtime.coverage
27
+ `,
28
+ };
29
+ }