@jskit-ai/assistant-core 0.1.199 → 0.1.201

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 (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,2220 @@
1
+ import { createConversationProviderFactory } from "./provider.js";
2
+ import { normalizeConversationConfiguration } from "./configuration.js";
3
+ import { runScopedConversationTurn, readPersistentConversation, startPersistentConversationTurn,
4
+ inspectPersistentConversationAdmission, preparePersistentConversationChangeover,
5
+ inspectPersistentConversationDelivery, stopPersistentConversation, deletePersistentConversation } from "./providers/scoped.js";
6
+ import { isDeepStrictEqual } from "node:util";
7
+ import { randomUUID } from "node:crypto";
8
+ import { createConversationTranscript } from "./transcript.js";
9
+ import { createConversationStreams } from "./streams.js";
10
+ import { createReentrantConversationStorage } from "./storage.js";
11
+ import { createCodexConversationStore } from "./agentRun.js";
12
+ import { codexAppServerTurnStateFromAgentRun } from "./codexTurn.js";
13
+ import { hasUnfinishedConversationRewind } from "./runtimeStateUpgrade.js";
14
+ import { createServiceToolCatalog } from "../lib/serviceToolCatalog.js";
15
+ import { createConversationTools } from "./tools.js";
16
+ import { conversationAttachmentIds, createConversationAttachmentReader } from "./attachments.js";
17
+ import { conversationAttachmentManifest } from "../../shared/conversation/attachments.js";
18
+ import { createConversationChangeover, conversationContinuity, conversationHistoryVersions,
19
+ conversationNativeMessages, conversationRequestText } from "./continuity.js";
20
+
21
+ function failure(message, code, statusCode = 409) {
22
+ return Object.assign(new Error(message), { code, statusCode });
23
+ }
24
+
25
+ const submittedMessage = turn => turn.user || turn.system;
26
+ const retainedSegments = state => [...new Map([...state.predecessors, state].map(segment => [segment.segmentId, segment])).values()];
27
+ const nativeIdentity = segment => segment.engine === "codex" ? segment.binding?.threadId
28
+ : segment.engine === "claude" ? segment.binding?.conversationId : segment.binding?.sessionId;
29
+ const pendingSuccessor = state => state.engine !== "api" && state.predecessors.findLast(segment => segment.successorId === state.segmentId &&
30
+ segment.segmentId !== state.segmentId && segment.replacement?.operation === "replace" && !segment.acceptedAt);
31
+ const sameRequest = (left, right) => left.text === right.text && (left.origin || "user") === right.origin &&
32
+ isDeepStrictEqual(left.goal, right.goal) && isDeepStrictEqual(left.data, right.data) &&
33
+ isDeepStrictEqual((left.attachments || []).map(file => file.attachmentId), right.attachments.map(file => file.attachmentId));
34
+
35
+ /** One application conversation API. Storage and authorization remain host facilities. */
36
+ export function createConversationRuntime({ engine: defaultEngine = "api", defaultIntegrationId, storage, authorize, connections, apiClientFactory, apiHistory,
37
+ actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, limits = {} } = {}) {
38
+ if (typeof authorize !== "function") throw new TypeError("Conversation access requires an authorize function.");
39
+ if (apiHistory !== undefined && typeof apiHistory !== "function") throw new TypeError("API request history requires a server-owned selector.");
40
+ if (toolCatalog && (actions || toolPolicy)) throw new TypeError("Supply the existing tool catalog or actions and toolPolicy, not both.");
41
+ storage = (storage || !defaultHost?.conversation) ? createReentrantConversationStorage(storage) : null;
42
+ const transcript = storage ? createConversationTranscript({ storage, applicationTurns: true }) : null;
43
+ const streams = createConversationStreams();
44
+ const entries = new Map();
45
+ let closed = false;
46
+ const providers = createConversationProviderFactory({ connections, apiClientFactory, fetch, limits, actions, toolCatalog });
47
+ const { createDriver } = providers;
48
+ const catalog = toolCatalog || (actions ? createServiceToolCatalog(actions, { ...limits, isActionAvailable: toolPolicy }) : null);
49
+ const maximumToolCalls = limits.maxToolCalls ?? 32;
50
+ if (!Number.isSafeInteger(maximumToolCalls) || maximumToolCalls < 1) throw new TypeError("Invalid application tool-call limit.");
51
+ const maximumInput = limits.maxInputCharacters ?? 32_000;
52
+ if (!Number.isSafeInteger(maximumInput) || maximumInput < 1) throw new TypeError("Invalid conversation input limit.");
53
+ const maximumContinuity = limits.maxContinuityCharacters ?? 128_000;
54
+ if (!Number.isSafeInteger(maximumContinuity) || maximumContinuity < 1) throw new TypeError("Invalid conversation continuity limit.");
55
+ const maximumAttachmentBytes = limits.maxAttachmentBytes ?? 8 * 1024 * 1024;
56
+ if (!Number.isSafeInteger(maximumAttachmentBytes) || maximumAttachmentBytes < 1) throw new TypeError("Invalid conversation attachment byte limit.");
57
+ if (attachments !== undefined && typeof attachments?.resolve !== "function") throw new TypeError("Conversation attachments require an authorized resolve function.");
58
+ const capabilities = entry => {
59
+ const value = { ...entry.driver.capabilities, ...entry.provider?.capabilities,
60
+ attachments: Boolean((attachments || entry.conversation?.prepareInput || entry.conversation?.commands) && entry.driver.capabilities.attachments) };
61
+ if (entry.conversation?.commands) {
62
+ delete value.goalCommands;
63
+ Object.assign(value, { goals: false, configuration: false, wake: false, replacement: false });
64
+ }
65
+ return value;
66
+ };
67
+ if (limits.timeoutMs !== undefined && (!Number.isSafeInteger(limits.timeoutMs) || limits.timeoutMs < 1)) {
68
+ throw new TypeError("Invalid conversation request time limit.");
69
+ }
70
+
71
+ async function access(context, id, operation, entry) {
72
+ if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed");
73
+ if (!await authorize({ context, conversationId: id, operation })) {
74
+ throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
75
+ }
76
+ if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed");
77
+ }
78
+
79
+ function serial(entry, callback) {
80
+ const operation = entry.pending.then(callback);
81
+ entry.pending = operation.catch(() => {});
82
+ return operation;
83
+ }
84
+
85
+ async function emit(entry, event) {
86
+ await Promise.all([...entry.listeners].map(async subscription => {
87
+ try { await access(subscription.context, entry.id, "subscribe", entry); }
88
+ catch { entry.listeners.delete(subscription); return; }
89
+ // Presentation failures cannot interrupt persistence or provider work.
90
+ try { Promise.resolve(subscription.listener(structuredClone({ conversationId: entry.id, ...event }))).catch(() => {}); }
91
+ catch { /* A failed observer does not own the conversation. */ }
92
+ }));
93
+ }
94
+
95
+ async function setPhase(entry, phase, turnId) {
96
+ if (!["", "preparing", "working", "compacting", "retrying"].includes(phase)) throw new Error("Invalid conversation phase from the engine adapter.");
97
+ if ((entry.phase || "") === phase) return;
98
+ entry.phase = phase;
99
+ await emit(entry, { type: "phase", phase, turnId });
100
+ }
101
+
102
+ const readTranscript = entry => entry.conversation
103
+ ? entry.conversation.transcript.readConversationLog() : transcript.readConversationLog(entry.id);
104
+
105
+ function readTranscriptPage(entry, query) {
106
+ if (!entry.conversation) return transcript.readConversationLogPage(entry.id, query);
107
+ if (typeof entry.conversation.transcript.readConversationLogPage !== "function") {
108
+ throw failure("This conversation does not supply paged history.", "conversation_unsupported", 400);
109
+ }
110
+ return entry.conversation.transcript.readConversationLogPage(query);
111
+ }
112
+
113
+ // A bound native conversation keeps its original run, identity and delivery
114
+ // files. This is a read projection, never a runtime-v3 record or a backfill.
115
+ async function readState(entry, context) {
116
+ if (!entry.conversation) return (await storage.read(entry.id, transaction => transaction.readMetadata())).runtime;
117
+ const value = entry.conversation.native && entry.driver.readState
118
+ ? await entry.driver.readState({ conversation: entry.conversation, context })
119
+ : await entry.conversation.read(context);
120
+ entry.segmentId = value.threadId ? `${entry.engine}:${value.threadId}` : null;
121
+ const pending = value.delivery?.engines[entry.engine]?.pending;
122
+ return { engine: entry.engine, configuration: value.configuration, segmentId: entry.segmentId,
123
+ binding: entry.engine === "codex" ? { threadId: value.threadId, codexAppServerRun: value.run }
124
+ : entry.engine === "claude" ? { conversationId: value.threadId } : { sessionId: value.threadId },
125
+ nativeTurn: value.turn, nativeResult: value.nativeResult,
126
+ lastEngine: value.delivery?.lastEngine ?? entry.engine,
127
+ replacement: ["preparing", "ready"].includes(value.delivery?.replacement?.status) ? value.delivery.replacement : null,
128
+ request: pending && { ...pending, text: pending.displayMessage, attachments: pending.displayAttachments || [] } };
129
+ }
130
+
131
+ async function read(entry, representation, context, query) {
132
+ if (entry.conversation?.native?.scoped) return entry.provider.readNative({ input: query, context });
133
+ if (representation === "native" && entry.conversation) {
134
+ const value = entry.driver.readState
135
+ ? await entry.driver.readState({ conversation: entry.conversation, context, representation })
136
+ : await entry.conversation.read(context, representation);
137
+ return value.nativeResult;
138
+ }
139
+ const logical = entry.conversation?.commands ? await entry.conversation.read(context, query) : null;
140
+ if (logical && logical.engine !== entry.engine) {
141
+ entry.engine = logical.engine;
142
+ entry.driver = createDriver(entry.engine, entry.host);
143
+ }
144
+ const metadata = { runtime: logical ? { configuration: logical.configuration,
145
+ segmentId: logical.threadId ? `${logical.engine}:${logical.threadId}` : null } : await readState(entry, context) };
146
+ if (representation === "native") return metadata.runtime.nativeResult;
147
+ const page = logical ? logical.pagination ? { conversationLog: logical.conversationLog, pagination: logical.pagination } : null
148
+ : query ? await readTranscriptPage(entry, query) : null;
149
+ const conversationLog = logical ? logical.conversationLog : page ? page.conversationLog : await readTranscript(entry);
150
+ // Browsing an older page does not change the current turn's control/error state.
151
+ const latest = !logical && query?.beforeTurnId ? (await readTranscriptPage(entry, { limit: 1 })).conversationLog : conversationLog;
152
+ const last = latest.at(-1)?.metadata?.runtime;
153
+ const savedRequest = metadata.runtime.request;
154
+ const nativeTurn = metadata.runtime.nativeTurn || (entry.driver.canonicalTranscript && metadata.runtime.binding?.codexAppServerRun
155
+ ? codexAppServerTurnStateFromAgentRun(metadata.runtime.binding.codexAppServerRun) : null);
156
+ const request = savedRequest && (entry.engine === "api" || savedRequest.attempted || savedRequest.inspectionOnly ||
157
+ entry.active?.requests.has(savedRequest.messageId)) ? savedRequest : null;
158
+ const pendingRequest = request ? Object.fromEntries([
159
+ "messageId", "text", "origin", "data", "attachments", "at", "goal", "steering", "error"
160
+ ].filter(name => Object.hasOwn(request, name)).map(name => [name, request[name]])) : null;
161
+ return {
162
+ id: entry.id, engine: entry.engine, capabilities: capabilities(entry), configuration: metadata.runtime.configuration,
163
+ segmentId: metadata.runtime.segmentId,
164
+ replacement: metadata.runtime.replacement ? { operationId: metadata.runtime.replacement.request?.operationId || metadata.runtime.replacement.operationId,
165
+ reason: metadata.runtime.replacement.reason, segmentId: metadata.runtime.replacement.segmentId } : null,
166
+ status: logical ? logical.status : entry.storageFailure || entry.executionFailure ? "unavailable" : entry.active || nativeTurn?.active ? "working" : metadata.runtime.replacement ? "replacement-pending"
167
+ : metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly) ? "unconfirmed" : "ready",
168
+ phase: logical ? logical.phase || "" : entry.phase || (nativeTurn?.active && nativeTurn.status !== "observation_lost" ? nativeTurn.phase === "compacting" ? "compacting" : "working" : ""),
169
+ error: logical ? logical.error || "" : entry.error || nativeTurn?.error || metadata.runtime.request?.error || last?.error || "",
170
+ goal: entry.goal || null,
171
+ pendingRequest: logical ? logical.pendingRequest || null : pendingRequest,
172
+ ...(logical?.presentation ? { presentation: logical.presentation } : {}),
173
+ conversationLog: conversationLog.filter(turn => !turn.metadata?.runtime?.supersededBy),
174
+ ...(page ? { pagination: page.pagination } : {}),
175
+ streaming: entry.conversation ? await entry.conversation.readStream() : streams.read(entry.id)
176
+ };
177
+ }
178
+
179
+ async function publish(entry, event) {
180
+ if (entry.disposed) return;
181
+ if (entry.conversation?.commands && ["phase", "configuration"].includes(event.type)) {
182
+ return emit(entry, event);
183
+ }
184
+ const payload = event.payload || {};
185
+ if (Object.hasOwn(payload, "goal")) {
186
+ entry.goal = payload.goal;
187
+ await emit(entry, { type: "goal", goal: entry.goal });
188
+ } else if (event.goalChanged) {
189
+ await emit(entry, { type: "goal" });
190
+ }
191
+ const turn = payload.conversationLogPatch?.turn;
192
+ if (turn) {
193
+ await emit(entry, { type: "transcript", patch: payload.conversationLogPatch,
194
+ ...(payload.conversationStream ? { streaming: payload.conversationStream } : {}) });
195
+ const authored = submittedMessage(turn);
196
+ const request = authored && entry.active?.requests.get(authored.messageId);
197
+ // The original receipt already owns this row. Common admission is
198
+ // still resolved by the dispatch continuation outside its queue.
199
+ if (request) request.turnId = turn.turnId;
200
+ for (const message of turn.messages || []) {
201
+ if (!["assistant", "commentary", "thinking"].includes(message.role)) continue;
202
+ await emit(entry, { type: "message", turnId: turn.turnId, messageId: message.messageId,
203
+ ...(message.outputId ? { outputId: message.outputId } : {}),
204
+ ...(["user", "application"].includes(turn.metadata?.runtime?.origin) ? { origin: turn.metadata.runtime.origin } : {}),
205
+ role: message.role, text: message.text, status: "complete", streaming: payload.conversationStream || streams.read(entry.id) });
206
+ }
207
+ }
208
+ if (payload.conversationStream) {
209
+ const snapshot = payload.conversationStream;
210
+ for (const message of snapshot.messages) {
211
+ await emit(entry, { type: "message", ...message, streaming: snapshot });
212
+ }
213
+ if (!snapshot.messages.length) await emit(entry, { type: "message-complete", streaming: snapshot });
214
+ }
215
+ const nativeTurn = payload.agentSession?.turn;
216
+ if (nativeTurn) {
217
+ entry.nativePhase = nativeTurn.active
218
+ ? ["preparing", "compacting", "retrying"].includes(nativeTurn.phase) ? nativeTurn.phase : "working"
219
+ : "";
220
+ const request = entry.active?.request;
221
+ if (!entry.active || request.accepted) {
222
+ if (request?.deliveryCommitted) {
223
+ try { await request.deliveryCommitted; }
224
+ catch { return; } // Admission owns this failure; native cleanup must still settle.
225
+ }
226
+ await setPhase(entry, entry.nativePhase, request?.turnId);
227
+ }
228
+ }
229
+ }
230
+
231
+ async function openProvider(entry, driver, binding, segmentId, owner = (entry.bindingOwner = {})) {
232
+ const isCurrent = () => entry.bindingOwner === owner && (entry.conversation || entry.segmentId === segmentId);
233
+ let conversation;
234
+ if (entry.conversation || driver.canonicalTranscript) {
235
+ const store = entry.conversation ? null : createCodexConversationStore({ storage, scope: entry.id, segmentId, isCurrent, transcript, streams });
236
+ conversation = {
237
+ ...entry.conversation,
238
+ sessionId: entry.id,
239
+ runtime: entry.conversation?.runtime || { store, getSession: store.getSession },
240
+ publish: event => isCurrent() ? publish(entry, event) : undefined,
241
+ checkpoint: entry.conversation?.checkpoint || (input => isCurrent() ? entry.active?.nativeCheckpoint?.(input) : null)
242
+ };
243
+ }
244
+ return providers.open(driver, {
245
+ binding,
246
+ conversation,
247
+ async onFailure(error, { recovered = false } = {}) {
248
+ if (!isCurrent()) return;
249
+ if (recovered && entry.executionFailure !== error) return;
250
+ entry.executionFailure = recovered ? null : error;
251
+ entry.error = error.message;
252
+ await emit(entry, { type: "error", error: error.message });
253
+ },
254
+ async writeBinding(value) {
255
+ if (entry.conversation) throw new Error("A bound native conversation writes identity through its original preparation owner.");
256
+ if (entry.bindingOwner !== owner) throw new Error("A retired provider cannot change a native binding.");
257
+ await storage.write(entry.id, async transaction => {
258
+ const metadata = await transaction.readMetadata();
259
+ if (entry.bindingOwner !== owner || metadata.runtime.segmentId !== segmentId) throw new Error("A retired native conversation cannot change the current binding.");
260
+ if (metadata.runtime.engine === "codex") {
261
+ const { codexAppServerRun: _staleRun, ...next } = value;
262
+ if (Object.hasOwn(metadata.runtime.binding, "codexAppServerRun")) {
263
+ next.codexAppServerRun = metadata.runtime.binding.codexAppServerRun;
264
+ }
265
+ metadata.runtime.binding = next;
266
+ } else metadata.runtime.binding = value;
267
+ await transaction.writeMetadata(metadata);
268
+ });
269
+ }
270
+ });
271
+ }
272
+
273
+ // The production changeover owner sees its existing state contract. Only this
274
+ // storage adapter knows about the common runtime's segment registry and one
275
+ // authored request record; provider binding writes remain independently fenced.
276
+ function changeoverState(entry, request, transaction) {
277
+ const readMetadata = () => transaction ? transaction.readMetadata()
278
+ : storage.read(entry.id, current => current.readMetadata());
279
+ const write = callback => transaction ? callback(transaction) : storage.write(entry.id, callback);
280
+ return {
281
+ async read() {
282
+ const { runtime } = await readMetadata();
283
+ const engines = Object.fromEntries(retainedSegments(runtime).filter(segment => segment.engine !== "api").map(segment => {
284
+ const pending = segment.request;
285
+ return [segment.engine, { seen: structuredClone(segment.seen),
286
+ ...(typeof pending?.message === "string" || pending?.inspectionOnly ? { pending: structuredClone({ ...pending,
287
+ ...(pending.inspectionOnly ? { attempted: true, displayMessage: pending.text, displayAttachments: pending.attachments } : {}) }) } : {}) }];
288
+ }));
289
+ const predecessor = pendingSuccessor(runtime);
290
+ const preparing = runtime.replacement?.request?.operation === "replace" ? runtime.replacement : null;
291
+ return { lastEngine: runtime.lastEngine, engines,
292
+ ...(preparing ? { replacement: { engineId: preparing.engine, status: "preparing",
293
+ operationId: preparing.request.operationId, handover: preparing.request.briefing || "",
294
+ bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: nativeIdentity(runtime) },
295
+ ...(preparing.preparedAt ? { preparedAt: preparing.preparedAt } : {}) } }
296
+ : predecessor ? { replacement: { engineId: runtime.engine, status: "ready",
297
+ operationId: predecessor.replacement.operationId, handover: predecessor.replacement.briefing || "",
298
+ previous: { engineId: predecessor.engine, conversationId: nativeIdentity(predecessor) },
299
+ ...(predecessor.preparedAt ? { preparedAt: predecessor.preparedAt } : {}) } } : {}) };
300
+ },
301
+ write(value) {
302
+ return write(async current => {
303
+ const metadata = await current.readMetadata();
304
+ const runtime = metadata.runtime;
305
+ if (runtime.segmentId !== entry.segmentId || runtime.engine !== entry.engine) throw new Error("The native conversation changed before its delivery was saved.");
306
+ const binding = value.engines[entry.engine];
307
+ if (binding.seen !== undefined) runtime.seen = binding.seen;
308
+ else if (!runtime.request?.inspectionOnly) throw new Error("Native delivery has no saved history cursor.");
309
+ // A pre-preparation v2 reservation has no snapshot. Its confirmed
310
+ // receipt settles only that request; it cannot acknowledge extra history.
311
+ runtime.lastEngine = value.lastEngine;
312
+ if (binding.pending) {
313
+ const authored = runtime.request?.messageId === binding.pending.messageId ? runtime.request
314
+ : request?.input.messageId === binding.pending.messageId ? { ...request.input, at: request.at } : null;
315
+ if (!authored) throw new Error("A native prompt has no matching authored request.");
316
+ // Inspection-only predecessors retain exactly their old authored
317
+ // record. Their absent prompt must never be manufactured on read.
318
+ runtime.request = authored.inspectionOnly ? authored : { ...authored, ...binding.pending };
319
+ } else if (runtime.request && (typeof runtime.request.message === "string" || runtime.request.inspectionOnly ||
320
+ runtime.request.messageId === request?.input.messageId)) {
321
+ delete runtime.request;
322
+ }
323
+ const predecessor = pendingSuccessor(runtime);
324
+ if (predecessor && value.replacement?.status === "accepted") {
325
+ predecessor.acceptedAt = value.replacement.acceptedAt;
326
+ predecessor.successorConversationId = value.replacement.successorConversationId;
327
+ delete runtime.continuity;
328
+ delete runtime.continuityAttachments;
329
+ }
330
+ await current.writeMetadata(metadata);
331
+ });
332
+ }
333
+ };
334
+ }
335
+
336
+ function nativeChangeover(entry, context, request, sendMessage) {
337
+ let observation;
338
+ return createConversationChangeover({
339
+ state: entry.conversation?.state || changeoverState(entry, request),
340
+ captureContext: !entry.conversation, applicationMessages: !entry.conversation,
341
+ presentation: entry.conversation?.presentation || { unconfirmedCode: "conversation_delivery_uncertain",
342
+ unconfirmedMessage: "The previous submission has uncertain delivery. Retry this message to check its native receipt; it will not be resent automatically." },
343
+ transcript: entry.conversation?.transcript || {
344
+ hasMessage: messageId => transcript.conversationMessageIdExists(entry.id, messageId),
345
+ async writeUserMessage({ messageId }) {
346
+ if (observation) await recoverDelivery(entry, context, messageId, { observation, allowActive: true });
347
+ }
348
+ },
349
+ agent: {
350
+ sendMessage,
351
+ async inspectMessageAdmission({ messageId, threadId }) {
352
+ if (entry.conversation) {
353
+ observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
354
+ await access(context, entry.id, request?.operation || "inspectDelivery", entry);
355
+ return { admission: observation.accepted ? "accepted" : "unknown" };
356
+ }
357
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
358
+ if (!threadId || threadId !== nativeIdentity(runtime) || runtime.request?.messageId !== messageId) return null;
359
+ observation = await entry.provider.inspectAdmission({ ...runtime.request, context, configuration: runtime.configuration });
360
+ await access(context, entry.id, request?.operation || "inspectDelivery", entry);
361
+ return { admission: observation.accepted ? "accepted" : "unknown" };
362
+ }
363
+ }
364
+ });
365
+ }
366
+
367
+ async function rememberNative(entry, transaction, turns) {
368
+ const changeover = createConversationChangeover({ state: changeoverState(entry, null, transaction) });
369
+ await changeover.remember({ engineId: entry.engine, messages: conversationNativeMessages(turns) });
370
+ }
371
+
372
+ async function initialize(entry, configuration, requestedEngine) {
373
+ if (entry.conversation) {
374
+ entry.engine = entry.conversation.engine;
375
+ if (requestedEngine && requestedEngine !== entry.engine) throw failure("The selected native engine changed.", "conversation_engine_mismatch");
376
+ entry.driver = createDriver(entry.engine, entry.host);
377
+ if (entry.conversation.commands) return;
378
+ if (entry.conversation.native?.scoped) {
379
+ entry.provider = await providers.open(entry.driver, { conversation: entry.conversation });
380
+ if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
381
+ return;
382
+ }
383
+ const state = await readState(entry);
384
+ if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
385
+ entry.provider = await openProvider(entry, entry.driver, state.binding, state.segmentId);
386
+ if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
387
+ return;
388
+ }
389
+ const binding = await storage.write(entry.id, async transaction => {
390
+ for (const method of ["readMetadata", "writeMetadata", "updateTurnMetadata", "listTurnIds", "readTurn", "nextTurnId", "hasMessage", "appendMessage", "replaceAssistant"]) {
391
+ if (typeof transaction[method] !== "function") throw new TypeError(`Conversation runtime storage requires transaction.${method}(). Run the runtime storage contract checks before using this adapter.`);
392
+ }
393
+ const metadata = await transaction.readMetadata();
394
+ entry.engine = metadata.runtime?.engine || requestedEngine || defaultEngine;
395
+ if (configuration) configuration = normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
396
+ const driver = entry.driver = createDriver(entry.engine, entry.host);
397
+ if (metadata.runtime) {
398
+ if (metadata.runtime.version !== 3 || typeof metadata.runtime.lastEngine !== "string" || typeof metadata.runtime.segmentId !== "string" || !metadata.runtime.segmentId || !Array.isArray(metadata.runtime.predecessors)) {
399
+ throw failure("This conversation needs its offline runtime-state upgrade before it can be opened. Its saved requests were not replayed or changed.", "conversation_storage_version");
400
+ }
401
+ if (hasUnfinishedConversationRewind(metadata.runtime)) {
402
+ throw failure("This conversation has an unfinished Undo operation from an earlier runtime. Inspect its saved native and local history offline before opening it; no history was changed.", "conversation_rewind_inspection_required");
403
+ }
404
+ if (requestedEngine && requestedEngine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch");
405
+ driver.validateConfiguration(metadata.runtime.configuration);
406
+ if (configuration && !isDeepStrictEqual(configuration, metadata.runtime.configuration)) {
407
+ throw failure("Use configure() to change a saved conversation's settings.", "conversation_configuration_mismatch");
408
+ }
409
+ // Stored work is never permission to replay it after application restart.
410
+ if (metadata.runtime.request && driver.admissionBeforeDispatch) {
411
+ // API requests are dispatched only after the accepted user message and
412
+ // receipt are committed together. A remaining reservation was not sent.
413
+ delete metadata.runtime.request;
414
+ } else if (metadata.runtime.request) {
415
+ metadata.runtime.request.error = "The previous submission was interrupted. Check delivery before submitting it again.";
416
+ }
417
+ for (const id of await transaction.listTurnIds()) {
418
+ const turn = await transaction.readTurn(id);
419
+ if (turn.metadata?.runtime?.status === "running") {
420
+ await transaction.updateTurnMetadata(id, { runtime: { ...turn.metadata.runtime,
421
+ status: "interrupted", error: "The application restarted before this answer completed. The request was not replayed." } });
422
+ }
423
+ }
424
+ } else {
425
+ driver.validateConfiguration(configuration);
426
+ metadata.runtime = { version: 3, segmentId: randomUUID(), engine: entry.engine, configuration: structuredClone(configuration), predecessors: [], seen: {},
427
+ lastEngine: (await transaction.listTurnIds()).length ? "" : entry.engine };
428
+ if (driver.createBinding) metadata.runtime.binding = await driver.createBinding();
429
+ }
430
+ await transaction.writeMetadata(metadata);
431
+ entry.segmentId = metadata.runtime.segmentId;
432
+ return metadata.runtime.binding;
433
+ });
434
+ entry.provider = await openProvider(entry, entry.driver, binding, entry.segmentId);
435
+ }
436
+
437
+ async function perform(entry, active, configuration) {
438
+ const { driver, engine, segmentId } = entry;
439
+ const initial = active.request;
440
+ let current = initial;
441
+ let goalOwner = initial.input.goal ? initial : null;
442
+ let problem;
443
+ const messageOwners = new Map();
444
+ const toolOwners = new Map();
445
+ const rejectionSaves = new Map();
446
+ let toolWork = Promise.resolve();
447
+ let toolFailure;
448
+
449
+ function prepareTools(request) {
450
+ if (!catalog || entry.conversation) return;
451
+ request.tools = createConversationTools({ catalog, context: request.context, signal: active.controller.signal,
452
+ maximumCalls: maximumToolCalls, discoveryOnly: driver.toolDiscovery === true,
453
+ authorize: () => access(request.context, entry.id, "tool", entry),
454
+ save: calls => {
455
+ if (!request.accepted) throw new Error("The engine requested an application tool before admitting the message.");
456
+ return storage.write(entry.id, transaction => transaction.updateTurnMetadata(request.turnId, { applicationTools: calls }));
457
+ },
458
+ emit: event => emit(entry, { ...event, turnId: request.turnId })
459
+ });
460
+ }
461
+
462
+ async function writeProgress(transaction, request, status, error = "", extra = {}) {
463
+ if (request.tools) await transaction.updateTurnMetadata(request.turnId, { applicationTools: request.tools.records() });
464
+ if (!driver.canonicalTranscript) {
465
+ for (const message of request.progress.values()) {
466
+ if (!await transaction.hasMessage(message.messageId)) await transaction.appendMessage(request.turnId, message);
467
+ }
468
+ if (request.answer?.text.trim()) await transaction.replaceAssistant(request.turnId, request.answer);
469
+ }
470
+ const saved = driver.canonicalTranscript ? await transaction.readTurn(request.turnId) : null;
471
+ await transaction.updateTurnMetadata(request.turnId, { runtime: { ...saved?.metadata?.runtime, status, engine, segmentId, origin: request.input.origin,
472
+ ...(request.nativeTurnId ? { nativeTurnId: request.nativeTurnId } : {}),
473
+ ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
474
+ ...(request.continuedBy ? { continuedBy: request.continuedBy } : {}), ...extra, ...(error ? { error } : {}) } });
475
+ return conversationHistoryVersions([{ turnId: request.turnId,
476
+ messages: [...request.progress.values(), ...(request.answer?.text.trim() ? [request.answer] : [])] }]);
477
+ }
478
+
479
+ async function publishAdmission(request, nativeResult) {
480
+ const receipt = { status: "accepted", messageId: request.input.messageId, turnId: request.turnId, origin: request.input.origin };
481
+ request.admission.resolve(entry.conversation ? { receipt, nativeResult } : receipt);
482
+ request.admissionSettled = true;
483
+ await emit(entry, { type: "accepted", ...receipt });
484
+ await setPhase(entry, "working", request.turnId);
485
+ if ((entry.conversation || driver.canonicalTranscript) && entry.nativePhase && entry.nativePhase !== "working") await setPhase(entry, entry.nativePhase, request.turnId);
486
+ }
487
+
488
+ async function admit(request, seen, { nativeDelivery = false, conversationTurn } = {}) {
489
+ await access(request.context, entry.id, request.operation, entry);
490
+ if (!conversationTurn) active.controller.signal.throwIfAborted();
491
+ if (request.input.goal) request.goalMessageId = request.input.messageId;
492
+ else if (goalOwner) request.goalMessageId = goalOwner.input.messageId;
493
+ const nativeTurnId = active.nativeTurnId;
494
+ const previous = current;
495
+ if (entry.conversation) {
496
+ const saved = (await readTranscript(entry)).find(turn => turn.turnId === conversationTurn?.turnId);
497
+ if (submittedMessage(saved || {})?.messageId !== request.input.messageId) {
498
+ throw new Error("The native receipt does not belong to this authored request.");
499
+ }
500
+ request.turnId = saved.turnId;
501
+ } else request.turnId = await storage.write(entry.id, async transaction => {
502
+ const metadata = await transaction.readMetadata();
503
+ if (metadata.runtime.request?.messageId !== request.input.messageId) throw new Error("The pending conversation message changed before admission.");
504
+ if (previous !== request && !previous.finished) {
505
+ const versions = await writeProgress(transaction, previous, "running", "", { continuedBy: request.input.messageId });
506
+ if (engine === "api") Object.assign(metadata.runtime.seen, versions);
507
+ }
508
+ let id;
509
+ if (conversationTurn) {
510
+ const saved = await transaction.readTurn(conversationTurn.turnId);
511
+ if (submittedMessage(saved || {})?.messageId !== request.input.messageId || saved.metadata?.runtime?.segmentId !== segmentId) {
512
+ throw new Error("The native receipt does not belong to this authored request.");
513
+ }
514
+ id = saved.turnId;
515
+ await transaction.updateTurnMetadata(id, { runtime: { ...saved.metadata.runtime,
516
+ ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
517
+ ...(nativeTurnId ? { nativeTurnId } : {}) } });
518
+ } else {
519
+ id = await transaction.nextTurnId();
520
+ await transaction.appendMessage(id, { role: request.input.origin === "application" ? "system" : "user", ...request.input, at: new Date().toISOString(),
521
+ turnMetadata: { runtime: { status: "running", engine, segmentId, origin: request.input.origin,
522
+ ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
523
+ ...(nativeTurnId ? { nativeTurnId } : {}) } } });
524
+ }
525
+ if (engine === "api") {
526
+ metadata.runtime.seen = { ...(seen || metadata.runtime.seen), ...conversationHistoryVersions([await transaction.readTurn(id)]) };
527
+ metadata.runtime.lastEngine = engine;
528
+ delete metadata.runtime.request;
529
+ }
530
+ await transaction.writeMetadata(metadata);
531
+ if (engine !== "api" && !nativeDelivery) await rememberNative(entry, transaction, [await transaction.readTurn(id)]);
532
+ return id;
533
+ });
534
+ if (previous !== request) {
535
+ previous.sealed = true;
536
+ previous.continuedBy = request.input.messageId;
537
+ if (!driver.canonicalTranscript) streams.clear(entry.id);
538
+ }
539
+ request.accepted = true;
540
+ request.nativeTurnId = nativeTurnId;
541
+ if (request.input.goal) goalOwner = request;
542
+ current = active.request = request;
543
+ if (!nativeDelivery) await publishAdmission(request);
544
+ }
545
+
546
+ async function onNativeTurn({ turnId, nativeOwner = false }) {
547
+ if (active.nativeTurnId === turnId) return;
548
+ if (!nativeOwner || !driver.canonicalTranscript) throw new Error("The native continuation has no original conversation owner.");
549
+ await access(current.context, entry.id, current.operation, entry);
550
+ active.controller.signal.throwIfAborted();
551
+ // Original Codex successors retain the admitted request, actor and budget.
552
+ active.nativeTurnId = turnId;
553
+ if (!current.nativeTurnId && !entry.conversation) {
554
+ current.nativeTurnId = turnId;
555
+ await storage.write(entry.id, async transaction => {
556
+ const turn = await transaction.readTurn(current.turnId);
557
+ await transaction.updateTurnMetadata(current.turnId, { runtime: { ...turn.metadata.runtime, nativeTurnId: turnId } });
558
+ });
559
+ }
560
+ }
561
+
562
+ async function completeNativeTurn({ turnId, status = "completed", nativeOwner = false }) {
563
+ if (!nativeOwner && active.nativeTurnId !== turnId) throw new Error("The completed native goal turn has a different identity.");
564
+ // The original owner checkpoints from its notification queue. Steering's
565
+ // exact receipt can still be behind that task, so only the outside run
566
+ // continuation joins active.steering; this checkpoint saves admitted work.
567
+ if (!nativeOwner) await active.steering;
568
+ await toolWork;
569
+ if (toolFailure) throw toolFailure;
570
+ const completedStatus = nativeOwner && active.timedOut ? "failed"
571
+ : nativeOwner && active.controller.signal.aborted ? "cancelled" : status === "completed" ? "complete" : status;
572
+ const completedError = nativeOwner && active.timedOut ? "The conversation request exceeded its time limit."
573
+ : nativeOwner && active.controller.signal.aborted ? "Work stopped." : "";
574
+ await storage.write(entry.id, async transaction => {
575
+ const metadata = await transaction.readMetadata();
576
+ const written = [];
577
+ for (const request of active.requests.values()) {
578
+ if (request.turnId && request.accepted && !request.finished &&
579
+ (nativeOwner && driver.canonicalTranscript || request.nativeTurnId === turnId)) {
580
+ const versions = await writeProgress(transaction, request, completedStatus, completedError);
581
+ if (engine === "api") Object.assign(metadata.runtime.seen, versions);
582
+ else written.push(await transaction.readTurn(request.turnId));
583
+ }
584
+ }
585
+ if (nativeOwner) for (const id of await transaction.listTurnIds()) {
586
+ const turn = await transaction.readTurn(id);
587
+ const saved = turn.metadata?.runtime;
588
+ if (saved?.segmentId !== segmentId || saved.nativeTurnId !== turnId || written.some(row => row.turnId === id)) continue;
589
+ if (["cancelled", "failed"].includes(saved.status)) continue;
590
+ await transaction.updateTurnMetadata(id, { runtime: { ...saved, status: completedStatus } });
591
+ written.push(await transaction.readTurn(id));
592
+ }
593
+ await transaction.writeMetadata(metadata);
594
+ if (written.length) await rememberNative(entry, transaction, written);
595
+ });
596
+ for (const request of active.requests.values()) {
597
+ if (request.accepted && (nativeOwner && driver.canonicalTranscript || request.nativeTurnId === turnId)) {
598
+ request.finished = request.sealed = true;
599
+ }
600
+ }
601
+ streams.clear(entry.id);
602
+ }
603
+ if (driver.canonicalTranscript && !entry.conversation) active.nativeCheckpoint = input => completeNativeTurn({ ...input, nativeOwner: true });
604
+
605
+ async function onMessage(message) {
606
+ const request = messageOwners.get(message.id) || current;
607
+ if (!request.accepted) throw new Error("The engine produced output before admitting the message.");
608
+ messageOwners.set(message.id, request);
609
+ // An already visible native item keeps its original request identity. Late
610
+ // snapshots cannot replay that answer after a newer instruction is admitted.
611
+ if (request.sealed) return;
612
+ await setPhase(entry, "working", request.turnId);
613
+ if (request.sealed) return;
614
+ const messageId = `${request.turnId}:${message.id}`;
615
+ const previous = request.answer?.messageId === messageId ? request.answer : request.progress.get(messageId);
616
+ const outputOwner = message.outputId && messageOwners.get(message.outputId) || request;
617
+ const outputId = previous?.outputId || (message.outputId ? `${outputOwner.turnId}:${message.outputId}` : "");
618
+ const value = { messageId, ...(outputId ? { outputId } : {}),
619
+ role: message.role, text: message.text, at: previous?.at || new Date().toISOString() };
620
+ // Some native streams have temporary block identities. Only their final
621
+ // history snapshots belong in the transcript, as in the original owner.
622
+ if (message.transient) {
623
+ streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value });
624
+ } else if (message.role === "assistant") {
625
+ if (request.answer && request.answer.messageId !== messageId) {
626
+ request.progress.set(request.answer.messageId, { ...request.answer, role: "commentary" });
627
+ streams.complete(entry.id, request.answer.messageId);
628
+ }
629
+ request.progress.delete(messageId);
630
+ request.answer = value;
631
+ streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value });
632
+ } else {
633
+ if (request.answer?.messageId === messageId) request.answer = null;
634
+ if (message.complete) request.progress.set(messageId, value);
635
+ if (message.complete) streams.complete(entry.id, messageId);
636
+ else streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value });
637
+ }
638
+ await emit(entry, { type: "message", turnId: request.turnId, messageId, origin: request.input.origin, role: message.role,
639
+ ...(outputId ? { outputId } : {}),
640
+ text: message.text, status: message.complete ? "complete" : "inProgress", streaming: streams.read(entry.id) });
641
+ }
642
+
643
+ function attachmentReader(request) {
644
+ return createConversationAttachmentReader({ attachments, context: request.context, conversationId: entry.id,
645
+ signal: active.controller.signal, authorize: () => access(request.context, entry.id, request.operation, entry),
646
+ types: driver.attachmentTypes, maximumBytes: maximumAttachmentBytes });
647
+ }
648
+
649
+ async function beforeDispatch(request) {
650
+ await access(request.context, entry.id, request.operation, entry);
651
+ active.controller.signal.throwIfAborted();
652
+ if (active.finishing) throw failure("The native turn finished before this instruction was sent. Send it as a new message.", "conversation_not_steerable");
653
+ }
654
+
655
+ async function deliverNative(request, dispatch) {
656
+ const history = (await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy);
657
+ let completion;
658
+ let committed;
659
+ let rendered;
660
+ let threadId;
661
+ let attachmentManifest;
662
+ let nativeResult;
663
+ const dispatchPrepared = async input => {
664
+ rendered = input;
665
+ rejectionSaves.delete(request.input.messageId);
666
+ if (request.operation === "steer" && !request.steering) throw failure("There is no active native turn to steer. Send a new message.", "conversation_not_steerable");
667
+ if ((input.contextText || "").length > maximumContinuity) throw new Error("Conversation updates exceed the configured history budget. Renew the context with a continuity briefing before continuing.");
668
+ if (request.input.goal && input.contextText) {
669
+ throw failure("Send a chat message to deliver the saved continuity briefing before starting a native terminal or goal.", "conversation_replacement_briefing_pending");
670
+ }
671
+ const readAttachments = attachmentReader(request);
672
+ // The bound host has already resolved its original attachment records;
673
+ // the production preparation owner keeps their manifest/native mapping.
674
+ const boundInput = entry.conversation ? await entry.conversation.prepareInput(input, request.context) : null;
675
+ const files = boundInput ? { attachments: boundInput.displayAttachments || [], localFiles: [], content: [] }
676
+ : await readAttachments((input.attachmentIds || []).map(attachmentId => ({ attachmentId })));
677
+ const contextFiles = boundInput ? { localFiles: [], content: [] } : await readAttachments(input.contextAttachments || []);
678
+ const localFiles = [...contextFiles.localFiles, ...files.localFiles];
679
+ if (request.input.goal && localFiles.length) {
680
+ throw failure("Native goals require extracted text or supported image bytes for attachments.", "conversation_unsupported", 400);
681
+ }
682
+ attachmentManifest = conversationAttachmentManifest(localFiles);
683
+ request.input.attachments = files.attachments;
684
+ if (!entry.conversation) await storage.write(entry.id, async transaction => {
685
+ const metadata = await transaction.readMetadata();
686
+ const pending = metadata.runtime.request;
687
+ if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
688
+ if (pending?.messageId === request.input.messageId && pending.attachmentManifest !== undefined &&
689
+ pending.attachmentManifest !== attachmentManifest) {
690
+ throw failure("Authorized attachment paths changed after this instruction was prepared. Restore the original files or send a new message.", "conversation_attachment_manifest_changed");
691
+ }
692
+ metadata.runtime.request = { ...(pending?.messageId === request.input.messageId ? pending
693
+ : { ...request.input, at: request.at, attempted: false }), attachmentManifest };
694
+ await transaction.writeMetadata(metadata);
695
+ });
696
+ const admitted = Promise.withResolvers();
697
+ committed = Promise.withResolvers();
698
+ committed.promise.catch(() => {});
699
+ request.deliveryCommitted = committed.promise;
700
+ completion = Promise.resolve().then(() => dispatch({
701
+ context: request.context,
702
+ input: { ...request.input, authoredInput: { ...request.input, at: request.at },
703
+ ...(boundInput ? { nativeMessage: boundInput } : {}),
704
+ text: input.message, contextText: input.contextText || "",
705
+ content: [...contextFiles.content, ...files.content], localFiles, attachmentManifest },
706
+ signal: active.controller.signal,
707
+ ...(engine === "opencode" ? { deliveryCommitted: committed.promise } : {}),
708
+ async beforeDispatch(native) {
709
+ await beforeDispatch(request);
710
+ threadId = native?.threadId;
711
+ if (!threadId) throw new Error("Native admission requires the saved conversation identity.");
712
+ await input.onPromptSending(entry.conversation ? native : { threadId, displayAttachments: files.attachments,
713
+ turnMetadata: { runtime: { engine, segmentId, origin: request.input.origin } } });
714
+ },
715
+ async accept(native) {
716
+ if (entry.conversation) nativeResult = native.nativeResult;
717
+ if (native?.nativeTurnId) active.nativeTurnId = native.nativeTurnId;
718
+ await admit(request, undefined, { nativeDelivery: true, conversationTurn: native?.conversationTurn });
719
+ admitted.resolve(entry.conversation ? nativeResult.value
720
+ : { ok: true, delivered: true, messageId: request.input.messageId, threadId });
721
+ // Drivers may stream or continue a goal as soon as accept returns.
722
+ // The original delivery marker must be durable before that happens.
723
+ await committed.promise;
724
+ }
725
+ })).then(result => {
726
+ if (entry.conversation && !request.accepted && result) {
727
+ nativeResult = result;
728
+ admitted.resolve(result.value);
729
+ return;
730
+ }
731
+ if (!request.accepted) throw new Error("The engine did not acknowledge the message.");
732
+ }).catch(async error => {
733
+ if (!entry.conversation && !request.accepted && error.delivery !== "uncertain") {
734
+ try { await input.onPromptRejected(); }
735
+ catch (saveError) {
736
+ rejectionSaves.set(request.input.messageId, input.onPromptRejected);
737
+ error = saveError;
738
+ }
739
+ }
740
+ admitted.reject(error);
741
+ throw error;
742
+ });
743
+ completion.catch(() => {});
744
+ return admitted.promise;
745
+ };
746
+ try {
747
+ const input = {
748
+ ...request.originalInput,
749
+ messageId: request.input.messageId,
750
+ message: request.originalInput ? request.originalInput.message ?? request.originalInput.text ?? "" : conversationRequestText(request.input),
751
+ displayMessage: request.input.text,
752
+ displayAttachments: request.originalInput?.displayAttachments || request.input.attachments,
753
+ attachmentIds: request.input.attachments.map(file => file.attachmentId),
754
+ async onPromptSending({ threadId, displayAttachments, turnMetadata }) {
755
+ if (entry.conversation) return request.originalInput?.onPromptSending?.({ threadId, displayAttachments, turnMetadata });
756
+ await storage.write(entry.id, async transaction => {
757
+ const metadata = await transaction.readMetadata();
758
+ const pending = metadata.runtime.request;
759
+ if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
760
+ metadata.runtime.request = { ...request.input, at: request.at,
761
+ message: rendered.message, displayMessage: rendered.displayMessage,
762
+ displayAttachments, attachmentIds: rendered.attachmentIds,
763
+ attachmentManifest,
764
+ ...(rendered.contextText !== undefined ? { contextText: rendered.contextText, contextAttachments: rendered.contextAttachments } : {}),
765
+ seen: pending?.seen || conversationHistoryVersions(history), attempted: true, threadId, turnMetadata,
766
+ ...(request.operation === "steer" ? { steering: true } : {}) };
767
+ await transaction.writeMetadata(metadata);
768
+ });
769
+ },
770
+ async onPromptRejected() {
771
+ if (entry.conversation) return request.originalInput?.onPromptRejected?.();
772
+ if (!entry.conversation) await storage.write(entry.id, async transaction => {
773
+ const metadata = await transaction.readMetadata();
774
+ if (metadata.runtime.request?.messageId === request.input.messageId) {
775
+ metadata.runtime.request.attempted = false;
776
+ await transaction.writeMetadata(metadata);
777
+ }
778
+ });
779
+ }
780
+ };
781
+ // Original goal commands call the same native sender directly. Chat
782
+ // context rendering and its delivery cursor must not change /goal input.
783
+ const delivered = entry.conversation && request.input.goal
784
+ ? await dispatchPrepared(input)
785
+ : await nativeChangeover(entry, request.context, request, dispatchPrepared).send({ engineId: engine,
786
+ messages: entry.conversation ? await entry.conversation.transcript.history() : conversationNativeMessages(history),
787
+ ...(entry.conversation ? { turnMetadata: { engineId: engine } } : {}), input });
788
+ if (!delivered?.delivered) {
789
+ const error = failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
790
+ if (entry.conversation) {
791
+ request.admission.resolve({ nativeResult: nativeResult || { value: delivered }, error });
792
+ request.admissionSettled = true;
793
+ }
794
+ throw error;
795
+ }
796
+ if (request.accepted) await publishAdmission(request, nativeResult);
797
+ else {
798
+ const saved = (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === request.input.messageId);
799
+ if (!saved && entry.conversation) {
800
+ // An original native-history duplicate need not have an authored
801
+ // application row. Preserve its exact result without inventing a
802
+ // canonical receipt or changing native success into a failure.
803
+ request.admission.resolve({ nativeResult: nativeResult || { value: delivered },
804
+ error: failure("The native receipt has no stored authored message.", "conversation_receipt_unavailable") });
805
+ request.admissionSettled = request.finished = true;
806
+ committed?.resolve();
807
+ await completion;
808
+ return;
809
+ }
810
+ if (!saved) throw new Error("The native receipt has no stored authored message.");
811
+ request.accepted = request.finished = true;
812
+ request.turnId = saved.turnId;
813
+ request.recovered = saved.metadata?.runtime;
814
+ const receipt = { status: "accepted", messageId: request.input.messageId, turnId: saved.turnId,
815
+ origin: request.input.origin, ...(delivered.duplicate ? { duplicate: true } : { recovered: true }) };
816
+ request.admission.resolve(entry.conversation ? { receipt, nativeResult: { value: delivered } } : receipt);
817
+ request.admissionSettled = true;
818
+ }
819
+ committed?.resolve();
820
+ await completion;
821
+ } catch (error) {
822
+ committed?.reject(error);
823
+ await completion?.catch(() => {});
824
+ throw error;
825
+ }
826
+ }
827
+
828
+ active.submit = async request => {
829
+ try {
830
+ prepareTools(request);
831
+ await deliverNative(request, entry.provider.steer);
832
+ } catch (error) {
833
+ let reported = error;
834
+ if (!request.admissionSettled) {
835
+ try {
836
+ if (!entry.conversation) await storage.write(entry.id, async transaction => {
837
+ const metadata = await transaction.readMetadata();
838
+ if (metadata.runtime.request?.messageId === request.input.messageId) {
839
+ metadata.runtime.request.error = error.message;
840
+ await transaction.writeMetadata(metadata);
841
+ }
842
+ });
843
+ } catch (saveError) { reported = saveError; }
844
+ request.admission.reject(reported);
845
+ if (!request.accepted) active.requests.delete(request.input.messageId);
846
+ }
847
+ if (error.delivery === "uncertain") active.steerError = error.message;
848
+ }
849
+ };
850
+
851
+ try {
852
+ await setPhase(entry, "preparing");
853
+ const recordedHistory = await readTranscript(entry);
854
+ const activeHistory = recordedHistory.filter(turn => !turn.metadata?.runtime?.supersededBy);
855
+ const metadata = { runtime: await readState(entry) };
856
+ const continuity = metadata.runtime.continuity;
857
+ // Existing application transcripts precede the first runtime segment.
858
+ // After replacement, their context is already carried by the briefing.
859
+ const history = activeHistory.filter(turn => turn.metadata?.runtime?.segmentId === segmentId ||
860
+ !turn.metadata?.runtime?.segmentId && !continuity);
861
+ prepareTools(initial);
862
+ const tools = initial.tools && { descriptors: initial.tools.descriptors, schemas: initial.tools.schemas, maximumArgumentBytes: initial.tools.maximumArgumentBytes,
863
+ execute(input, options) {
864
+ const request = toolOwners.get(input.id) || (options?.messageId ? active.requests.get(options.messageId) : current);
865
+ if (!request?.accepted) throw new Error("The application tool does not belong to an admitted message.");
866
+ toolOwners.set(input.id, request);
867
+ // Calls already in flight retain their actor and turn. Keep application
868
+ // mutations ordered across steering as well as within each request.
869
+ const work = toolWork.then(() => request.tools.execute(input, options));
870
+ toolWork = work.catch(error => { toolFailure ||= error; });
871
+ return work;
872
+ }
873
+ };
874
+ const providerInput = { configuration, context: initial.context, tools,
875
+ ...(engine === "opencode" || entry.conversation && ["codex", "claude"].includes(engine) ? {
876
+ async finishIfCurrent(isCurrent) {
877
+ // A reserved Send may still be preparing when the native turn ends.
878
+ // Join that same admission outside the native notification queue.
879
+ for (;;) {
880
+ const steering = active.steering;
881
+ await steering;
882
+ if (active.steering && active.steering !== steering) continue;
883
+ if (!isCurrent()) return false;
884
+ active.finishing = true;
885
+ return true;
886
+ }
887
+ }
888
+ } : {}),
889
+ onMessage, onNativeTurn,
890
+ onEvent: async event => {
891
+ if (event.type === "message-complete") {
892
+ const request = messageOwners.get(event.messageId);
893
+ if (!request) return;
894
+ const messageId = `${request.turnId}:${event.messageId}`;
895
+ streams.complete(entry.id, messageId);
896
+ return emit(entry, { ...event, turnId: request.turnId, messageId, streaming: streams.read(entry.id) });
897
+ }
898
+ if (event.type === "goal") {
899
+ if (!driver.canonicalTranscript && event.goal?.status === "active" && !goalOwner) {
900
+ await access(current.context, entry.id, "goal", entry);
901
+ await storage.write(entry.id, async transaction => {
902
+ const turn = await transaction.readTurn(current.turnId);
903
+ await transaction.updateTurnMetadata(current.turnId, { runtime: { ...turn.metadata.runtime, goalMessageId: current.input.messageId } });
904
+ });
905
+ goalOwner = current;
906
+ current.goalMessageId = current.input.messageId;
907
+ }
908
+ entry.goal = event.goal;
909
+ }
910
+ return event.type === "phase" ? setPhase(entry, event.phase, current.turnId) : emit(entry, { ...event, turnId: current.turnId });
911
+ }
912
+ };
913
+ if (engine === "api") {
914
+ const readAttachments = attachmentReader(initial);
915
+ const files = await readAttachments(initial.input.attachments);
916
+ initial.input.attachments = files.attachments;
917
+ const selectedHistory = apiHistory ? structuredClone(await apiHistory({ context: initial.context })) : undefined;
918
+ let requestHistory;
919
+ if (selectedHistory !== undefined) {
920
+ if (!Array.isArray(selectedHistory)) throw new TypeError("API request history must be an array of authored user/assistant context rows.");
921
+ requestHistory = [];
922
+ for (const message of selectedHistory) {
923
+ if (!["user", "assistant"].includes(message?.role) || typeof message.content !== "string") {
924
+ throw new TypeError("API request history requires user/assistant roles and text content.");
925
+ }
926
+ const attachmentIds = conversationAttachmentIds(message.attachmentIds);
927
+ if (message.role !== "user" && attachmentIds.length) throw new TypeError("Only authored user context may reference attachments.");
928
+ const previousFiles = await readAttachments(attachmentIds.map(attachmentId => ({ attachmentId })));
929
+ requestHistory.push({ role: message.role, content: previousFiles.content.length
930
+ ? [{ type: "text", text: message.content }, ...previousFiles.content] : message.content });
931
+ }
932
+ } else {
933
+ for (const turn of history) {
934
+ const message = submittedMessage(turn);
935
+ if (message?.attachments?.length) message.content = (await readAttachments(message.attachments)).content;
936
+ }
937
+ }
938
+ const seen = conversationHistoryVersions(activeHistory);
939
+ const priorFiles = await readAttachments(metadata.runtime.continuityAttachments || []);
940
+ const input = { ...initial.input, text: conversationRequestText(initial.input), contextText: "",
941
+ content: [...priorFiles.content, ...files.content] };
942
+ await storage.write(entry.id, async transaction => {
943
+ const metadata = await transaction.readMetadata();
944
+ metadata.runtime.request.seen = seen;
945
+ metadata.runtime.request.attachments = files.attachments;
946
+ await transaction.writeMetadata(metadata);
947
+ });
948
+ const completion = await entry.provider.run({ ...providerInput, input, signal: active.controller.signal, continuity, history, requestHistory,
949
+ beforeDispatch: () => beforeDispatch(initial), accept: () => admit(initial, seen) });
950
+ if (completion?.metadata && Object.keys(completion.metadata).length) {
951
+ await storage.write(entry.id, transaction => transaction.updateTurnMetadata(initial.turnId, { completion: completion.metadata }));
952
+ }
953
+ } else {
954
+ await deliverNative(initial, input => entry.provider.run({ ...providerInput, ...input }));
955
+ }
956
+ active.controller.signal.throwIfAborted();
957
+ } catch (error) { problem = error; }
958
+ active.finishing = true;
959
+ await active.steering;
960
+ await toolWork;
961
+ problem ||= toolFailure;
962
+ clearTimeout(active.timer);
963
+ if (problem?.cleanupFailed) {
964
+ entry.executionFailure = problem;
965
+ entry.cleanupTurnId = current.turnId;
966
+ }
967
+ const status = entry.executionFailure ? "interrupted" : active.timedOut ? "failed" : active.controller.signal.aborted ? "cancelled"
968
+ : problem?.status === "interrupted" ? "interrupted" : problem ? "failed" : initial.recovered?.status || "complete";
969
+ const error = entry.executionFailure?.message || (active.timedOut ? "The conversation request exceeded its time limit."
970
+ : status === "cancelled" ? "Work stopped." : problem?.message || active.steerError || initial.recovered?.error || "");
971
+ entry.saveProgress = async () => {
972
+ if (entry.conversation) {
973
+ // Native output, checkpoints and receipt durability are already owned
974
+ // by the original run owner and sender; do not annotate old rows.
975
+ entry.error = error;
976
+ entry.storageFailure = null;
977
+ entry.saveProgress = null;
978
+ return { type: "settled", turnId: current.turnId, status, error };
979
+ }
980
+ // Preserve the common storage-retry facility by retrying the original
981
+ // failed rejection save, never by deleting or rebuilding its frozen draft.
982
+ for (const [messageId, save] of rejectionSaves) {
983
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
984
+ if (runtime.request?.messageId === messageId) await save();
985
+ rejectionSaves.delete(messageId);
986
+ }
987
+ await storage.write(entry.id, async transaction => {
988
+ const metadata = await transaction.readMetadata();
989
+ const written = [];
990
+ for (const request of active.requests.values()) {
991
+ if (request.accepted && !request.finished) {
992
+ const versions = await writeProgress(transaction, request, status, error);
993
+ if (engine === "api") Object.assign(metadata.runtime.seen, versions);
994
+ else written.push(await transaction.readTurn(request.turnId));
995
+ }
996
+ }
997
+ if (!initial.accepted && metadata.runtime.request?.messageId === initial.input.messageId) {
998
+ if (engine !== "api" || problem?.delivery === "uncertain") metadata.runtime.request.error = error;
999
+ else delete metadata.runtime.request;
1000
+ }
1001
+ await transaction.writeMetadata(metadata);
1002
+ if (written.length) await rememberNative(entry, transaction, written);
1003
+ });
1004
+ streams.clear(entry.id);
1005
+ entry.error = error;
1006
+ entry.storageFailure = null;
1007
+ entry.saveProgress = null;
1008
+ return { type: "settled", turnId: current.turnId, status, error: entry.error };
1009
+ };
1010
+ try { await entry.saveProgress(); }
1011
+ catch (saveError) {
1012
+ entry.error = "Conversation progress could not be saved. Restore access to its storage before continuing.";
1013
+ entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
1014
+ entry.storageFailure.cause = saveError;
1015
+ problem ||= saveError;
1016
+ }
1017
+ if (!initial.admissionSettled) initial.admission.reject(problem || new Error("The engine did not acknowledge the message."));
1018
+ entry.active = null;
1019
+ await setPhase(entry, "", current.turnId);
1020
+ await emit(entry, { type: "settled", turnId: current.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status, error: entry.error });
1021
+ }
1022
+
1023
+ async function send(entry, context, input, origin = "user", goal, representation) {
1024
+ await access(context, entry.id, goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send", entry);
1025
+ if (entry.conversation?.commands) {
1026
+ if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400);
1027
+ return entry.conversation.commands.send(input, context);
1028
+ }
1029
+ if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
1030
+ const originalInput = representation === "native" ? input : undefined;
1031
+ const message = originalInput ? { ...input, text: input.displayMessage ?? input.message ?? input.text ?? "" } : input;
1032
+ const admission = await serial(entry, () => admitSend(entry, context, message, origin, goal, originalInput));
1033
+ const result = await (admission.receipt || admission.promise);
1034
+ if (!entry.conversation) return result;
1035
+ if (representation === "native") return result.nativeResult;
1036
+ if (result.error) throw result.error;
1037
+ return result.receipt;
1038
+ }
1039
+
1040
+ // Called while holding the conversation queue, including Claude's existing
1041
+ // stop-then-Send goal commands. All commands retain the ordinary receipt path.
1042
+ async function admitSend(entry, context, input, origin = "user", goal, originalInput) {
1043
+ const operation = goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send";
1044
+ await access(context, entry.id, operation, entry);
1045
+ await entry.conversation?.admission(context, operation);
1046
+ const attachmentIds = conversationAttachmentIds(input?.attachmentIds);
1047
+ if (!input || typeof input.messageId !== "string" || !input.messageId.trim() || input.messageId.length > 128 ||
1048
+ entry.engine !== "api" && !/^[\w-]{1,128}$/u.test(input.messageId) ||
1049
+ typeof input.text !== "string" || !input.text.trim() && !attachmentIds.length || input.text.length > maximumInput) {
1050
+ throw failure("Bounded text or attachment IDs and a stable messageId are required.", "conversation_invalid_message", 400);
1051
+ }
1052
+ if (!originalInput && input.attachments?.length) throw failure("Supply attachmentIds; only the application's resolver can supply file content.", "conversation_unsupported", 400);
1053
+ if (attachmentIds.length && !capabilities(entry).attachments) throw failure("File attachments are not configured for this conversation.", "conversation_unsupported", 400);
1054
+ if (input.steer !== undefined && typeof input.steer !== "boolean") throw failure("steer must be a boolean.", "conversation_invalid_message", 400);
1055
+ if (input.steer && !entry.driver.capabilities.steering) throw failure("This connection does not support steering.", "conversation_unsupported", 400);
1056
+ let data;
1057
+ if (input.data !== undefined) {
1058
+ try {
1059
+ if (!input.data || typeof input.data !== "object" || Array.isArray(input.data)) throw new TypeError();
1060
+ const encoded = JSON.stringify(input.data);
1061
+ if (input.text.length + encoded.length > maximumInput) throw new TypeError();
1062
+ data = JSON.parse(encoded);
1063
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new TypeError();
1064
+ } catch {
1065
+ throw failure("Application data must be a JSON object within the conversation input limit.", "conversation_invalid_message", 400);
1066
+ }
1067
+ }
1068
+ const message = { messageId: input.messageId, text: input.text.trim(), origin,
1069
+ attachments: attachmentIds.map(attachmentId => ({ attachmentId })), ...(data !== undefined ? { data } : {}), ...(goal ? { goal } : {}) };
1070
+ if (entry.storageFailure) throw entry.storageFailure;
1071
+ if (entry.executionFailure) throw entry.executionFailure;
1072
+ if (goal && goal.expectedSegmentId !== entry.segmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
1073
+ const pending = entry.active?.requests.get(message.messageId);
1074
+ if (pending && entry.engine !== "api" && !pending.admissionSettled) {
1075
+ if (!entry.conversation && !sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
1076
+ return { promise: pending.admission.promise };
1077
+ }
1078
+ const history = await readTranscript(entry);
1079
+ const existing = history.find(turn => submittedMessage(turn)?.messageId === message.messageId);
1080
+ const nativeState = entry.engine !== "api" && (await readState(entry));
1081
+ const nativePending = nativeState?.request;
1082
+ if (existing) {
1083
+ const previous = submittedMessage(existing);
1084
+ if (!entry.conversation && !sameRequest(previous, message)) {
1085
+ throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
1086
+ }
1087
+ if (!entry.conversation && !nativePending) return { receipt: { status: "accepted", messageId: message.messageId, turnId: existing.turnId, origin, duplicate: true } };
1088
+ }
1089
+ if (pending && !entry.conversation) {
1090
+ if (!sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
1091
+ return { promise: pending.admission.promise };
1092
+ }
1093
+ const working = entry.active;
1094
+ if (entry.conversation && ["codex", "opencode", "claude"].includes(entry.engine) && operation === "send" && working?.finishing) {
1095
+ // No native dispatch has occurred for this request. Finish the previous
1096
+ // waiter before reserving the same authored request on the ordinary path.
1097
+ await working.done;
1098
+ return admitSend(entry, context, input, origin, goal, originalInput);
1099
+ }
1100
+ const nativeSteering = !working && (nativeState.nativeTurn?.active || entry.driver.canonicalTranscript && nativeState.binding?.codexAppServerRun &&
1101
+ codexAppServerTurnStateFromAgentRun(nativeState.binding.codexAppServerRun).active);
1102
+ if (working && !input.steer && !goal && !entry.conversation) throw failure("Wait for this answer or use steer to send another instruction.", "conversation_busy");
1103
+ if (input.steer && (!working && !nativeSteering || working?.finishing) && !(nativePending?.attempted || nativePending?.inspectionOnly)) throw failure("There is no active native turn to steer. Send a new message.", "conversation_not_steerable");
1104
+ if (goal && working?.finishing) throw failure("Wait for the current turn to finish saving before starting the goal.", "conversation_busy");
1105
+ if (working && (!working.request.admissionSettled || working.steering)) throw failure("Wait for the pending instruction to be admitted before steering again.", "conversation_busy");
1106
+ const barrier = working ? Promise.withResolvers() : null;
1107
+ if (barrier) working.steering = barrier.promise;
1108
+ const release = () => {
1109
+ barrier?.resolve();
1110
+ if (working && working.steering === barrier?.promise) working.steering = null;
1111
+ };
1112
+ let configuration;
1113
+ let requestAt = new Date().toISOString();
1114
+ try { configuration = entry.conversation ? (await readState(entry)).configuration : await storage.write(entry.id, async transaction => {
1115
+ const metadata = await transaction.readMetadata();
1116
+ if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before sending another message.", "conversation_replacement_pending");
1117
+ if (retainedSegments(metadata.runtime).some(segment => segment.segmentId !== entry.segmentId && segment.request?.messageId === message.messageId)) {
1118
+ throw failure("This message has uncertain delivery in a retained conversation. Select that engine and check its receipt before submitting it again.", "conversation_delivery_uncertain");
1119
+ }
1120
+ if (metadata.runtime.request) {
1121
+ if (entry.engine === "api") throw failure("The previous submission has uncertain delivery. It will not be resent automatically.", "conversation_delivery_uncertain");
1122
+ if (metadata.runtime.request.messageId === message.messageId) {
1123
+ if (!sameRequest(metadata.runtime.request, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
1124
+ requestAt = metadata.runtime.request.at;
1125
+ }
1126
+ } else metadata.runtime.request = { ...message, at: requestAt,
1127
+ ...(entry.engine !== "api" ? { attempted: false } : {}), ...(input.steer ? { steering: true } : {}) };
1128
+ await transaction.writeMetadata(metadata);
1129
+ return metadata.runtime.configuration;
1130
+ }); } catch (error) { release(); throw error; }
1131
+ const request = { input: message, originalInput, operation, context, at: requestAt, steering: Boolean(working || input.steer && nativeSteering),
1132
+ admission: Promise.withResolvers(), progress: new Map() };
1133
+ request.admission.promise.catch(() => {});
1134
+ if (working) {
1135
+ working.requests.set(message.messageId, request);
1136
+ working.submit(request).catch(error => request.admission.reject(error)).finally(release);
1137
+ return { promise: request.admission.promise };
1138
+ }
1139
+ const active = { request, requests: new Map([[message.messageId, request]]), controller: new AbortController() };
1140
+ entry.active = active;
1141
+ entry.error = "";
1142
+ if (limits.timeoutMs !== undefined) active.timer = setTimeout(() => {
1143
+ active.timedOut = true;
1144
+ active.controller.abort(new Error("The conversation request exceeded its time limit."));
1145
+ }, limits.timeoutMs);
1146
+ active.done = perform(entry, active, configuration);
1147
+ return { promise: request.admission.promise };
1148
+ }
1149
+
1150
+ async function stop(entry, context, input, representation) {
1151
+ if (representation === "native") return entry.provider.cancel({ context, input, representation });
1152
+ const active = entry.active;
1153
+ if (!active && entry.executionFailure) {
1154
+ await entry.provider.cancel();
1155
+ await retrySave(entry);
1156
+ if (entry.cleanupTurnId && !entry.conversation) await storage.write(entry.id, async transaction => {
1157
+ const turn = await transaction.readTurn(entry.cleanupTurnId);
1158
+ await transaction.updateTurnMetadata(entry.cleanupTurnId,
1159
+ { runtime: { ...turn.metadata.runtime, status: "cancelled", error: "Work stopped." } });
1160
+ });
1161
+ entry.executionFailure = null;
1162
+ entry.cleanupTurnId = null;
1163
+ entry.error = "Work stopped.";
1164
+ return { stopped: true };
1165
+ }
1166
+ if (!active) {
1167
+ if (!entry.conversation && !entry.driver.canonicalTranscript) return { stopped: false };
1168
+ const metadata = { runtime: await readState(entry) };
1169
+ const result = await entry.provider.cancel({ configuration: metadata.runtime.configuration, context });
1170
+ return { stopped: result?.interrupted === true || result?.operationOutcome === "interrupted" };
1171
+ }
1172
+ active.controller.abort(new Error("Work stopped."));
1173
+ await active.done;
1174
+ if (entry.executionFailure) throw entry.executionFailure;
1175
+ return { stopped: true };
1176
+ }
1177
+
1178
+ async function readGoal(entry, context, representation) {
1179
+ await access(context, entry.id, "readGoal", entry);
1180
+ if (!capabilities(entry).goals) return null;
1181
+ const configuration = representation === "native" ? undefined : (await readState(entry)).configuration;
1182
+ const result = await entry.provider.readGoal({ context, configuration });
1183
+ if (entry.conversation && representation !== "native" && result.nativeResult.status === "unavailable") {
1184
+ throw failure("The native goal is unavailable. Try reading its status again.", "conversation_goal_unavailable", 503);
1185
+ }
1186
+ entry.goal = entry.conversation ? result.goal : result;
1187
+ return representation === "native" ? result.nativeResult : structuredClone(entry.goal);
1188
+ }
1189
+
1190
+ async function updateGoal(entry, context, input, representation) {
1191
+ await access(context, entry.id, "goal", entry);
1192
+ if (!capabilities(entry).goals) throw failure("This connection does not support native goals.", "conversation_unsupported", 400);
1193
+ if (representation !== "native" && !capabilities(entry).goals) {
1194
+ throw failure("This bound conversation does not yet map native goal messages to canonical receipts.", "conversation_unsupported", 400);
1195
+ }
1196
+ if (representation === "native") {
1197
+ const { operation } = await serial(entry, async () => {
1198
+ await access(context, entry.id, "goal", entry);
1199
+ // The original goal owner has its own admission. Its native message
1200
+ // acknowledgement must not hold the common queue ahead of Stop.
1201
+ return { operation: entry.provider.updateGoal({ context, input, representation }) };
1202
+ });
1203
+ const result = await operation;
1204
+ if (result.nativeResult.ok !== false) {
1205
+ entry.goal = result.goal;
1206
+ await emit(entry, { type: "goal", goal: entry.goal });
1207
+ }
1208
+ return result.nativeResult;
1209
+ }
1210
+ if (!input || !["set", "resume", "pause", "cancel"].includes(input.action) ||
1211
+ (typeof input.expectedSegmentId !== "string" && !(entry.conversation && input.expectedSegmentId === null)) ||
1212
+ input.expectedGoalId != null && (typeof input.expectedGoalId !== "string" || !input.expectedGoalId)) {
1213
+ throw failure("A goal action and current conversation segment are required.", "conversation_invalid_goal", 400);
1214
+ }
1215
+ const starting = input.action === "set" || input.action === "resume";
1216
+ const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId, expectedGoalId: input.expectedGoalId || null };
1217
+ if (starting) {
1218
+ if (input.action === "set") {
1219
+ if (input.tokenBudget != null && !entry.driver.capabilities.goalBudgets) throw failure("This connection does not support goal token budgets.", "conversation_unsupported", 400);
1220
+ if (typeof input.objective !== "string" || !input.objective.trim() || input.objective.length > 4000 ||
1221
+ input.tokenBudget != null && (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget < 1)) {
1222
+ throw failure("A bounded objective and optional positive token budget are required.", "conversation_invalid_goal", 400);
1223
+ }
1224
+ goal.objective = input.objective.trim();
1225
+ if (input.tokenBudget != null) goal.tokenBudget = input.tokenBudget;
1226
+ } else if (!goal.expectedGoalId) throw failure("Select the goal to resume.", "conversation_invalid_goal", 400);
1227
+ }
1228
+ if (!starting && !input.expectedGoalId) throw failure("Select the goal to stop.", "conversation_invalid_goal", 400);
1229
+ const delivery = entry.driver.capabilities.goalCommands?.[input.action]?.delivery;
1230
+ if (!["message", "control"].includes(delivery)) throw failure("This connection does not support that goal command.", "conversation_unsupported", 400);
1231
+ const messageCommand = delivery === "message";
1232
+ if (messageCommand && (typeof input.messageId !== "string" || !/^[\w-]{1,128}$/u.test(input.messageId))) {
1233
+ throw failure("A stable messageId is required for this goal command.", "conversation_invalid_message", 400);
1234
+ }
1235
+ if (conversationAttachmentIds(input.attachmentIds).length || input.attachments?.length) {
1236
+ throw failure("This engine's native goal commands do not accept attachments.", "conversation_unsupported", 400);
1237
+ }
1238
+ const result = await serial(entry, async () => {
1239
+ await access(context, entry.id, "goal", entry);
1240
+ if (entry.conversation) await readState(entry);
1241
+ if (entry.segmentId !== input.expectedSegmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
1242
+ if (entry.conversation && input.expectedSegmentId === null && input.action !== "set") {
1243
+ throw failure("This command requires an existing native conversation.", "conversation_goal_changed");
1244
+ }
1245
+ if (entry.storageFailure) throw entry.storageFailure;
1246
+ if (entry.executionFailure) throw entry.executionFailure;
1247
+ if (messageCommand) {
1248
+ const history = await readTranscript(entry);
1249
+ const existing = history.find(turn => submittedMessage(turn)?.messageId === input.messageId);
1250
+ if (existing) {
1251
+ if (!entry.conversation && !isDeepStrictEqual(submittedMessage(existing).goal, goal)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
1252
+ return { receipt: { status: "accepted", messageId: input.messageId, turnId: existing.turnId, origin: "user", duplicate: true } };
1253
+ }
1254
+ }
1255
+ if (entry.active && !entry.active.request.accepted) throw failure("Wait for the goal command to be admitted.", "conversation_busy");
1256
+ const metadata = { runtime: await readState(entry) };
1257
+ if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
1258
+ throw failure("Check the uncertain command's delivery before changing the goal.", "conversation_delivery_uncertain");
1259
+ }
1260
+ if (entry.driver.canonicalTranscript && metadata.runtime.binding?.goalRequest) {
1261
+ throw failure("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal.", "conversation_delivery_uncertain");
1262
+ }
1263
+ if (starting && (metadata.runtime.replacement || !entry.conversation && pendingSuccessor(metadata.runtime) || metadata.runtime.lastEngine !== entry.engine)) {
1264
+ throw failure("Send a message to catch this AI up before starting or resuming its goal.", "conversation_replacement_briefing_pending");
1265
+ }
1266
+ // Definitions keep native thread discovery stable. A control has no
1267
+ // admitted request, application-tool actor or executor of its own.
1268
+ const tools = catalog && entry.driver.toolDiscovery
1269
+ ? { schemas: catalog.resolveToolSet(context, { discoveryOnly: true }).tools.map(catalog.toOpenAiToolSchema) } : undefined;
1270
+ if (entry.conversation) {
1271
+ let receipt;
1272
+ // Preserve the original goal owner's acknowledgement wait outside the
1273
+ // invocation queue so the same Stop command can still interrupt it.
1274
+ const operation = entry.provider.updateGoal({ context, configuration: metadata.runtime.configuration,
1275
+ input: { ...input, ...goal }, representation, tools, interrupt: () => stop(entry, context),
1276
+ async send({ message }, { threadId }) {
1277
+ const admission = await serial(entry, async () => {
1278
+ const state = await readState(entry);
1279
+ const selectedSegment = `${entry.engine}:${threadId}`;
1280
+ if (!threadId || state.segmentId !== selectedSegment ||
1281
+ input.expectedSegmentId !== null && input.expectedSegmentId !== selectedSegment) {
1282
+ throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
1283
+ }
1284
+ return admitSend(entry, context, { messageId: input.messageId, text: message }, "user",
1285
+ { ...goal, expectedSegmentId: selectedSegment });
1286
+ });
1287
+ const accepted = await (admission.receipt || admission.promise);
1288
+ if (accepted.error) throw accepted.error;
1289
+ receipt = accepted.receipt;
1290
+ }
1291
+ }).then(async updated => {
1292
+ if (updated.nativeResult.ok === false) throw Object.assign(new Error(updated.nativeResult.error), updated.nativeResult);
1293
+ entry.goal = updated.goal;
1294
+ await emit(entry, { type: "goal", goal: entry.goal });
1295
+ return receipt ? { receipt } : { goal: structuredClone(entry.goal) };
1296
+ });
1297
+ return { operation };
1298
+ }
1299
+ let delivery;
1300
+ const updated = await entry.provider.updateGoal({ context, configuration: metadata.runtime.configuration, input: { ...input, ...goal }, tools,
1301
+ interrupt: () => stop(entry, context),
1302
+ async send({ message }) {
1303
+ delivery = await admitSend(entry, context, { messageId: input.messageId, text: message }, "user", goal);
1304
+ }
1305
+ });
1306
+ // Release the queue once Send is reserved, just as an ordinary message
1307
+ // does. Stop remains available while native acknowledgement is pending.
1308
+ if (delivery) return delivery;
1309
+ if (entry.conversation && updated.nativeResult.ok === false) throw Object.assign(new Error(updated.nativeResult.error), updated.nativeResult);
1310
+ entry.goal = entry.conversation ? updated.goal : updated;
1311
+ await emit(entry, { type: "goal", goal: entry.goal });
1312
+ return { goal: structuredClone(entry.goal) };
1313
+ });
1314
+ const completed = result.operation ? await result.operation : result;
1315
+ return completed.receipt || completed.promise || completed.goal;
1316
+ }
1317
+
1318
+ async function retrySave(entry) {
1319
+ if (!entry.storageFailure || !entry.saveProgress) return { saved: false };
1320
+ const event = await entry.saveProgress();
1321
+ await emit(entry, event);
1322
+ return { saved: true };
1323
+ }
1324
+
1325
+ async function recoverDelivery(entry, context, messageId, { observation: suppliedObservation, allowActive = false } = {}) {
1326
+ await access(context, entry.id, "inspectDelivery", entry);
1327
+ if (entry.active && !allowActive) throw failure("Wait for the current submission before checking delivery.", "conversation_busy");
1328
+ if (entry.storageFailure) throw entry.storageFailure;
1329
+ if (entry.executionFailure) throw entry.executionFailure;
1330
+ const history = await readTranscript(entry);
1331
+ const existing = history.find(turn => submittedMessage(turn)?.messageId === messageId);
1332
+ const receipt = existing && { status: "accepted", messageId, turnId: existing.turnId, duplicate: true };
1333
+ const goalMessageId = existing?.metadata.runtime.goalMessageId;
1334
+ const goalRequest = Boolean(submittedMessage(existing || {})?.goal || goalMessageId === messageId);
1335
+ if (existing && (existing.metadata?.runtime?.segmentId !== entry.segmentId || existing.metadata.runtime.supersededBy ||
1336
+ existing.metadata.runtime.status === "complete" && !goalRequest)) return receipt;
1337
+ if (goalMessageId && goalMessageId !== messageId) {
1338
+ const owner = history.find(turn => submittedMessage(turn)?.messageId === goalMessageId);
1339
+ if (owner?.metadata.runtime.goalMessageId !== goalMessageId || owner.metadata.runtime.segmentId !== entry.segmentId) throw new Error("The goal command for this native continuation is missing.");
1340
+ const result = await inspectDelivery(entry, context, goalMessageId);
1341
+ return { ...receipt, recovered: result.recovered === true };
1342
+ }
1343
+ const metadata = { runtime: await readState(entry) };
1344
+ if (metadata.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending");
1345
+ const request = existing ? submittedMessage(existing) : metadata.runtime.request;
1346
+ if (!request || request.messageId !== messageId || !entry.provider.inspectAdmission) return receipt || { status: "unknown", messageId };
1347
+ const pending = metadata.runtime.request?.messageId === messageId ? metadata.runtime.request : null;
1348
+ if (pending?.threadId && pending.threadId !== nativeIdentity(metadata.runtime)) return receipt || { status: "unknown", messageId };
1349
+ const observation = suppliedObservation || await entry.provider.inspectAdmission({ ...request, nativeTurnId: existing?.metadata?.runtime?.nativeTurnId,
1350
+ context, configuration: metadata.runtime.configuration });
1351
+ await access(context, entry.id, "inspectDelivery", entry);
1352
+ if (!observation.accepted) return { ...(receipt || { status: "unknown", messageId }),
1353
+ ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
1354
+ if (entry.driver.canonicalTranscript) {
1355
+ const latest = await readTranscript(entry);
1356
+ const saved = latest.find(turn => submittedMessage(turn)?.messageId === messageId);
1357
+ if (!saved) throw new Error("The native receipt did not persist its authored message.");
1358
+ const state = saved.metadata?.runtime;
1359
+ if (state?.segmentId !== entry.segmentId) throw new Error("The native receipt belongs to another conversation segment.");
1360
+ const unknownTool = saved.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
1361
+ const status = ["cancelled", "failed"].includes(state.status) ? state.status : unknownTool ? "interrupted"
1362
+ : observation.recoveryLimitation ? "interrupted" : ["completed", "idle"].includes(observation.status) ? "complete"
1363
+ : observation.status === "failed" ? "failed" : "interrupted";
1364
+ const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
1365
+ : observation.recoveryLimitation || (status === "complete" ? "" : state.error || "Delivery was confirmed. Completion could not be verified; this request was not replayed.");
1366
+ await storage.write(entry.id, async transaction => {
1367
+ const current = await transaction.readMetadata();
1368
+ if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) {
1369
+ throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending");
1370
+ }
1371
+ const exact = await transaction.readTurn(saved.turnId);
1372
+ const { error: _previousError, ...runtime } = exact.metadata.runtime;
1373
+ await transaction.updateTurnMetadata(saved.turnId, { runtime: { ...runtime, status, ...(error ? { error } : {}) } });
1374
+ });
1375
+ if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
1376
+ entry.error = error;
1377
+ const result = { ...(receipt || { status: "accepted", messageId, turnId: saved.turnId }),
1378
+ recovered: observation.recovered === true, ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
1379
+ if (!existing) await emit(entry, { type: "accepted", ...result });
1380
+ if (result.recovered) await emit(entry, { type: "settled", turnId: saved.turnId, messageId, status, error });
1381
+ return result;
1382
+ }
1383
+ const recovered = await storage.write(entry.id, async transaction => {
1384
+ const current = await transaction.readMetadata();
1385
+ if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending");
1386
+ const native = { ...observation, messageId };
1387
+ const results = [];
1388
+ const id = messageId;
1389
+ let turn = history.find(turn => submittedMessage(turn)?.messageId === id);
1390
+ if (turn?.metadata?.runtime?.supersededBy || turn && turn.metadata?.runtime?.segmentId !== entry.segmentId) {
1391
+ throw new Error("Native history refers to a turn outside the active conversation segment.");
1392
+ }
1393
+ if (turn?.metadata?.runtime?.status !== "complete") {
1394
+ if (!turn) {
1395
+ const input = request;
1396
+ const turnId = await transaction.nextTurnId();
1397
+ await transaction.appendMessage(turnId, { role: input.origin === "application" ? "system" : "user", origin: input.origin || "user",
1398
+ messageId: id, text: input.text, attachments: input.attachments, at: input.at,
1399
+ ...(input.data !== undefined ? { data: input.data } : {}), ...(input.goal ? { goal: input.goal } : {}),
1400
+ turnMetadata: { runtime: { engine: entry.engine, segmentId: entry.segmentId, origin: input.origin || "user" } } });
1401
+ turn = await transaction.readTurn(turnId);
1402
+ history.push(turn);
1403
+ }
1404
+ const runtime = turn.metadata.runtime;
1405
+ if (runtime.nativeTurnId && native.nativeTurnId && runtime.nativeTurnId !== native.nativeTurnId) throw new Error("Native history changed the accepted turn's identity.");
1406
+ const replies = [];
1407
+ for (const message of native.messages || []) {
1408
+ if (message.role === "assistant") replies.push(message.text);
1409
+ else {
1410
+ const messageId = `${turn.turnId}:${message.id}`;
1411
+ if (!await transaction.hasMessage(messageId)) await transaction.appendMessage(turn.turnId,
1412
+ { messageId, ...(message.outputId ? { outputId: `${turn.turnId}:${message.outputId}` } : {}),
1413
+ role: message.role, text: message.text, at: submittedMessage(turn).at });
1414
+ }
1415
+ }
1416
+ const nativeReplies = (native.messages || []).filter(message => message.role === "assistant");
1417
+ const outputId = nativeReplies.length === 1 && nativeReplies[0].outputId;
1418
+ if (replies.length) await transaction.replaceAssistant(turn.turnId, { messageId: turn.assistant?.messageId || `${turn.turnId}:assistant`,
1419
+ ...(!turn.assistant?.outputId && outputId ? { outputId: `${turn.turnId}:${outputId}` } : {}),
1420
+ role: "assistant", text: replies.join("\n\n"), at: submittedMessage(turn).at });
1421
+ const unknownTool = turn.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
1422
+ const status = ["cancelled", "failed"].includes(runtime.status) ? runtime.status : unknownTool ? "interrupted" : native.status || "interrupted";
1423
+ const { error: previousError, ...saved } = runtime;
1424
+ const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
1425
+ : status === "complete" ? "" : previousError || native.error || "Delivery was confirmed from native history. Completion was not confirmed; this request was not replayed.";
1426
+ await transaction.updateTurnMetadata(turn.turnId, { runtime: { ...saved, status,
1427
+ ...(request.goal || goalRequest ? { goalMessageId: messageId } : {}),
1428
+ ...(native.nativeTurnId ? { nativeTurnId: native.nativeTurnId } : {}), ...(error ? { error } : {}) } });
1429
+ results.push({ turnId: turn.turnId, messageId: id, status, error });
1430
+ }
1431
+ await transaction.writeMetadata(current);
1432
+ return results;
1433
+ });
1434
+ if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
1435
+ entry.error = recovered.find(turn => turn.error)?.error || "";
1436
+ const result = { ...(receipt || { status: "accepted", messageId, turnId: recovered[0].turnId }), recovered: recovered.length > 0 };
1437
+ if (!existing) await emit(entry, { type: "accepted", ...result });
1438
+ for (const turn of recovered) await emit(entry, { type: "settled", ...turn });
1439
+ return result;
1440
+ }
1441
+
1442
+ async function inspectDelivery(entry, context, messageId) {
1443
+ if (entry.conversation?.commands) {
1444
+ await access(context, entry.id, "inspectDelivery", entry);
1445
+ const result = await entry.conversation.commands.inspectDelivery({ messageId }, context);
1446
+ await access(context, entry.id, "inspectDelivery", entry);
1447
+ return result;
1448
+ }
1449
+ if (entry.conversation) {
1450
+ await access(context, entry.id, "inspectDelivery", entry);
1451
+ const state = await readState(entry);
1452
+ // A running owner completes its own receipt. Only an idle owner recovers
1453
+ // the saved changeover, preserving write-receipt-before-clear ordering.
1454
+ if (!entry.active && state.request?.messageId === messageId && state.request.attempted) {
1455
+ const sender = nativeChangeover(entry, context, null, () => {
1456
+ throw new Error("Receipt inspection cannot submit another native prompt.");
1457
+ });
1458
+ const delivered = await sender.send({ engineId: entry.engine,
1459
+ messages: await entry.conversation.transcript.history(), input: { messageId } });
1460
+ if (!delivered?.delivered) throw failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
1461
+ }
1462
+ const row = (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId);
1463
+ if (row) return { status: "accepted", messageId, turnId: row.turnId, duplicate: true };
1464
+ const threadId = state.request?.messageId === messageId ? state.request.threadId : nativeIdentity(state);
1465
+ if (!threadId) return { status: "unknown", messageId };
1466
+ const observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
1467
+ await access(context, entry.id, "inspectDelivery", entry);
1468
+ // Native-only acceptance is evidence of delivery, not permission to
1469
+ // manufacture an authored row or use a native turn ID as its receipt.
1470
+ return observation.accepted ? { status: "accepted", messageId, duplicate: true } : { status: "unknown", messageId };
1471
+ }
1472
+ const result = await recoverDelivery(entry, context, messageId);
1473
+ if (result.status !== "accepted" || entry.engine === "api") return result;
1474
+ if (entry.driver.canonicalTranscript && result.recoveryLimitation) {
1475
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
1476
+ if (runtime.request?.goal || runtime.binding?.goalRequest?.messageId === messageId ||
1477
+ runtime.binding?.goalReceipt?.messageId === messageId) return result;
1478
+ }
1479
+ const messages = conversationNativeMessages((await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy));
1480
+ const sender = nativeChangeover(entry, context, null, () => {
1481
+ throw new Error("Receipt inspection cannot submit another native prompt.");
1482
+ });
1483
+ const pending = (await readState(entry)).request;
1484
+ if (pending?.messageId === messageId) {
1485
+ const delivered = await sender.send({ engineId: entry.engine, messages, input: { messageId } });
1486
+ if (!delivered?.delivered) throw failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
1487
+ }
1488
+ await sender.remember({ engineId: entry.engine, messages });
1489
+ return result;
1490
+ }
1491
+
1492
+ async function replace(entry, context, input, selection = false, representation) {
1493
+ const operation = selection ? "select" : "replace";
1494
+ await access(context, entry.id, operation, entry);
1495
+ if (entry.conversation?.commands) {
1496
+ if (!selection) throw failure("This conversation retains its original replacement policy.", "conversation_unsupported", 400);
1497
+ return entry.conversation.commands.select(input, context);
1498
+ }
1499
+ if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original selected profile.", "conversation_unsupported");
1500
+ const keys = selection ? ["operationId", "expectedSegmentId", "engine", "configuration"]
1501
+ : ["operationId", "expectedSegmentId", "reason", "engine", "configuration", "briefing"];
1502
+ if (!input || !/^[\w-]{1,128}$/u.test(input.operationId || "") || typeof input.expectedSegmentId !== "string" ||
1503
+ !input.expectedSegmentId || (selection ? typeof input.engine !== "string" : !["engine-change", "model-change", "renewal"].includes(input.reason)) ||
1504
+ input.briefing !== undefined && typeof input.briefing !== "string" ||
1505
+ Object.keys(input).some(key => !keys.includes(key))) {
1506
+ throw failure("Replacement needs an operationId, exact predecessor segment, and an engine-change, model-change or renewal reason.", "conversation_invalid_replacement", 400);
1507
+ }
1508
+ if (entry.active) throw failure("Stop the current turn before replacing its native conversation.", "conversation_busy");
1509
+ if (entry.storageFailure) throw entry.storageFailure;
1510
+ if (entry.conversation) {
1511
+ const state = await readState(entry);
1512
+ const saved = await entry.conversation.state.read();
1513
+ const retry = saved?.replacement?.operationId === input.operationId;
1514
+ let nativeResult;
1515
+ if (state.segmentId !== input.expectedSegmentId && !retry) throw failure("The native predecessor has changed. Read the conversation before replacing it.", "conversation_replacement_conflict");
1516
+ if (selection) {
1517
+ if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported");
1518
+ await entry.conversation.selection.write(input, context);
1519
+ } else {
1520
+ if (!input.briefing?.trim()) throw failure("Native replacement requires a continuity briefing.", "conversation_invalid_replacement", 400);
1521
+ const replacement = createConversationChangeover({ state: entry.conversation.state,
1522
+ identity: entry.conversation.identity, presentation: entry.conversation.presentation,
1523
+ agent: { async closeSession() { await entry.provider.dispose({ changeover: true, forgetConversationBinding: true }); return { ok: true }; } } });
1524
+ nativeResult = await replacement.replace({ engineId: entry.engine, messages: await entry.conversation.transcript.history(),
1525
+ operationId: input.operationId, expectedId: input.expectedSegmentId.startsWith(`${entry.engine}:`)
1526
+ ? input.expectedSegmentId.slice(entry.engine.length + 1) : "", handover: input.briefing || "" });
1527
+ if (retry && saved.replacement.status === "accepted") {
1528
+ return representation === "native" ? nativeResult : { operationId: input.operationId, segmentId: entry.segmentId, duplicate: true };
1529
+ }
1530
+ }
1531
+ const next = await entry.host.conversation({ id: entry.id, context });
1532
+ entry.conversation = next;
1533
+ entry.engine = next.engine;
1534
+ entry.driver = createDriver(entry.engine, entry.host);
1535
+ const current = await readState(entry);
1536
+ entry.provider = await openProvider(entry, entry.driver, current.binding, current.segmentId);
1537
+ entry.goal = null;
1538
+ const receipt = { operationId: input.operationId, segmentId: entry.segmentId, ...(retry ? { duplicate: true } : {}) };
1539
+ await emit(entry, { type: "replaced", reason: input.reason || "model-change", engine: entry.engine, ...receipt });
1540
+ return representation === "native" ? nativeResult : receipt;
1541
+ }
1542
+ const request = { ...structuredClone(input), operation };
1543
+ // Receipt identity must survive JSON storage without picking up a later
1544
+ // runtime's connection default when the same operation is retried.
1545
+ if (request.engine === undefined) delete request.engine;
1546
+ if (request.briefing === undefined) delete request.briefing;
1547
+ if (request.configuration === undefined) delete request.configuration;
1548
+ else if (request.configuration) request.configuration = normalizeConversationConfiguration(request.configuration);
1549
+ let state = (await readState(entry));
1550
+ if (hasUnfinishedConversationRewind(state)) {
1551
+ throw failure("This conversation has an unfinished Undo operation from an earlier runtime. Inspect its saved native and local history offline before replacing it; no history was changed.", "conversation_rewind_inspection_required");
1552
+ }
1553
+ const completed = state.predecessors.find(item => item.replacement?.operationId === input.operationId);
1554
+ if (completed) {
1555
+ if (!isDeepStrictEqual(completed.replacement, request)) throw failure("This replacement operationId already belongs to another request.", "conversation_replacement_conflict");
1556
+ return { operationId: input.operationId, segmentId: completed.successorId, duplicate: true };
1557
+ }
1558
+ let pending = state.replacement;
1559
+ if (pending && !isDeepStrictEqual(pending.request, request)) throw failure("Retry the unfinished native replacement before starting another one.", "conversation_replacement_pending");
1560
+ if (state.segmentId !== input.expectedSegmentId) throw failure("The native predecessor has changed. Read the conversation before replacing it.", "conversation_replacement_conflict");
1561
+ if (!selection && retainedSegments(state).some(segment => segment.request)) throw failure("Resolve pending delivery before replacing native history. Another engine may be selected while its receipt stays pending.", "conversation_delivery_uncertain");
1562
+ if (!selection && !pending && pendingSuccessor(state)) throw failure("Deliver the previous replacement's briefing before replacing native history again.", "conversation_replacement_briefing_pending");
1563
+ if (entry.executionFailure && !pending) throw entry.executionFailure;
1564
+ const engine = pending?.engine || input.engine || state.engine;
1565
+ const configuration = pending?.configuration || (input.configuration
1566
+ ? normalizeConversationConfiguration(input.configuration, { engine, defaultIntegrationId }) : state.configuration);
1567
+ const driver = createDriver(engine, entry.host);
1568
+ driver.validateConfiguration(configuration);
1569
+ const predecessorId = nativeIdentity(state);
1570
+ const nativeReplacement = !selection && state.engine !== "api" && predecessorId &&
1571
+ (state.engine !== "claude" || state.binding.sent);
1572
+ if (selection && engine !== "api" && engine === state.engine && !pending) {
1573
+ // The production native owners apply compatible settings to the running
1574
+ // conversation themselves. Selecting a model is not process retirement.
1575
+ await storage.write(entry.id, async transaction => {
1576
+ const metadata = await transaction.readMetadata();
1577
+ const current = metadata.runtime;
1578
+ current.predecessors.push({ segmentId: current.segmentId, engine: current.engine,
1579
+ configuration: current.configuration, binding: current.binding, seen: current.seen,
1580
+ request: current.request, successorId: current.segmentId, replacement: request });
1581
+ current.configuration = structuredClone(configuration);
1582
+ await transaction.writeMetadata(metadata);
1583
+ });
1584
+ const receipt = { operationId: input.operationId, segmentId: state.segmentId };
1585
+ await emit(entry, { type: "replaced", reason: "model-change", engine, ...receipt });
1586
+ return receipt;
1587
+ }
1588
+ if (!pending) {
1589
+ const history = (await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy);
1590
+ const destination = selection ? state.engine === engine ? state
1591
+ : driver.createBinding ? retainedSegments(state).findLast(segment => segment.engine === engine) : null : null;
1592
+ if (engine !== "api" && (input.briefing || "").length > maximumContinuity) throw new Error("The continuity briefing exceeds the configured history budget.");
1593
+ const continuity = destination ? { text: destination.continuity, attachments: destination.continuityAttachments || [] }
1594
+ : engine === "api" ? conversationContinuity({ history, briefing: input.briefing, maximumCharacters: maximumContinuity })
1595
+ : { text: undefined, attachments: [] };
1596
+ pending = { request, engine, configuration: structuredClone(configuration),
1597
+ reason: selection ? state.engine === engine ? "model-change" : "engine-change" : input.reason,
1598
+ segmentId: destination?.segmentId || randomUUID(),
1599
+ continuity: continuity.text, continuityAttachments: continuity.attachments,
1600
+ seen: destination ? destination.seen : engine === "api" ? conversationHistoryVersions(history) : {},
1601
+ ...(destination ? { binding: destination.binding || null, submission: destination.request } : {}) };
1602
+ }
1603
+
1604
+ function saveReplacement() {
1605
+ return storage.write(entry.id, async transaction => {
1606
+ const metadata = await transaction.readMetadata();
1607
+ if (metadata.runtime.segmentId !== input.expectedSegmentId || metadata.runtime.replacement &&
1608
+ !isDeepStrictEqual(metadata.runtime.replacement.request, request)) {
1609
+ throw failure("The saved replacement changed before it could be written.", "conversation_replacement_conflict");
1610
+ }
1611
+ metadata.runtime.replacement = pending;
1612
+ await transaction.writeMetadata(metadata);
1613
+ });
1614
+ }
1615
+
1616
+ async function closeSession() {
1617
+ try { await entry.provider.dispose(); }
1618
+ catch (error) { entry.executionFailure = error; entry.error = error.message; throw error; }
1619
+ return { ok: true };
1620
+ }
1621
+
1622
+ let provider;
1623
+ const bindingOwner = {};
1624
+ async function replacementTransaction(callback) {
1625
+ // These common storage/host facilities allocate an inert successor handle.
1626
+ // Native process/thread creation stays in ordinary Send, after this commit.
1627
+ await access(context, entry.id, operation, entry);
1628
+ if (pending.segmentId === state.segmentId) {
1629
+ pending.binding = (await readState(entry)).binding || null;
1630
+ }
1631
+ if (!Object.hasOwn(pending, "binding")) {
1632
+ pending.binding = driver.createBinding ? await driver.createBinding() : null;
1633
+ await saveReplacement();
1634
+ }
1635
+ provider = await openProvider(entry, driver, pending.binding, pending.segmentId, bindingOwner);
1636
+ try {
1637
+ await access(context, entry.id, operation, entry);
1638
+ await storage.write(entry.id, async transaction => {
1639
+ const metadata = await transaction.readMetadata();
1640
+ state = metadata.runtime;
1641
+ if (state.segmentId !== input.expectedSegmentId || !isDeepStrictEqual(state.replacement?.request, request)) {
1642
+ throw failure("The saved replacement changed before its binding was released.", "conversation_replacement_conflict");
1643
+ }
1644
+ await callback({
1645
+ async releaseBinding(replacement) {
1646
+ if (replacement && nativeIdentity(state) !== replacement.previous.conversationId) {
1647
+ throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict");
1648
+ }
1649
+ state.predecessors.push({ segmentId: state.segmentId, engine: state.engine,
1650
+ configuration: state.configuration, binding: state.binding, seen: state.seen,
1651
+ continuity: state.continuity, continuityAttachments: state.continuityAttachments, request: state.request,
1652
+ successorId: pending.segmentId, replacement: request,
1653
+ ...(pending.preparedAt ? { preparedAt: pending.preparedAt } : {}) });
1654
+ Object.assign(state, { engine, configuration: pending.configuration, binding: pending.binding,
1655
+ segmentId: pending.segmentId, continuity: pending.continuity, continuityAttachments: pending.continuityAttachments, seen: pending.seen });
1656
+ if (pending.submission) state.request = pending.submission;
1657
+ else delete state.request;
1658
+ },
1659
+ async write(value) {
1660
+ if (value) {
1661
+ if (engine !== "api") state.seen = value.engines[engine].seen;
1662
+ state.lastEngine = value.lastEngine;
1663
+ } else if (!selection && engine !== "api") state.lastEngine = "";
1664
+ delete state.replacement;
1665
+ await transaction.writeMetadata(metadata);
1666
+ }
1667
+ });
1668
+ });
1669
+ } catch (error) { await provider.dispose(); throw error; }
1670
+ }
1671
+
1672
+ if (nativeReplacement) {
1673
+ const replacement = createConversationChangeover({
1674
+ state: {
1675
+ read: changeoverState(entry).read,
1676
+ async write(value) {
1677
+ pending.preparedAt = value.replacement.preparedAt;
1678
+ await saveReplacement();
1679
+ },
1680
+ transaction: replacementTransaction
1681
+ },
1682
+ identity: {
1683
+ async inspect(expectedId) {
1684
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
1685
+ if (runtime.segmentId !== input.expectedSegmentId || nativeIdentity(runtime) !== expectedId) {
1686
+ throw failure("The native predecessor identity changed or is incomplete.", "conversation_replacement_conflict");
1687
+ }
1688
+ return { bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: expectedId } };
1689
+ }
1690
+ },
1691
+ agent: { closeSession }
1692
+ });
1693
+ await replacement.replace({ engineId: engine, messages: [], operationId: input.operationId,
1694
+ expectedId: predecessorId, handover: input.briefing || "" });
1695
+ } else {
1696
+ // API conversations, retained-engine selection and unstarted native handles
1697
+ // have no native predecessor replacement. Keep their common facilities.
1698
+ if (!state.replacement) await saveReplacement();
1699
+ await closeSession();
1700
+ await replacementTransaction(async transaction => {
1701
+ await transaction.releaseBinding();
1702
+ await transaction.write();
1703
+ });
1704
+ }
1705
+ Object.assign(entry, { engine, driver, provider, bindingOwner, segmentId: pending.segmentId,
1706
+ goal: null, executionFailure: null, error: pending.submission?.error || "" });
1707
+ const receipt = { operationId: input.operationId, segmentId: pending.segmentId };
1708
+ await emit(entry, { type: "replaced", reason: pending.reason, engine, ...receipt });
1709
+ return receipt;
1710
+ }
1711
+
1712
+ function retireEntry(entry) {
1713
+ entry.disposed = true;
1714
+ entry.listeners.clear();
1715
+ streams.clear(entry.id);
1716
+ if (entries.get(entry.key) === entry) entries.delete(entry.key);
1717
+ }
1718
+
1719
+ async function disposeEntry(entry, context, options, representation) {
1720
+ if (entry.conversation?.commands) { retireEntry(entry); return; }
1721
+ if (!entry.conversation) await stop(entry, context);
1722
+ await retrySave(entry);
1723
+ const result = await entry.provider.dispose(entry.conversation?.native?.scoped && representation === "native"
1724
+ ? { input: options, context } : representation === "native" ? options : undefined);
1725
+ if (result?.ok === false) return result;
1726
+ if (entry.conversation) await entry.active?.done;
1727
+ retireEntry(entry);
1728
+ return result;
1729
+ }
1730
+
1731
+ return Object.freeze({
1732
+ // Internal host composition uses its existing admission/write lease. These
1733
+ // original coordinators share the scoped owner and introduce no entry or queue.
1734
+ async readPersistentConversation(options) {
1735
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1736
+ return readPersistentConversation(options);
1737
+ },
1738
+ async startPersistentConversationTurn(options) {
1739
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1740
+ return startPersistentConversationTurn(options);
1741
+ },
1742
+ async inspectPersistentConversationAdmission(options) {
1743
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1744
+ return inspectPersistentConversationAdmission(options);
1745
+ },
1746
+ async preparePersistentConversationChangeover(options) {
1747
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1748
+ return preparePersistentConversationChangeover(options);
1749
+ },
1750
+ async inspectPersistentConversationDelivery(options) {
1751
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1752
+ return inspectPersistentConversationDelivery(options);
1753
+ },
1754
+ async stopPersistentConversation(options) {
1755
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1756
+ return stopPersistentConversation(options);
1757
+ },
1758
+ async deletePersistentConversation(options) {
1759
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1760
+ return deletePersistentConversation(options);
1761
+ },
1762
+ async runScopedTurn(options = {}) {
1763
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1764
+ if (["createEphemeralConversation", "startEphemeralConversationTurn", "waitForEphemeralConversationTurn", "stopEphemeralConversation"]
1765
+ .some(name => typeof options.operations?.[name] !== "function")) {
1766
+ throw new TypeError("A scoped turn requires its existing admitted conversation operations.");
1767
+ }
1768
+ return runScopedConversationTurn(options);
1769
+ },
1770
+ // Activity can still be inspected while cleanup retries after runtime closure.
1771
+ // Use the configured owner without retaining or creating a Main conversation.
1772
+ async inspectNativeTemporaryActivity({ id, context } = {}) {
1773
+ if (!await authorize({ context, conversationId: id, operation: "inspectTemporaryActivity" })) {
1774
+ throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
1775
+ }
1776
+ if (typeof defaultHost?.conversation !== "function") throw new TypeError("Native activity requires a configured conversation host.");
1777
+ const prepared = await defaultHost.conversation({ id, context, operation: "inspectTemporaryActivity" });
1778
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) ||
1779
+ !prepared.native || !Object.hasOwn(prepared, "context")) {
1780
+ throw new TypeError("Native activity requires its authorized host preparation.");
1781
+ }
1782
+ return createDriver(prepared.engine, defaultHost).inspectTemporaryActivity(prepared);
1783
+ },
1784
+ // Renewal cleanup keeps its existing trusted server authority after closure.
1785
+ // Neither proof release opens a retained conversation or admits inference.
1786
+ async releaseNativeRenewalPredecessorProcessExitProof({ id, context, input = {} } = {}) {
1787
+ if (typeof defaultHost?.conversation !== "function") {
1788
+ throw new TypeError("Native renewal proof release requires a configured conversation host.");
1789
+ }
1790
+ const prepared = defaultHost.conversation({ id, context, input, operation: "releaseRenewalPredecessorProcessExitProof" });
1791
+ if (!prepared || typeof prepared.then === "function" || prepared.sessionId !== id ||
1792
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
1793
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
1794
+ throw new TypeError("Native renewal proof release requires synchronous configured host preparation.");
1795
+ }
1796
+ const driver = createDriver(prepared.engine, defaultHost);
1797
+ if (typeof driver.releaseRenewalPredecessorProcessExitProof !== "function") {
1798
+ throw new TypeError("The selected conversation driver does not implement native renewal proof release.");
1799
+ }
1800
+ return driver.releaseRenewalPredecessorProcessExitProof(prepared);
1801
+ },
1802
+ async releaseNativeRenewalSuccessorProcessExitProof({ id, context, input = {} } = {}) {
1803
+ if (typeof defaultHost?.conversation !== "function") {
1804
+ throw new TypeError("Native renewal proof release requires a configured conversation host.");
1805
+ }
1806
+ const prepared = defaultHost.conversation({ id, context, input, operation: "releaseRenewalSuccessorProcessExitProof" });
1807
+ if (!prepared || typeof prepared.then === "function" || prepared.sessionId !== id ||
1808
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
1809
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
1810
+ throw new TypeError("Native renewal proof release requires synchronous configured host preparation.");
1811
+ }
1812
+ const driver = createDriver(prepared.engine, defaultHost);
1813
+ if (typeof driver.releaseRenewalSuccessorProcessExitProof !== "function") {
1814
+ throw new TypeError("The selected conversation driver does not implement native renewal proof release.");
1815
+ }
1816
+ return driver.releaseRenewalSuccessorProcessExitProof(prepared);
1817
+ },
1818
+ // Renewal retains the application's existing session and private successor transaction.
1819
+ async generateNativeRenewalHandover({ id, context, input = {} } = {}) {
1820
+ if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
1821
+ await access(context, id, "generateRenewalHandover");
1822
+ if (typeof defaultHost?.conversation !== "function") {
1823
+ throw new TypeError("Native renewal requires a configured conversation host.");
1824
+ }
1825
+ const prepared = await defaultHost.conversation({ id, context, input, operation: "generateRenewalHandover" });
1826
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1827
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
1828
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
1829
+ throw new TypeError("Native renewal requires its authorized host preparation.");
1830
+ }
1831
+ return createDriver(prepared.engine, defaultHost).generateRenewalHandover(prepared);
1832
+ },
1833
+ // Renewal retains the application's existing session and private successor transaction.
1834
+ async seedNativeRenewalHandover({ id, context, input = {} } = {}) {
1835
+ if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
1836
+ await access(context, id, "seedRenewalHandover");
1837
+ if (typeof defaultHost?.conversation !== "function") {
1838
+ throw new TypeError("Native renewal requires a configured conversation host.");
1839
+ }
1840
+ const prepared = await defaultHost.conversation({ id, context, input, operation: "seedRenewalHandover" });
1841
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1842
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
1843
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
1844
+ throw new TypeError("Native renewal requires its authorized host preparation.");
1845
+ }
1846
+ return createDriver(prepared.engine, defaultHost).seedRenewalHandover(prepared);
1847
+ },
1848
+ // Readiness uses the same authorized host and original native owner without
1849
+ // opening a retained handle or starting a conversational turn.
1850
+ async ensureNativeConversation({ id, context } = {}) {
1851
+ if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
1852
+ await access(context, id, "ensure");
1853
+ if (typeof defaultHost?.conversation !== "function") {
1854
+ throw new TypeError("Native conversation readiness requires a configured conversation host.");
1855
+ }
1856
+ const prepared = await defaultHost.conversation({ id, context, operation: "ensure" });
1857
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1858
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) ||
1859
+ !prepared.native || !Object.hasOwn(prepared, "context")) {
1860
+ throw new TypeError("Native conversation readiness requires its authorized host preparation.");
1861
+ }
1862
+ const driver = createDriver(prepared.engine, defaultHost);
1863
+ if (typeof driver.ensureConversation !== "function") {
1864
+ throw new TypeError("The selected conversation driver does not implement native readiness.");
1865
+ }
1866
+ return driver.ensureConversation(prepared);
1867
+ },
1868
+ // Advanced hosts create a native identity before a retained handle exists.
1869
+ // Only the configured host supplies native facilities; caller input is data.
1870
+ async createNativeConversation({ id, context, input = {} } = {}) {
1871
+ if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
1872
+ await access(context, id, "create");
1873
+ if (typeof defaultHost?.conversation !== "function") {
1874
+ throw new TypeError("Native conversation creation requires a configured conversation host.");
1875
+ }
1876
+ const prepared = await defaultHost.conversation({ id, context, input, operation: "create" });
1877
+ if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
1878
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
1879
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
1880
+ throw new TypeError("Native conversation creation requires its authorized host preparation.");
1881
+ }
1882
+ return createDriver(prepared.engine, defaultHost).createConversation(prepared);
1883
+ },
1884
+ async open({ id, context, configuration, engine, host, representation = "canonical" } = {}) {
1885
+ if (typeof id !== "string" || !id.trim()) throw new TypeError("A conversation id is required.");
1886
+ await access(context, id, "open");
1887
+ const selectedHost = host || defaultHost;
1888
+ const conversation = selectedHost?.conversation ? await selectedHost.conversation({ id, context }) : null;
1889
+ const scoped = conversation?.native?.scoped;
1890
+ const commands = conversation?.commands;
1891
+ const invalidClaudeOwner = conversation?.engine === "claude" && (!conversation.native?.owner ||
1892
+ typeof conversation.native.owner !== "object" || typeof conversation.native.owner.acquire !== "function" ||
1893
+ typeof conversation.native.preparation?.cleanup !== "function");
1894
+ const invalidOpenCodeOwner = conversation?.engine === "opencode" && (!conversation.native?.owner ||
1895
+ typeof conversation.native.owner !== "object" || typeof conversation.native.preparation?.cleanup !== "function" ||
1896
+ (scoped ? ["turn", "existing"] : ["message", "inspection", "interruption"])
1897
+ .some(name => typeof conversation.native.preparation?.[name] !== "function") ||
1898
+ !scoped && typeof conversation.native.acquire !== "function");
1899
+ if (commands && (representation !== "canonical" || configuration !== undefined ||
1900
+ ["send", "cancel", "select", "inspectDelivery"].some(name => typeof commands[name] !== "function") ||
1901
+ typeof conversation.read !== "function" || typeof conversation.readStream !== "function")) {
1902
+ throw new TypeError("A logical conversation requires its complete original command, read and stream owners.");
1903
+ }
1904
+ if (scoped && (representation !== "native" || configuration !== undefined || !["codex", "claude", "opencode"].includes(conversation.engine) ||
1905
+ typeof scoped.conversationId !== "string" || !scoped.conversationId ||
1906
+ (conversation.engine === "claude" ? invalidClaudeOwner : conversation.engine === "opencode" ? invalidOpenCodeOwner
1907
+ : typeof conversation.native.preparation?.cleanup !== "function" ||
1908
+ ["startConversationTurn", "readConversation", "waitForConversationTurn", "stopConversation", "deleteConversation"]
1909
+ .some(name => typeof conversation.native.runOwner?.[name] !== "function")))) {
1910
+ throw new TypeError("An original scoped conversation requires its native representation, identity and existing operations.");
1911
+ }
1912
+ if (conversation && (conversation.sessionId !== id || typeof conversation.namespace !== "string" || !conversation.namespace ||
1913
+ !["codex", "claude", "opencode"].includes(conversation.engine) || !scoped && !commands && (
1914
+ (conversation.engine === "codex" ? !conversation.native?.runOwner || !conversation.native?.providerOwner
1915
+ : conversation.engine === "claude" ? invalidClaudeOwner
1916
+ : invalidOpenCodeOwner) ||
1917
+ typeof conversation.prepareInput !== "function" || typeof conversation.read !== "function"))) {
1918
+ throw new TypeError("A bound native conversation requires its original identity, store, preparation and native owners.");
1919
+ }
1920
+ if (!["canonical", "native"].includes(representation) || representation === "native" && !conversation) {
1921
+ throw new TypeError("Native result representation requires an original bound conversation.");
1922
+ }
1923
+ const key = conversation?.namespace || id;
1924
+ let entry = entries.get(key);
1925
+ if (!entry) {
1926
+ entry = { id, key, conversation, host: selectedHost, pending: Promise.resolve(), active: null, listeners: new Set(), error: "" };
1927
+ entries.set(key, entry);
1928
+ entry.initializing = initialize(entry, configuration, engine).catch(error => {
1929
+ if (entries.get(key) === entry) entries.delete(key);
1930
+ throw error;
1931
+ });
1932
+ }
1933
+ await entry.initializing;
1934
+ if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
1935
+ if (host && !isDeepStrictEqual(host, entry.host)) throw failure("This open conversation already belongs to another host scope. Dispose its handle before reopening it on the original host.", "conversation_host_mismatch");
1936
+ if (engine && engine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch");
1937
+ if (configuration) {
1938
+ const metadata = { runtime: await readState(entry) };
1939
+ const supplied = entry.conversation ? configuration
1940
+ : normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
1941
+ if (!isDeepStrictEqual(supplied, metadata.runtime.configuration)) {
1942
+ throw failure("Use configure() to change a saved conversation's settings.", "conversation_configuration_mismatch");
1943
+ }
1944
+ }
1945
+ return Object.freeze({
1946
+ id,
1947
+ get capabilities() { return capabilities(entry); },
1948
+ replace: input => serial(entry, () => replace(entry, context, input, false, representation)),
1949
+ select: input => serial(entry, () => replace(entry, context, input, true, representation)),
1950
+ async read(query) {
1951
+ await access(context, id, "read", entry);
1952
+ const result = await read(entry, representation, context, query);
1953
+ if (entry.conversation?.commands) await access(context, id, "read", entry);
1954
+ return result;
1955
+ },
1956
+ send: input => send(entry, context, input, "user", undefined, representation),
1957
+ wake: input => send(entry, context, input, "application", undefined, representation),
1958
+ readGoal: () => serial(entry, () => readGoal(entry, context, representation)),
1959
+ updateGoal: input => updateGoal(entry, context, input, representation),
1960
+ inspectDelivery({ messageId, threadId } = {}) {
1961
+ if (typeof messageId !== "string" || !messageId) throw new TypeError("A message id is required.");
1962
+ return serial(entry, async () => {
1963
+ if (representation !== "native") return inspectDelivery(entry, context, messageId);
1964
+ await access(context, id, "inspectDelivery", entry);
1965
+ return entry.provider.inspectAdmission({ messageId, threadId, context, representation });
1966
+ });
1967
+ },
1968
+ async wait(input) {
1969
+ await access(context, id, "read", entry);
1970
+ if (entry.conversation?.commands) throw failure("This conversation uses its original read and subscription operations.", "conversation_unsupported", 400);
1971
+ if (entry.conversation?.native?.scoped) return entry.provider.waitNative({ input, context });
1972
+ await entry.pending;
1973
+ await entry.active?.done;
1974
+ await access(context, id, "read", entry);
1975
+ return read(entry);
1976
+ },
1977
+ async cancel(input) {
1978
+ await access(context, id, "cancel", entry);
1979
+ if (entry.conversation?.commands) return entry.conversation.commands.cancel(input, context);
1980
+ await entry.pending;
1981
+ await access(context, id, "cancel", entry);
1982
+ return stop(entry, context, input, representation);
1983
+ },
1984
+ async retrySave() {
1985
+ await access(context, id, "retrySave", entry);
1986
+ return serial(entry, async () => {
1987
+ await access(context, id, "retrySave", entry);
1988
+ return retrySave(entry);
1989
+ });
1990
+ },
1991
+ async configure(patch) {
1992
+ await access(context, id, "configure", entry);
1993
+ if (entry.conversation?.commands) throw failure("Use the conversation's authorized selection operation.", "conversation_unsupported", 400);
1994
+ return serial(entry, async () => {
1995
+ await access(context, id, "configure", entry);
1996
+ if (entry.active) throw failure("Stop the current turn before changing its settings.", "conversation_busy");
1997
+ if (entry.storageFailure) throw entry.storageFailure;
1998
+ if (entry.executionFailure) throw entry.executionFailure;
1999
+ if (entry.conversation) {
2000
+ if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported");
2001
+ return entry.conversation.selection.write({ configuration: patch }, context);
2002
+ }
2003
+ return storage.write(id, async transaction => {
2004
+ const metadata = await transaction.readMetadata();
2005
+ if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before changing settings.", "conversation_replacement_pending");
2006
+ if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
2007
+ throw failure("Resolve uncertain delivery before changing settings.", "conversation_delivery_uncertain");
2008
+ }
2009
+ const next = normalizeConversationConfiguration({ ...metadata.runtime.configuration, ...patch }, {
2010
+ engine: metadata.runtime.engine, defaultIntegrationId
2011
+ });
2012
+ entry.driver.validateConfiguration(next);
2013
+ metadata.runtime.configuration = next;
2014
+ await transaction.writeMetadata(metadata);
2015
+ return structuredClone(next);
2016
+ });
2017
+ });
2018
+ },
2019
+ async subscribe(listener) {
2020
+ await access(context, id, "subscribe", entry);
2021
+ if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original event delivery.", "conversation_unsupported");
2022
+ if (typeof listener !== "function") throw new TypeError("A conversation listener is required.");
2023
+ const subscription = { context, listener };
2024
+ entry.listeners.add(subscription);
2025
+ return () => entry.listeners.delete(subscription);
2026
+ },
2027
+ async dispose(options) {
2028
+ await access(context, id, "dispose", entry);
2029
+ return serial(entry, async () => {
2030
+ await access(context, id, "dispose", entry);
2031
+ const result = await disposeEntry(entry, context, options, representation);
2032
+ if (result?.ok === false || representation === "native") return result;
2033
+ });
2034
+ }
2035
+ });
2036
+ },
2037
+ // Trusted server lifecycle, like close(): the application has already
2038
+ // authorized its project operation. Never expose this through browser routes.
2039
+ // Acquisition is synchronous and creates no retained conversation or queue.
2040
+ async closeNativeProject({ context, input = {} } = {}) {
2041
+ if (typeof defaultHost?.conversation !== "function") {
2042
+ throw new TypeError("Native project cleanup requires a configured conversation host.");
2043
+ }
2044
+ const prepared = defaultHost.conversation({ context, input, operation: "closeProject" });
2045
+ if (!prepared || typeof prepared.then === "function" ||
2046
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2047
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
2048
+ throw new TypeError("Native project cleanup requires synchronous configured host preparation.");
2049
+ }
2050
+ const driver = createDriver(prepared.engine, defaultHost);
2051
+ if (typeof driver.closeProject !== "function") {
2052
+ throw new TypeError("The selected conversation driver does not implement native project cleanup.");
2053
+ }
2054
+ return driver.closeProject(prepared);
2055
+ },
2056
+ // Account revocation and shutdown use this same trusted lifecycle boundary.
2057
+ // The configured owner enters its native closing fence before its first await.
2058
+ invalidateNativeRuntimes({ context, input = {} } = {}) {
2059
+ if (typeof defaultHost?.conversation !== "function") {
2060
+ throw new TypeError("Native runtime invalidation requires a configured conversation host.");
2061
+ }
2062
+ const prepared = defaultHost.conversation({ context, input, operation: "invalidateRuntimes" });
2063
+ if (!prepared || typeof prepared.then === "function" ||
2064
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2065
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
2066
+ throw new TypeError("Native runtime invalidation requires synchronous configured host preparation.");
2067
+ }
2068
+ const driver = createDriver(prepared.engine, defaultHost);
2069
+ if (typeof driver.invalidateRuntimes !== "function") {
2070
+ throw new TypeError("The selected conversation driver does not implement native runtime invalidation.");
2071
+ }
2072
+ return driver.invalidateRuntimes(prepared);
2073
+ },
2074
+ // Startup reconciliation and subscription reset use the existing trusted
2075
+ // lifecycle caller. Native owners retain their own shutdown fences and tasks.
2076
+ reconcileNativeSessions({ context, sessions, options = {} } = {}) {
2077
+ if (typeof defaultHost?.conversation !== "function") {
2078
+ throw new TypeError("Native reconciliation requires a configured conversation host.");
2079
+ }
2080
+ const prepared = defaultHost.conversation({ context, input: sessions, options, operation: "reconcileSessions" });
2081
+ if (!prepared || typeof prepared.then === "function" ||
2082
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2083
+ !Object.hasOwn(prepared, "sessions") || !Object.hasOwn(prepared, "options") ||
2084
+ !Object.hasOwn(prepared, "context")) {
2085
+ throw new TypeError("Native reconciliation requires synchronous configured host preparation.");
2086
+ }
2087
+ const driver = createDriver(prepared.engine, defaultHost);
2088
+ if (typeof driver.reconcileSessions !== "function") {
2089
+ throw new TypeError("The selected conversation driver does not implement native reconciliation.");
2090
+ }
2091
+ return driver.reconcileSessions(prepared);
2092
+ },
2093
+ unsubscribeNativeSessions({ context, sessions } = {}) {
2094
+ if (typeof defaultHost?.conversation !== "function") {
2095
+ throw new TypeError("Native subscription reset requires a configured conversation host.");
2096
+ }
2097
+ const prepared = defaultHost.conversation({ context, input: sessions, operation: "unsubscribeSessions" });
2098
+ if (!prepared || typeof prepared.then === "function" ||
2099
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2100
+ !Object.hasOwn(prepared, "sessions") || !Object.hasOwn(prepared, "context")) {
2101
+ throw new TypeError("Native subscription reset requires synchronous configured host preparation.");
2102
+ }
2103
+ const driver = createDriver(prepared.engine, defaultHost);
2104
+ if (typeof driver.unsubscribeSessions !== "function") {
2105
+ throw new TypeError("The selected conversation driver does not implement native subscription reset.");
2106
+ }
2107
+ return driver.unsubscribeSessions(prepared);
2108
+ },
2109
+ // Historical cleanup has a saved native identity but no retained scope.
2110
+ // Recheck its original caller authority; native owners retain shutdown guards.
2111
+ async interruptNativeDetachedConversation({ id, context, input = {} } = {}) {
2112
+ if (!await authorize({ context, conversationId: id, operation: "interruptDetachedConversation" })) {
2113
+ throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
2114
+ }
2115
+ if (typeof defaultHost?.conversation !== "function") {
2116
+ throw new TypeError("Historical native cleanup requires a configured conversation host.");
2117
+ }
2118
+ const prepared = await defaultHost.conversation({ id, context, input, operation: "interruptDetachedConversation" });
2119
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2120
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
2121
+ throw new TypeError("Historical native cleanup requires its authorized host preparation.");
2122
+ }
2123
+ return createDriver(prepared.engine, defaultHost).interruptDetachedConversation(prepared);
2124
+ },
2125
+ async deleteNativeDetachedConversation({ id, context, input = {} } = {}) {
2126
+ if (!await authorize({ context, conversationId: id, operation: "deleteDetachedConversation" })) {
2127
+ throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
2128
+ }
2129
+ if (typeof defaultHost?.conversation !== "function") {
2130
+ throw new TypeError("Historical native cleanup requires a configured conversation host.");
2131
+ }
2132
+ const prepared = await defaultHost.conversation({ id, context, input, operation: "deleteDetachedConversation" });
2133
+ if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2134
+ !Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
2135
+ throw new TypeError("Historical native cleanup requires its authorized host preparation.");
2136
+ }
2137
+ return createDriver(prepared.engine, defaultHost).deleteDetachedConversation(prepared);
2138
+ },
2139
+ // Trusted server storage operations retain the saved engine and preservation
2140
+ // policy. They neither authorize inference nor acquire a retained handle.
2141
+ async listNativeConversationStorage({ id, context, binding } = {}) {
2142
+ if (typeof defaultHost?.conversation !== "function") {
2143
+ throw new TypeError("Native history storage requires a configured conversation host.");
2144
+ }
2145
+ const prepared = await defaultHost.conversation({ id, context, input: binding, operation: "listNativeConversationStorage" });
2146
+ if (prepared?.sessionId !== id || prepared.binding !== binding || prepared.engine !== binding?.engineId ||
2147
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2148
+ !Object.hasOwn(prepared, "context")) {
2149
+ throw new TypeError("Native history storage requires its saved binding and configured host preparation.");
2150
+ }
2151
+ return createDriver(prepared.engine, defaultHost).listConversationStorage(prepared);
2152
+ },
2153
+ async retireNativeConversationHistory({ id, context, binding } = {}) {
2154
+ if (typeof defaultHost?.conversation !== "function") {
2155
+ throw new TypeError("Native history storage requires a configured conversation host.");
2156
+ }
2157
+ const prepared = await defaultHost.conversation({ id, context, input: binding, operation: "retireConversationHistory" });
2158
+ if (prepared?.sessionId !== id || prepared.binding !== binding || prepared.engine !== binding?.engineId ||
2159
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2160
+ !Object.hasOwn(prepared, "context")) {
2161
+ throw new TypeError("Native history storage requires its saved binding and configured host preparation.");
2162
+ }
2163
+ return createDriver(prepared.engine, defaultHost).retireConversationHistory(prepared);
2164
+ },
2165
+ // Cleanup may retry after runtime closure. Missing or unready retained
2166
+ // handles use the configured native owner without opening a Main record.
2167
+ async disposeNative({ namespace, sessionId, context, options }) {
2168
+ const authorizeCleanup = async () => {
2169
+ if (!await authorize({ context, conversationId: sessionId, operation: "dispose" })) {
2170
+ throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
2171
+ }
2172
+ };
2173
+ const cleanupUnopened = async () => {
2174
+ if (typeof defaultHost?.conversation !== "function") return null;
2175
+ await authorizeCleanup();
2176
+ const prepared = await defaultHost.conversation({ id: sessionId, context, input: options, operation: "dispose" });
2177
+ if (prepared?.sessionId !== sessionId || prepared.namespace !== namespace ||
2178
+ !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
2179
+ !Object.hasOwn(prepared, "context") || !Object.hasOwn(prepared, "options")) {
2180
+ throw new TypeError("Native cleanup requires its authorized host preparation.");
2181
+ }
2182
+ return { result: await createDriver(prepared.engine, defaultHost).disposeNative(prepared) };
2183
+ };
2184
+ const entry = entries.get(namespace);
2185
+ if (!entry || entry.disposed) return cleanupUnopened();
2186
+ if (entry.id !== sessionId || !entry.conversation) {
2187
+ throw failure("Native cleanup requires its original bound conversation.", "conversation_host_mismatch");
2188
+ }
2189
+ await authorizeCleanup();
2190
+ const retained = await serial(entry, async () => {
2191
+ if (entry.disposed) return null;
2192
+ await authorizeCleanup();
2193
+ if (!entry.provider) {
2194
+ retireEntry(entry);
2195
+ return null;
2196
+ }
2197
+ return { result: await disposeEntry(entry, context, options, "native") };
2198
+ });
2199
+ return retained || cleanupUnopened();
2200
+ },
2201
+ // Host publication from the same native owner, through the existing entry.
2202
+ // This does not subscribe to a provider or retain another observer registry.
2203
+ async publishNative({ namespace, sessionId, event }) {
2204
+ const entry = entries.get(namespace);
2205
+ if (!entry || entry.id !== sessionId || entry.disposed || !entry.conversation) return;
2206
+ await entry.initializing;
2207
+ return entry.conversation.commands ? publish(entry, event) : entry.provider.publishNative(event);
2208
+ },
2209
+ async close() {
2210
+ closed = true;
2211
+ const results = await Promise.allSettled([...entries.values()].map(entry => serial(entry, async () => {
2212
+ await entry.initializing;
2213
+ const result = await disposeEntry(entry);
2214
+ if (result?.ok === false) throw Object.assign(new Error(result.error || "Native cleanup could not be confirmed."), result);
2215
+ })));
2216
+ const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
2217
+ if (errors.length) throw new AggregateError(errors, "Some conversations could not be safely closed. Restore their storage or execution host and retry close().");
2218
+ }
2219
+ });
2220
+ }