narraeon 0.3.1 → 0.5.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 (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +188 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +447 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-BXfelCi9.js +1207 -0
  70. package/dist/web/assets/index-BYnhUxlk.css +1 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D5VqDvjY.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -0,0 +1,714 @@
1
+ import { authoringRequestPreviews } from "../authoring/AuthoringRequestSnapshot.js";
2
+ import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
3
+ import { InvalidContentTreeError, } from "../content/ContentWorkspace.js";
4
+ import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
5
+ import { equalModelHostBinding } from "../model/ModelHost.js";
6
+ import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
7
+ import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, } from "../authoring/ModelToolConversation.js";
8
+ import { summarizeStoredSettingImprovementSession, } from "./FileNativeSettingImprovementStore.js";
9
+ import { SettingAuthoringTransaction, settingImprovementRuntimeContract, settingImprovementToolDefinitions, } from "./SettingAuthoringTransaction.js";
10
+ class RecoveryWriteConflictError extends Error {
11
+ }
12
+ export class SettingImprovementRollbackError extends Error {
13
+ constructor(message, options) {
14
+ super(message, options);
15
+ this.name = "SettingImprovementRollbackError";
16
+ }
17
+ }
18
+ export class SettingImprovementSession {
19
+ #store;
20
+ #content;
21
+ #compiler;
22
+ #locale;
23
+ #bindModelHost;
24
+ #bindExistingModelHost;
25
+ #bindPlayPreset;
26
+ #preview;
27
+ #conversation = new ModelToolConversation();
28
+ #changed;
29
+ #mutationTail = Promise.resolve();
30
+ constructor(input) {
31
+ this.#changed =
32
+ input.changed ??
33
+ (() => {
34
+ /* No observers attached. */
35
+ });
36
+ this.#store = input.store;
37
+ this.#content = input.content;
38
+ this.#compiler = input.compiler;
39
+ this.#locale = input.locale;
40
+ this.#bindModelHost = input.bindModelHost;
41
+ this.#bindExistingModelHost =
42
+ input.bindExistingModelHost ??
43
+ (async (binding) => {
44
+ const host = await input.bindModelHost();
45
+ if (!equalModelHostBinding(binding, host.binding()))
46
+ throw new Error("The enabled model connection no longer matches this setting-improvement conversation");
47
+ return host;
48
+ });
49
+ this.#bindPlayPreset = input.bindPlayPreset;
50
+ this.#preview = input.preview;
51
+ }
52
+ /** Latest conversation for compatibility with the original read endpoint. */
53
+ async read(packageId) {
54
+ return this.#mutate(async () => {
55
+ const session = await this.#store.findLatestByPackage(packageId);
56
+ return session === null
57
+ ? null
58
+ : this.#view(await this.#recoverIfNeeded(session));
59
+ });
60
+ }
61
+ async overview(packageId) {
62
+ return this.#mutate(async () => {
63
+ let summaries = await this.#store.listSummariesByPackage(packageId);
64
+ const latestSummary = summaries[0];
65
+ const latest = latestSummary === undefined
66
+ ? null
67
+ : await this.#recoverIfNeeded(await this.#store.read(latestSummary.sessionId));
68
+ if (latest !== null) {
69
+ const index = summaries.findIndex(({ sessionId }) => sessionId === latest.sessionId);
70
+ if (index >= 0)
71
+ summaries[index] = summarizeStoredSettingImprovementSession(latest);
72
+ summaries = summaries.sort(compareStoredSummaries);
73
+ }
74
+ return {
75
+ latest: latest === null ? null : this.#view(latest),
76
+ history: summaries.map(settingImprovementHistoryItem),
77
+ };
78
+ });
79
+ }
80
+ observeProgress(status) {
81
+ const selected = status.selected;
82
+ if (selected === null)
83
+ return status;
84
+ return {
85
+ ...status,
86
+ selected: {
87
+ ...selected,
88
+ progress: {
89
+ ...selected.progress,
90
+ streaming: this.#conversation.streaming(selected.sessionId),
91
+ },
92
+ },
93
+ };
94
+ }
95
+ async status(packageId, sessionId) {
96
+ return this.#mutate(async () => {
97
+ let status = await this.#store.summaryStatusByPackage(packageId, sessionId);
98
+ let selected = status.selected;
99
+ if (selected?.needsRecovery === true &&
100
+ !this.#conversation.isRunning(selected.sessionId)) {
101
+ const recovered = await this.#recoverIfNeeded(await this.#store.read(selected.sessionId));
102
+ selected = summarizeStoredSettingImprovementSession(recovered);
103
+ status = await this.#store.summaryStatusByPackage(packageId, sessionId);
104
+ }
105
+ return {
106
+ revision: status.revision,
107
+ selected: selected === null ? null : this.#statusView(selected),
108
+ };
109
+ });
110
+ }
111
+ async readSession(packageId, sessionId) {
112
+ return this.#mutate(async () => {
113
+ const stored = await this.#store.read(sessionId);
114
+ assertPackage(stored, packageId);
115
+ return this.#view(await this.#recoverIfNeeded(stored));
116
+ });
117
+ }
118
+ async deleteSession(packageId, sessionId) {
119
+ await this.#mutate(async () => {
120
+ if (this.#conversation.isRunning(sessionId))
121
+ throw new Error("A running setting-improvement conversation cannot be deleted");
122
+ const stored = await this.#store.read(sessionId);
123
+ assertPackage(stored, packageId);
124
+ await this.#recoverIfNeeded(stored);
125
+ await this.#store.deleteSession(packageId, sessionId);
126
+ });
127
+ return this.overview(packageId);
128
+ }
129
+ /**
130
+ * Restore one selected file from a historical AI tool call. The explicit
131
+ * rollback wins over the file's current contents; the transcript remains
132
+ * append-only.
133
+ */
134
+ async rollback(input) {
135
+ return this.#mutate(async () => {
136
+ if (this.#conversation.isRunning(input.sessionId))
137
+ throw new SettingImprovementRollbackError("A running setting-improvement conversation cannot be rolled back");
138
+ let session = await this.#store.read(input.sessionId);
139
+ if (session.packageId !== input.packageId)
140
+ throw new SettingImprovementRollbackError("The setting-improvement conversation does not belong to this content package");
141
+ session = await this.#recoverIfNeeded(session);
142
+ const change = rollbackChangeFor(session, input.changeSetId, input.path);
143
+ const lease = await this.#content.beginCurrentTreeContentPackageOperation(input.packageId);
144
+ try {
145
+ if (treeMatchesChangeBefore(lease.package.files, change))
146
+ return {
147
+ status: "already_rolled_back",
148
+ changeSetId: input.changeSetId,
149
+ path: input.path,
150
+ changes: [],
151
+ };
152
+ const rollbackDiff = diffForRollback(lease.package.files, change);
153
+ const restoredFiles = restoreChangeBefore(lease.package.files, change);
154
+ try {
155
+ this.#content.validateCurrentTreeContentPackage(restoredFiles);
156
+ await lease.replace(restoredFiles);
157
+ }
158
+ catch (error) {
159
+ if (!(error instanceof InvalidContentTreeError))
160
+ throw error;
161
+ throw new SettingImprovementRollbackError("The recorded previous files no longer satisfy the current storage boundary", { cause: error });
162
+ }
163
+ return {
164
+ status: "rolled_back",
165
+ changeSetId: input.changeSetId,
166
+ path: input.path,
167
+ changes: [rollbackDiff],
168
+ };
169
+ }
170
+ finally {
171
+ lease.release();
172
+ }
173
+ });
174
+ }
175
+ async send(input) {
176
+ const message = requiredMessage(input.message);
177
+ const requestId = requiredRequestId(input.requestId);
178
+ const prepared = await this.#mutate(async () => {
179
+ let session;
180
+ if (input.continuation.kind === "fresh_context") {
181
+ session =
182
+ (await this.#store.findByCreationRequest(input.packageId, requestId)) ?? (await this.#create(input.packageId, requestId));
183
+ }
184
+ else {
185
+ session = await this.#store.read(input.continuation.sessionId);
186
+ assertPackage(session, input.packageId);
187
+ }
188
+ session = await this.#recoverIfNeeded(session);
189
+ if (session.completedRequestIds.includes(requestId))
190
+ return { session, run: null };
191
+ const running = this.#conversation.running(session.sessionId);
192
+ if (running !== null) {
193
+ if (running.requestId !== requestId)
194
+ throw new Error("Another message in this setting-improvement conversation is still running");
195
+ return { session, run: running.promise };
196
+ }
197
+ const host = await this.#bindExistingModelHost(session.modelBinding);
198
+ if (!equalModelHostBinding(session.modelBinding, host.binding()))
199
+ throw new Error("No saved model connection matches this setting-improvement conversation");
200
+ const { playPreset, bootstrap } = await this.#compileCurrent(input.packageId, session.modelBinding);
201
+ session.schemaVersion = 3;
202
+ session.requests ??= [];
203
+ session.requests.push({
204
+ requestId,
205
+ modelItemStart: session.modelItems.length,
206
+ bootstrap,
207
+ playPreset: structuredClone(playPreset),
208
+ });
209
+ appendAuthoringUserMessage(session, message, requestId);
210
+ await this.#store.save(session);
211
+ return { session, run: this.#startRun(session, host, requestId) };
212
+ });
213
+ return prepared.run ?? this.#view(prepared.session);
214
+ }
215
+ async preview(packageId, sessionId) {
216
+ const session = sessionId === undefined ? null : await this.#store.read(sessionId);
217
+ if (session !== null)
218
+ assertPackage(session, packageId);
219
+ const host = session === null
220
+ ? await this.#bindModelHost()
221
+ : await this.#bindExistingModelHost(session.modelBinding);
222
+ if (session !== null &&
223
+ !equalModelHostBinding(session.modelBinding, host.binding()))
224
+ throw new Error("No compatible saved model connection matches this authoring conversation");
225
+ const { bootstrap } = await this.#compileCurrent(packageId, host.binding());
226
+ return bootstrap;
227
+ }
228
+ async #compileCurrent(packageId, modelBinding) {
229
+ const playPreset = await this.#bindPlayPreset();
230
+ const locale = this.#locale();
231
+ const package_ = await this.#content.readCurrentTreeContentPackage(packageId);
232
+ const bootstrap = this.#compiler.compileSettingImprovement({
233
+ contentPackageTitle: package_.title,
234
+ runtimeContract: settingImprovementRuntimeContract(locale),
235
+ authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
236
+ playPreset,
237
+ modelBinding: modelBinding,
238
+ tools: settingImprovementToolDefinitions(locale),
239
+ });
240
+ return { bootstrap, playPreset };
241
+ }
242
+ async cancel(sessionId) {
243
+ const running = this.#conversation.cancel(sessionId);
244
+ if (running !== null)
245
+ return running;
246
+ return this.#mutate(async () => this.#view(await this.#recoverIfNeeded(await this.#store.read(sessionId))));
247
+ }
248
+ async #create(packageId, creationRequestId) {
249
+ const [package_, host, playPreset] = await Promise.all([
250
+ this.#content.readCurrentTreeContentPackage(packageId),
251
+ this.#bindModelHost(),
252
+ this.#bindPlayPreset(),
253
+ ]);
254
+ const locale = this.#locale();
255
+ const modelBinding = host.binding();
256
+ const tools = settingImprovementToolDefinitions(locale);
257
+ const bootstrap = this.#compiler.compileSettingImprovement({
258
+ contentPackageTitle: package_.title,
259
+ runtimeContract: settingImprovementRuntimeContract(locale),
260
+ authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
261
+ playPreset,
262
+ modelBinding,
263
+ tools,
264
+ });
265
+ const now = Date.now();
266
+ const session = {
267
+ schemaVersion: 2,
268
+ sessionId: this.#store.createId(),
269
+ packageId,
270
+ contentPackageTitle: package_.title,
271
+ locale,
272
+ runStatus: "ready",
273
+ createdAt: now,
274
+ updatedAt: now,
275
+ creationRequestId,
276
+ bootstrap,
277
+ modelBinding,
278
+ playPreset: structuredClone(playPreset),
279
+ modelItems: [],
280
+ messages: [],
281
+ usage: emptyAggregatedModelUsage(),
282
+ exchange: 0,
283
+ toolCalls: 0,
284
+ activeRequestId: null,
285
+ completedRequestIds: [],
286
+ lastFailure: null,
287
+ authorization: null,
288
+ pendingSettlement: null,
289
+ legacyDraft: null,
290
+ };
291
+ await this.#store.save(session);
292
+ return session;
293
+ }
294
+ #startRun(session, host, requestId) {
295
+ let activeTransaction = null;
296
+ return this.#conversation.run({
297
+ changed: (durable) => this.#changed(session.packageId, durable),
298
+ session,
299
+ host,
300
+ requestId,
301
+ save: (next) => this.#store.save(next),
302
+ settleToolResponse: async (next, assistantItemIndex, toolCalls) => {
303
+ activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, toolCalls, activeTransaction);
304
+ },
305
+ view: (next) => this.#view(next),
306
+ failure: {
307
+ emptyResponse: "The model returned an empty tool-free authoring response",
308
+ exchangeLimit: "Setting improvement exceeded 64 model exchanges for one user message",
309
+ cancelled: "The setting-improvement response was cancelled. Complete tool responses already published to the current tree were retained.",
310
+ interrupted: "The setting-improvement response was interrupted",
311
+ },
312
+ });
313
+ }
314
+ async #settleToolResponse(session, assistantItemIndex, calls, active) {
315
+ const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
316
+ try {
317
+ const beforeFingerprint = contentTreeFingerprint(lease.package.files);
318
+ session.pendingSettlement = {
319
+ phase: "response_confirmed",
320
+ assistantItemIndex,
321
+ beforeFingerprint,
322
+ };
323
+ session.updatedAt = Date.now();
324
+ // Persist the complete Provider-native assistant item together with the
325
+ // exact tree revision it may settle against before executing any call.
326
+ await this.#store.save(session);
327
+ const transaction = active?.revision === beforeFingerprint
328
+ ? active.transaction
329
+ : this.#openTransaction(session, lease.package.files, beforeFingerprint);
330
+ const results = transaction.execute(calls);
331
+ const afterFiles = transaction.files();
332
+ const afterFingerprint = contentTreeFingerprint(afterFiles);
333
+ const prepared = {
334
+ phase: "publication_prepared",
335
+ assistantItemIndex,
336
+ beforeFingerprint,
337
+ afterFingerprint,
338
+ toolResults: structuredClone(results),
339
+ authorization: transaction.authorization(afterFingerprint),
340
+ };
341
+ session.pendingSettlement = prepared;
342
+ session.updatedAt = Date.now();
343
+ await this.#store.save(session);
344
+ if (afterFingerprint !== beforeFingerprint) {
345
+ try {
346
+ await lease.replace(afterFiles);
347
+ }
348
+ catch (error) {
349
+ if (!(error instanceof InvalidContentTreeError))
350
+ throw error;
351
+ throw new Error("A current-tree transaction passed per-call validation but publication rejected it", { cause: error });
352
+ }
353
+ }
354
+ this.#finalizeSettlement(session, prepared);
355
+ session.updatedAt = Date.now();
356
+ await this.#store.save(session);
357
+ return { revision: afterFingerprint, transaction };
358
+ }
359
+ finally {
360
+ lease.release();
361
+ }
362
+ }
363
+ #openTransaction(session, files, revision) {
364
+ return new SettingAuthoringTransaction({
365
+ baseFiles: files,
366
+ locale: session.locale,
367
+ revision,
368
+ authorization: session.authorization,
369
+ validateFiles: (files) => this.#content.validateCurrentTreeContentPackage(files),
370
+ preview: (snapshot) => this.#preview(snapshot, session.modelBinding, session.requests?.at(-1)?.playPreset ?? session.playPreset),
371
+ });
372
+ }
373
+ #finalizeSettlement(session, pending) {
374
+ for (const result of pending.toolResults)
375
+ session.modelItems.push({
376
+ kind: "tool",
377
+ toolCallId: result.toolCallId,
378
+ markdown: result.markdown,
379
+ ...(result.isError ? { isError: true } : {}),
380
+ changes: structuredClone(result.changes),
381
+ });
382
+ session.authorization = structuredClone(pending.authorization);
383
+ session.toolCalls += pending.toolResults.length;
384
+ session.pendingSettlement = null;
385
+ }
386
+ async #recoverIfNeeded(original) {
387
+ if (this.#conversation.isRunning(original.sessionId))
388
+ return original;
389
+ let expected = original;
390
+ const session = structuredClone(original);
391
+ const pendingAssistant = lastPendingAssistant(session);
392
+ if (session.pendingSettlement === null && pendingAssistant !== null) {
393
+ 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.");
394
+ markInterruptedAfterRestart(session);
395
+ session.updatedAt = Date.now();
396
+ if (await this.#store.saveIfUnchanged(expected, session))
397
+ return session;
398
+ return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
399
+ }
400
+ if (session.pendingSettlement !== null) {
401
+ const lease = await this.#content.beginCurrentTreeContentPackageOperation(session.packageId);
402
+ let retry = false;
403
+ try {
404
+ try {
405
+ let fingerprint = contentTreeFingerprint(lease.package.files);
406
+ const pending = session.pendingSettlement;
407
+ let prepared = pending.phase === "publication_prepared" ? pending : null;
408
+ if (fingerprint === pending.beforeFingerprint) {
409
+ const assistant = assistantAt(session, pending.assistantItemIndex);
410
+ const transaction = this.#openTransaction(session, lease.package.files, fingerprint);
411
+ const results = transaction.execute(assistant.toolCalls);
412
+ const afterFiles = transaction.files();
413
+ const afterFingerprint = contentTreeFingerprint(afterFiles);
414
+ prepared = {
415
+ phase: "publication_prepared",
416
+ assistantItemIndex: pending.assistantItemIndex,
417
+ beforeFingerprint: fingerprint,
418
+ afterFingerprint,
419
+ toolResults: structuredClone(results),
420
+ authorization: transaction.authorization(afterFingerprint),
421
+ };
422
+ session.pendingSettlement = prepared;
423
+ session.updatedAt = Date.now();
424
+ if (!(await this.#store.saveIfUnchanged(expected, session)))
425
+ throw new RecoveryWriteConflictError();
426
+ expected = structuredClone(session);
427
+ if (afterFingerprint !== fingerprint) {
428
+ await lease.replace(afterFiles);
429
+ fingerprint = afterFingerprint;
430
+ }
431
+ }
432
+ if (prepared === null) {
433
+ this.#finalizeUnconfirmedToolResponse(session, pending.assistantItemIndex, "The content package changed after Runtime confirmed this tool response but before it could prepare publication. The calls were not replayed against the different tree; read the current tree again before editing.");
434
+ }
435
+ else if (fingerprint === prepared.afterFingerprint) {
436
+ this.#finalizeSettlement(session, prepared);
437
+ }
438
+ else {
439
+ 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.");
440
+ }
441
+ markInterruptedAfterRestart(session);
442
+ session.updatedAt = Date.now();
443
+ if (!(await this.#store.saveIfUnchanged(expected, session)))
444
+ throw new RecoveryWriteConflictError();
445
+ }
446
+ catch (error) {
447
+ if (error instanceof RecoveryWriteConflictError)
448
+ retry = true;
449
+ else
450
+ throw error;
451
+ }
452
+ }
453
+ finally {
454
+ lease.release();
455
+ }
456
+ if (retry)
457
+ return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
458
+ return session;
459
+ }
460
+ if (session.runStatus === "running") {
461
+ markInterruptedAfterRestart(session);
462
+ session.updatedAt = Date.now();
463
+ }
464
+ else {
465
+ return session;
466
+ }
467
+ if (await this.#store.saveIfUnchanged(expected, session))
468
+ return session;
469
+ return this.#recoverIfNeeded(await this.#store.read(session.sessionId));
470
+ }
471
+ #finalizeUnconfirmedToolResponse(session, assistantItemIndex, message) {
472
+ const assistant = assistantAt(session, assistantItemIndex);
473
+ for (const call of assistant.toolCalls)
474
+ session.modelItems.push({
475
+ kind: "tool",
476
+ toolCallId: call.id,
477
+ markdown: `# Current-tree settlement could not be confirmed\n\n${message}`,
478
+ isError: true,
479
+ changes: [],
480
+ });
481
+ session.authorization = null;
482
+ session.toolCalls += assistant.toolCalls.length;
483
+ session.pendingSettlement = null;
484
+ }
485
+ #statusView(summary) {
486
+ const streaming = this.#conversation.streaming(summary.sessionId);
487
+ return {
488
+ sessionId: summary.sessionId,
489
+ runStatus: streaming === null ? summary.runStatus : "running",
490
+ progress: {
491
+ exchange: summary.exchange,
492
+ toolCalls: summary.toolCalls,
493
+ streaming,
494
+ updatedAt: summary.updatedAt,
495
+ },
496
+ };
497
+ }
498
+ #view(session) {
499
+ const streaming = session.runStatus === "running"
500
+ ? this.#conversation.streaming(session.sessionId)
501
+ : null;
502
+ return {
503
+ requestPreviews: authoringRequestPreviews(session),
504
+ sessionId: session.sessionId,
505
+ packageId: session.packageId,
506
+ runStatus: streaming === null ? session.runStatus : "running",
507
+ messages: structuredClone(session.messages),
508
+ turns: settingConversationTurns(session),
509
+ legacyDraft: session.legacyDraft === null
510
+ ? null
511
+ : structuredClone(session.legacyDraft),
512
+ usage: structuredClone(session.usage),
513
+ progress: {
514
+ exchange: session.exchange,
515
+ toolCalls: session.toolCalls,
516
+ streaming,
517
+ updatedAt: session.updatedAt,
518
+ },
519
+ lastFailure: session.lastFailure,
520
+ };
521
+ }
522
+ async #mutate(operation) {
523
+ const previous = this.#mutationTail;
524
+ let release = () => undefined;
525
+ this.#mutationTail = new Promise((resolve) => {
526
+ release = resolve;
527
+ });
528
+ await previous;
529
+ try {
530
+ return await operation();
531
+ }
532
+ finally {
533
+ release();
534
+ }
535
+ }
536
+ }
537
+ function assertPackage(session, packageId) {
538
+ if (session.packageId !== packageId)
539
+ throw new Error("The setting-improvement conversation does not belong to this content package");
540
+ }
541
+ function assistantAt(session, itemIndex) {
542
+ const item = session.modelItems[itemIndex];
543
+ if (item?.kind !== "assistant" || item.toolCalls.length === 0)
544
+ throw new Error("Pending setting-improvement settlement is damaged");
545
+ return item;
546
+ }
547
+ function lastPendingAssistant(session) {
548
+ const itemIndex = session.modelItems.length - 1;
549
+ const item = session.modelItems[itemIndex];
550
+ return item?.kind === "assistant" && item.toolCalls.length > 0
551
+ ? { itemIndex, toolCalls: item.toolCalls }
552
+ : null;
553
+ }
554
+ function markInterruptedAfterRestart(session) {
555
+ const requestId = session.activeRequestId;
556
+ session.runStatus = "interrupted";
557
+ session.activeRequestId = null;
558
+ if (requestId !== null)
559
+ session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
560
+ session.lastFailure =
561
+ "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.";
562
+ }
563
+ function requiredMessage(message) {
564
+ if (message.trim().length === 0)
565
+ throw new Error("A setting-improvement message cannot be empty");
566
+ if (message.length > 64 * 1024)
567
+ throw new Error("The setting-improvement message is too large");
568
+ return message;
569
+ }
570
+ function requiredRequestId(requestId) {
571
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(requestId))
572
+ throw new Error("Invalid setting-improvement request ID");
573
+ return requestId;
574
+ }
575
+ function settingConversationTurns(session) {
576
+ const userMessages = session.messages.filter(({ role }) => role === "user");
577
+ const turns = [];
578
+ let userIndex = 0;
579
+ let exchange = 0;
580
+ let current = null;
581
+ let pendingToolCalls = new Map();
582
+ for (const [itemIndex, item] of session.modelItems.entries()) {
583
+ if (item.kind === "user") {
584
+ const persisted = userMessages[userIndex];
585
+ userIndex += 1;
586
+ current = {
587
+ id: persisted?.id ?? `${session.sessionId}:turn:${itemIndex}`,
588
+ user: persisted === undefined
589
+ ? {
590
+ id: `${session.sessionId}:user:${itemIndex}`,
591
+ role: "user",
592
+ text: item.text,
593
+ createdAt: session.createdAt,
594
+ }
595
+ : structuredClone(persisted),
596
+ exchanges: [],
597
+ };
598
+ turns.push(current);
599
+ pendingToolCalls = new Map();
600
+ continue;
601
+ }
602
+ if (item.kind === "assistant") {
603
+ if (current === null)
604
+ continue;
605
+ exchange += 1;
606
+ const toolCalls = item.toolCalls.map((call) => ({
607
+ callId: call.id,
608
+ name: call.name,
609
+ arguments: structuredClone(call.arguments),
610
+ result: null,
611
+ }));
612
+ current.exchanges.push({
613
+ id: `${session.sessionId}:exchange:${itemIndex}`,
614
+ exchange,
615
+ text: item.text,
616
+ ...(item.reasoningContent === undefined
617
+ ? {}
618
+ : { reasoning: item.reasoningContent }),
619
+ toolCalls,
620
+ });
621
+ pendingToolCalls = new Map(toolCalls.map((call) => [call.callId, call]));
622
+ continue;
623
+ }
624
+ if (item.kind === "tool") {
625
+ const call = pendingToolCalls.get(item.toolCallId);
626
+ if (call?.result === null)
627
+ call.result = {
628
+ markdown: item.markdown,
629
+ isError: item.isError === true,
630
+ changes: structuredClone(item.changes),
631
+ changeSetId: item.isError === true || item.changes.length === 0
632
+ ? null
633
+ : changeSetIdForItem(itemIndex),
634
+ };
635
+ }
636
+ }
637
+ return turns;
638
+ }
639
+ function changeSetIdForItem(itemIndex) {
640
+ return `change-set:${itemIndex}`;
641
+ }
642
+ function rollbackChangeFor(session, changeSetId, path) {
643
+ const result = settingConversationTurns(session)
644
+ .flatMap(({ exchanges }) => exchanges)
645
+ .flatMap(({ toolCalls }) => toolCalls)
646
+ .map(({ result }) => result)
647
+ .find((candidate) => candidate?.changeSetId === changeSetId);
648
+ if (result === undefined ||
649
+ result === null ||
650
+ result.isError ||
651
+ result.changes.length === 0)
652
+ throw new SettingImprovementRollbackError("The requested AI change set does not exist in this conversation");
653
+ const matches = result.changes.filter((change) => change.path === path);
654
+ const change = matches[0];
655
+ if (matches.length !== 1 ||
656
+ change === undefined ||
657
+ (change.kind === "create" &&
658
+ (change.before !== null || change.after === null)) ||
659
+ (change.kind === "modify" &&
660
+ (change.before === null ||
661
+ change.after === null ||
662
+ change.before === change.after)) ||
663
+ (change.kind === "delete" &&
664
+ (change.before === null || change.after !== null)))
665
+ throw new SettingImprovementRollbackError("The requested file does not exist in this AI change set");
666
+ return structuredClone(change);
667
+ }
668
+ function treeMatchesChangeBefore(files, change) {
669
+ const byPath = new Map(files.map((file) => [file.path, file]));
670
+ const expected = change.before;
671
+ const current = byPath.get(change.path);
672
+ if (expected === null)
673
+ return current === undefined;
674
+ return (current !== undefined &&
675
+ current.encoding === undefined &&
676
+ current.contents === expected);
677
+ }
678
+ function restoreChangeBefore(files, change) {
679
+ const restored = new Map(files.map((file) => [file.path, structuredClone(file)]));
680
+ if (change.before === null)
681
+ restored.delete(change.path);
682
+ else
683
+ restored.set(change.path, { path: change.path, contents: change.before });
684
+ return [...restored.values()].sort((left, right) => left.path.localeCompare(right.path));
685
+ }
686
+ function diffForRollback(files, change) {
687
+ const current = files.find(({ path }) => path === change.path);
688
+ const before = current?.contents ?? null;
689
+ const after = change.before;
690
+ return {
691
+ path: change.path,
692
+ kind: before === null ? "create" : after === null ? "delete" : "modify",
693
+ before,
694
+ after,
695
+ };
696
+ }
697
+ function settingImprovementHistoryItem(summary) {
698
+ return {
699
+ sessionId: summary.sessionId,
700
+ runStatus: summary.runStatus,
701
+ createdAt: summary.createdAt,
702
+ updatedAt: summary.updatedAt,
703
+ excerpt: summary.excerpt,
704
+ turnCount: summary.turnCount,
705
+ exchangeCount: summary.exchangeCount,
706
+ toolCallCount: summary.toolCallCount,
707
+ changedFileCount: summary.changedFileCount,
708
+ };
709
+ }
710
+ function compareStoredSummaries(left, right) {
711
+ return (right.updatedAt - left.updatedAt ||
712
+ right.createdAt - left.createdAt ||
713
+ right.sessionId.localeCompare(left.sessionId));
714
+ }