@jskit-ai/assistant-core 0.2.6 → 0.2.8

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +18 -12
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -0,0 +1,394 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { notSentDeliveryReceipt } from "./providers/scoped.js";
3
+ import { isCompletedEnvelopeTurn } from "./transcript.js";
4
+ import { conversationNativeIdentity } from "./runtimeStateUpgrade.js";
5
+ import { createConversationChangeover, conversationNativeMessages } from "./continuity.js";
6
+ import { deliveryFailure, failure, stateErrorFields } from "./conversationRefusals.js";
7
+ import { pendingSuccessor, retainedSegments, submittedMessage } from "./conversationReads.js";
8
+
9
+ /** @typedef {import("./conversationEntries.js").ConversationEntry} ConversationEntry */
10
+ /** @typedef {import("./conversationSending.js").ConversationRequest} ConversationRequest */
11
+
12
+ /**
13
+ * Native delivery receipts: the changeover state a native owner sees, the changeover that
14
+ * sends a message with the history its engine has not seen, and recovery of a message
15
+ * whose delivery is unconfirmed.
16
+ * @param {object} dependencies
17
+ * @param {import("../types.js").ReentrantConversationStorage | null} dependencies.storage
18
+ * @param {import("../types.js").ConversationTranscript | null} dependencies.transcript
19
+ * @param {import("./conversationEvents.js").ConversationEvents} dependencies.events
20
+ * @param {import("./conversationEntries.js").ConversationEntries} dependencies.entries
21
+ * @param {import("./conversationReads.js").ConversationReads} dependencies.reads
22
+ * @param {import("../types.js").ConversationLimits} dependencies.limits
23
+ * @param {boolean} dependencies.persistCommentary
24
+ * @param {boolean} dependencies.completedEnvelope
25
+ * @param {number} dependencies.maximumFinalReply
26
+ * @param {number} dependencies.maximumCompletedOutput
27
+ */
28
+ function createConversationDelivery({ storage, transcript, events, entries, reads, limits, persistCommentary, completedEnvelope, maximumFinalReply,
29
+ maximumCompletedOutput }) {
30
+ /**
31
+ * The changeover owner sees its usual state contract. Only this storage adapter knows about the
32
+ * runtime's segment registry and its one authored request record; provider binding writes stay
33
+ * fenced on their own.
34
+ * @param {ConversationEntry} entry
35
+ * @param {ConversationRequest | null} [request]
36
+ * @param {object} [transaction] The storage transaction already open, if any.
37
+ */
38
+ function changeoverState(entry, request, transaction) {
39
+ const readMetadata = () => transaction ? transaction.readMetadata()
40
+ : storage.read(entry.id, current => current.readMetadata());
41
+ const write = callback => transaction ? callback(transaction) : storage.write(entry.id, callback);
42
+ return {
43
+ async read() {
44
+ const { runtime } = await readMetadata();
45
+ const engines = Object.fromEntries(retainedSegments(runtime).filter(segment => segment.engine !== "api").map(segment => {
46
+ const pending = segment.request;
47
+ return [segment.engine, { seen: structuredClone(segment.seen),
48
+ ...(typeof pending?.message === "string" || pending?.inspectionOnly ? { pending: structuredClone({ ...pending,
49
+ ...(pending.inspectionOnly ? { attempted: true, displayMessage: pending.text, displayAttachments: pending.attachments } : {}) }) } : {}) }];
50
+ }));
51
+ const predecessor = pendingSuccessor(runtime);
52
+ const preparing = runtime.replacement?.request?.operation === "replace" ? runtime.replacement : null;
53
+ return { lastEngine: runtime.lastEngine, engines,
54
+ ...(preparing ? { replacement: { engineId: preparing.engine, status: "preparing",
55
+ operationId: preparing.request.operationId, handover: preparing.request.briefing || "",
56
+ bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: conversationNativeIdentity(runtime) },
57
+ ...(preparing.preparedAt ? { preparedAt: preparing.preparedAt } : {}) } }
58
+ : predecessor ? { replacement: { engineId: runtime.engine, status: "ready",
59
+ operationId: predecessor.replacement.operationId, handover: predecessor.replacement.briefing || "",
60
+ previous: { engineId: predecessor.engine, conversationId: conversationNativeIdentity(predecessor) },
61
+ ...(predecessor.preparedAt ? { preparedAt: predecessor.preparedAt } : {}) } } : {}) };
62
+ },
63
+ write(value) {
64
+ return write(async current => {
65
+ const metadata = await current.readMetadata();
66
+ const runtime = metadata.runtime;
67
+ if (runtime.segmentId !== entry.segmentId || runtime.engine !== entry.engine) throw new Error("The native conversation changed before its delivery was saved.");
68
+ const binding = value.engines[entry.engine];
69
+ if (binding.seen !== undefined) runtime.seen = binding.seen;
70
+ else if (!runtime.request?.inspectionOnly) throw new Error("Native delivery has no saved history cursor.");
71
+ // A pre-preparation v2 reservation has no snapshot. Its confirmed
72
+ // receipt settles only that request; it cannot acknowledge extra history.
73
+ runtime.lastEngine = value.lastEngine;
74
+ if (binding.pending) {
75
+ const authored = runtime.request?.messageId === binding.pending.messageId ? runtime.request
76
+ : request?.input.messageId === binding.pending.messageId ? { ...request.input, at: request.at } : null;
77
+ if (!authored) throw new Error("A native prompt has no matching authored request.");
78
+ // Inspection-only predecessors retain exactly their old authored
79
+ // record. Their absent prompt must never be manufactured on read.
80
+ runtime.request = authored.inspectionOnly ? authored : { ...authored, ...binding.pending };
81
+ } else if (runtime.request && (typeof runtime.request.message === "string" || runtime.request.inspectionOnly ||
82
+ runtime.request.messageId === request?.input.messageId)) {
83
+ delete runtime.request;
84
+ }
85
+ const predecessor = pendingSuccessor(runtime);
86
+ if (predecessor && value.replacement?.status === "accepted") {
87
+ predecessor.acceptedAt = value.replacement.acceptedAt;
88
+ predecessor.successorConversationId = value.replacement.successorConversationId;
89
+ delete runtime.continuity;
90
+ delete runtime.continuityAttachments;
91
+ }
92
+ await current.writeMetadata(metadata);
93
+ });
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * The changeover that sends a message with the history its native engine has not seen.
100
+ * @param {ConversationEntry} entry
101
+ * @param {object} context
102
+ * @param {ConversationRequest | null} request
103
+ * @param {(input: object) => Promise<unknown>} sendMessage
104
+ * @param {AbortSignal} [signal]
105
+ */
106
+ function nativeChangeover(entry, context, request, sendMessage, signal) {
107
+ let observation;
108
+ const initialWindow = limits.maxInitialNativeHistoryMessages;
109
+ const budget = entry.conversation?.maximumHistoryBytes;
110
+ if (initialWindow !== undefined && (!Number.isSafeInteger(initialWindow) || initialWindow < 0)) {
111
+ throw new TypeError("Initial native history requires a non-negative message limit.");
112
+ }
113
+ return createConversationChangeover({
114
+ state: entry.conversation?.state || changeoverState(entry, request),
115
+ captureContext: !entry.conversation, applicationMessages: !entry.conversation,
116
+ maximumInitialMessages: initialWindow === undefined ? (budget ? Infinity : undefined)
117
+ : Math.max(0, initialWindow - (request?.input.origin === "user" ? 1 : 0)),
118
+ maximumInitialMessageCharacters: limits.maxInitialNativeHistoryMessageCharacters,
119
+ ...(budget ? { maximumHistoryBytes: async options => {
120
+ try {
121
+ await events.access(context, entry.id, request.operation, entry);
122
+ signal.throwIfAborted();
123
+ // A host may free native context first; show that as compacting.
124
+ const bytes = await budget({ ...options, context, signal,
125
+ // readable-check: allow setter-seam - not a slot: the history budget resolver reports compacting through setPhase
126
+ setPhase: phase => events.setPhase(entry, phase === "compacting" ? "compacting" : "preparing") });
127
+ signal.throwIfAborted();
128
+ return bytes;
129
+ } catch (error) {
130
+ // Sizing missed history precedes native dispatch, so this message
131
+ // was definitely not sent. Hosts and clients report it as such.
132
+ throw Object.assign(error, { delivery: "not-sent" });
133
+ }
134
+ } } : {}),
135
+ presentation: entry.conversation?.presentation || { unconfirmedCode: "conversation_delivery_uncertain",
136
+ unconfirmedMessage: "The previous submission has uncertain delivery. Retry this message to check its native receipt; it will not be resent automatically." },
137
+ transcript: entry.conversation?.transcript || {
138
+ hasMessage: messageId => transcript.conversationMessageIdExists(entry.id, messageId),
139
+ async writeUserMessage({ messageId }) {
140
+ if (observation) await recoverDelivery(entry, context, messageId, { observation, allowActive: true });
141
+ }
142
+ },
143
+ agent: {
144
+ sendMessage,
145
+ async inspectMessageAdmission({ messageId, threadId }) {
146
+ if (entry.conversation) {
147
+ observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
148
+ await events.access(context, entry.id, request?.operation || "inspectDelivery", entry);
149
+ return { admission: observation.accepted ? "accepted" : "unknown" };
150
+ }
151
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
152
+ if (!threadId || threadId !== conversationNativeIdentity(runtime) || runtime.request?.messageId !== messageId) return null;
153
+ observation = await entry.provider.inspectAdmission({ ...runtime.request, context, configuration: runtime.configuration });
154
+ await events.access(context, entry.id, request?.operation || "inspectDelivery", entry);
155
+ return { admission: observation.accepted ? "accepted" : "unknown" };
156
+ }
157
+ }
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Records saved turns as seen by the entry's native engine.
163
+ * @param {ConversationEntry} entry
164
+ * @param {object} transaction
165
+ * @param {object[]} turns
166
+ */
167
+ async function rememberNative(entry, transaction, turns) {
168
+ const changeover = createConversationChangeover({ state: changeoverState(entry, null, transaction) });
169
+ await changeover.remember({ engineId: entry.engine, messages: conversationNativeMessages(turns) });
170
+ }
171
+
172
+ /**
173
+ * Recovers a message whose delivery is unconfirmed, from its engine's own admission record.
174
+ * @param {ConversationEntry} entry
175
+ * @param {object} context
176
+ * @param {string} messageId
177
+ * @param {{ observation?: object, allowActive?: boolean }} [options]
178
+ */
179
+ async function recoverDelivery(entry, context, messageId, { observation: suppliedObservation, allowActive = false } = {}) {
180
+ await events.access(context, entry.id, "inspectDelivery", entry);
181
+ if (entry.active && !allowActive) throw failure("Wait for the current submission before checking delivery.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForSubmission" });
182
+ if (entry.storageFailure) throw entry.storageFailure;
183
+ if (entry.executionFailure) throw entry.executionFailure;
184
+ const history = await reads.readTranscript(entry);
185
+ const existing = history.find(turn => submittedMessage(turn)?.messageId === messageId);
186
+ const receipt = existing && { status: "accepted", messageId, turnId: existing.turnId, duplicate: true };
187
+ // Turns written before the first runtime segment carry no runtime metadata.
188
+ const existingRuntime = existing?.metadata?.runtime;
189
+ const goalMessageId = existingRuntime?.goalMessageId;
190
+ const goalRequest = Boolean(submittedMessage(existing || {})?.goal || goalMessageId === messageId);
191
+ if (existing && (existingRuntime?.segmentId !== entry.segmentId || existingRuntime.supersededBy ||
192
+ existingRuntime.status === "complete" && !goalRequest)) return receipt;
193
+ if (goalMessageId && goalMessageId !== messageId) {
194
+ const owner = history.find(turn => submittedMessage(turn)?.messageId === goalMessageId);
195
+ const ownerRuntime = owner?.metadata?.runtime;
196
+ if (ownerRuntime?.goalMessageId !== goalMessageId || ownerRuntime.segmentId !== entry.segmentId) throw new Error("The goal command for this native continuation is missing.");
197
+ const result = await inspectDelivery(entry, context, goalMessageId);
198
+ return { ...receipt, recovered: result.recovered === true };
199
+ }
200
+ const metadata = { runtime: await entries.readState(entry) };
201
+ if (metadata.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending_before_recovery", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
202
+ const savedRequests = [...metadata.runtime.predecessors, metadata.runtime].filter(segment => segment.request?.messageId === messageId);
203
+ if (!existing && savedRequests.length && savedRequests.every(segment =>
204
+ ["codex", "claude", "opencode"].includes(segment.engine) && segment.request.attempted === false && !segment.request.inspectionOnly &&
205
+ isDeepStrictEqual(segment.request, savedRequests[0].request))) {
206
+ await events.access(context, entry.id, "inspectDelivery", entry);
207
+ return notSentDeliveryReceipt(messageId);
208
+ }
209
+ const request = existing ? submittedMessage(existing) : metadata.runtime.request;
210
+ if (!request || request.messageId !== messageId || !entry.provider.inspectAdmission) return receipt || { status: "unknown", messageId };
211
+ const pending = metadata.runtime.request?.messageId === messageId ? metadata.runtime.request : null;
212
+ if (pending?.threadId && pending.threadId !== conversationNativeIdentity(metadata.runtime)) return receipt || { status: "unknown", messageId };
213
+ const observation = suppliedObservation || await entry.provider.inspectAdmission({ ...request, nativeTurnId: existing?.metadata?.runtime?.nativeTurnId,
214
+ context, configuration: metadata.runtime.configuration });
215
+ await events.access(context, entry.id, "inspectDelivery", entry);
216
+ // The engine proved it never admitted this saved request. Record that it
217
+ // was not sent, as a definite rejection would, so Retry may send it.
218
+ if (!existing && observation.notAdmitted && pending && !pending.inspectionOnly && !entry.active) {
219
+ await storage.write(entry.id, async transaction => {
220
+ const current = await transaction.readMetadata();
221
+ if (current.runtime.request?.messageId !== messageId) return;
222
+ current.runtime.request.attempted = false;
223
+ await transaction.writeMetadata(current);
224
+ });
225
+ return notSentDeliveryReceipt(messageId);
226
+ }
227
+ if (!observation.accepted) return { ...(receipt || { status: "unknown", messageId }),
228
+ ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
229
+ if (entry.driver.canonicalTranscript) {
230
+ const latest = await reads.readTranscript(entry);
231
+ const saved = latest.find(turn => submittedMessage(turn)?.messageId === messageId);
232
+ if (!saved) throw new Error("The native receipt did not persist its authored message.");
233
+ const state = saved.metadata?.runtime;
234
+ if (state?.segmentId !== entry.segmentId) throw new Error("The native receipt belongs to another conversation segment.");
235
+ const unknownTool = saved.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
236
+ const status = ["cancelled", "failed"].includes(state.status) ? state.status : unknownTool ? "interrupted"
237
+ : observation.recoveryLimitation ? "interrupted" : ["completed", "idle"].includes(observation.status) ? "complete"
238
+ : observation.status === "failed" ? "failed" : "interrupted";
239
+ const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
240
+ : observation.recoveryLimitation || (status === "complete" ? "" : state.error || "Delivery was confirmed. Completion could not be verified; this request was not replayed.");
241
+ await storage.write(entry.id, async transaction => {
242
+ const current = await transaction.readMetadata();
243
+ if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) {
244
+ throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending_before_recovery", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
245
+ }
246
+ const exact = await transaction.readTurn(saved.turnId);
247
+ const { error: _previousError, ...runtime } = exact.metadata.runtime;
248
+ await transaction.updateTurnMetadata(saved.turnId, { runtime: { ...runtime, status, ...(error ? { error } : {}) } });
249
+ });
250
+ if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
251
+ entry.error = error;
252
+ const result = { ...(receipt || { status: "accepted", messageId, turnId: saved.turnId }),
253
+ recovered: observation.recovered === true, ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
254
+ if (!existing) await events.emit(entry, { type: "accepted", ...result });
255
+ if (result.recovered) await events.emit(entry, { type: "settled", turnId: saved.turnId, messageId, status, error, ...stateErrorFields(error) });
256
+ return result;
257
+ }
258
+ const recovered = await storage.write(entry.id, async transaction => {
259
+ const current = await transaction.readMetadata();
260
+ if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending_before_recovery", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
261
+ const native = { ...observation, messageId };
262
+ const results = [];
263
+ const id = messageId;
264
+ let turn = history.find(turn => submittedMessage(turn)?.messageId === id);
265
+ if (turn?.metadata?.runtime?.supersededBy || turn && turn.metadata?.runtime?.segmentId !== entry.segmentId) {
266
+ throw new Error("Native history refers to a turn outside the active conversation segment.");
267
+ }
268
+ if (turn?.metadata?.runtime?.status !== "complete") {
269
+ if (!turn) {
270
+ const input = request;
271
+ const turnId = await transaction.nextTurnId();
272
+ await transaction.appendMessage(turnId, { role: input.origin === "application" ? "system" : "user", origin: input.origin || "user",
273
+ messageId: id, text: input.text, attachments: input.attachments, at: input.at,
274
+ ...(input.data !== undefined ? { data: input.data } : {}), ...(input.goal ? { goal: input.goal } : {}),
275
+ turnMetadata: { runtime: { engine: entry.engine, segmentId: entry.segmentId, origin: input.origin || "user",
276
+ ...(input.completedEnvelope === true ? { completedEnvelope: true } : {}),
277
+ ...(typeof input.nativeToolAttempt === "boolean" ? { nativeToolAttempt: input.nativeToolAttempt } : {}) } } });
278
+ turn = await transaction.readTurn(turnId);
279
+ history.push(turn);
280
+ }
281
+ const runtime = turn.metadata.runtime;
282
+ if (runtime.nativeTurnId && native.nativeTurnId && runtime.nativeTurnId !== native.nativeTurnId) throw new Error("Native history changed the accepted turn's identity.");
283
+ const replies = [];
284
+ for (const message of native.messages || []) {
285
+ if (!persistCommentary && message.role === "commentary") continue;
286
+ if (message.role === "assistant") replies.push(message.text);
287
+ else {
288
+ const messageId = `${turn.turnId}:${message.id}`;
289
+ if (!await transaction.hasMessage(messageId)) await transaction.appendMessage(turn.turnId,
290
+ { messageId, ...(message.outputId ? { outputId: `${turn.turnId}:${message.outputId}` } : {}),
291
+ role: message.role, text: message.text, at: submittedMessage(turn).at });
292
+ }
293
+ }
294
+ const nativeReplies = (native.messages || []).filter(message => message.role === "assistant");
295
+ const outputId = nativeReplies.length === 1 && nativeReplies[0].outputId;
296
+ const replyText = replies.join("\n\n");
297
+ const finalReplyLimit = completedEnvelope && isCompletedEnvelopeTurn(turn) && !submittedMessage(turn)?.goal
298
+ ? maximumCompletedOutput : maximumFinalReply;
299
+ const oversizedReply = replyText.length > finalReplyLimit;
300
+ if (replies.length && !oversizedReply) await transaction.replaceAssistant(turn.turnId, { messageId: turn.assistant?.messageId || `${turn.turnId}:assistant`,
301
+ ...(!turn.assistant?.outputId && outputId ? { outputId: `${turn.turnId}:${outputId}` } : {}),
302
+ role: "assistant", text: replyText, at: submittedMessage(turn).at });
303
+ const unknownTool = turn.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
304
+ const retainedFailure = ["cancelled", "failed"].includes(runtime.status);
305
+ const status = retainedFailure ? runtime.status : oversizedReply ? "failed" : unknownTool ? "interrupted" : native.status || "interrupted";
306
+ const { error: previousError, ...saved } = runtime;
307
+ const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
308
+ : oversizedReply ? retainedFailure && (previousError || native.error) || "The assistant exceeded the configured final reply limit."
309
+ : status === "complete" ? "" : previousError || native.error || "Delivery was confirmed from native history. Completion was not confirmed; this request was not replayed.";
310
+ await transaction.updateTurnMetadata(turn.turnId, { runtime: { ...saved, status,
311
+ ...(request.goal || goalRequest ? { goalMessageId: messageId } : {}),
312
+ ...(native.nativeTurnId ? { nativeTurnId: native.nativeTurnId } : {}), ...(error ? { error } : {}) } });
313
+ results.push({ turnId: turn.turnId, messageId: id, status, error });
314
+ }
315
+ await transaction.writeMetadata(current);
316
+ return results;
317
+ });
318
+ if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
319
+ entry.error = recovered.find(turn => turn.error)?.error || "";
320
+ const result = { ...(receipt || { status: "accepted", messageId, turnId: recovered[0].turnId }), recovered: recovered.length > 0 };
321
+ if (!existing) await events.emit(entry, { type: "accepted", ...result });
322
+ for (const turn of recovered) await events.emit(entry, { type: "settled", ...turn, ...stateErrorFields(turn.error) });
323
+ return result;
324
+ }
325
+
326
+ /**
327
+ * @param {ConversationEntry} entry
328
+ * @param {object} context
329
+ * @param {string} messageId
330
+ */
331
+ async function inspectDelivery(entry, context, messageId) {
332
+ if (entry.conversation?.commands) {
333
+ await events.access(context, entry.id, "inspectDelivery", entry);
334
+ const result = await entry.conversation.commands.inspectDelivery({ messageId }, context);
335
+ await events.access(context, entry.id, "inspectDelivery", entry);
336
+ return result;
337
+ }
338
+ if (entry.conversation) {
339
+ await events.access(context, entry.id, "inspectDelivery", entry);
340
+ const state = await entries.readState(entry);
341
+ // A running owner completes its own receipt. Only an idle owner recovers
342
+ // the saved changeover, preserving write-receipt-before-clear ordering.
343
+ if (!entry.active && state.request?.messageId === messageId && state.request.attempted) {
344
+ const sender = nativeChangeover(entry, context, null, () => {
345
+ throw new Error("Receipt inspection cannot submit another native prompt.");
346
+ });
347
+ const delivered = await sender.send({ engineId: entry.engine,
348
+ messages: await entry.conversation.transcript.history(), input: { messageId }, inspectOnly: true });
349
+ if (delivered?.notSent) return notSentDeliveryReceipt(messageId);
350
+ if (!delivered?.delivered) throw deliveryFailure(delivered);
351
+ }
352
+ const row = (await reads.readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId);
353
+ if (row) return { status: "accepted", messageId, turnId: row.turnId, duplicate: true };
354
+ const threadId = state.request?.messageId === messageId ? state.request.threadId : conversationNativeIdentity(state);
355
+ if (!threadId) return { status: "unknown", messageId };
356
+ const observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
357
+ await events.access(context, entry.id, "inspectDelivery", entry);
358
+ // Native-only acceptance is evidence of delivery, not permission to
359
+ // manufacture an authored row or use a native turn ID as its receipt.
360
+ if (observation.accepted) return { status: "accepted", messageId, duplicate: true };
361
+ return observation.notAdmitted && !entry.active ? notSentDeliveryReceipt(messageId) : { status: "unknown", messageId };
362
+ }
363
+ const result = await recoverDelivery(entry, context, messageId);
364
+ if (result.status !== "accepted" || entry.engine === "api") return result;
365
+ if (entry.driver.canonicalTranscript && result.recoveryLimitation) {
366
+ const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
367
+ if (runtime.request?.goal || runtime.binding?.goalRequest?.messageId === messageId ||
368
+ runtime.binding?.goalReceipt?.messageId === messageId) return result;
369
+ }
370
+ const messages = conversationNativeMessages((await reads.readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy));
371
+ const sender = nativeChangeover(entry, context, null, () => {
372
+ throw new Error("Receipt inspection cannot submit another native prompt.");
373
+ });
374
+ const pending = (await entries.readState(entry)).request;
375
+ if (pending?.messageId === messageId) {
376
+ const delivered = await sender.send({ engineId: entry.engine, messages, input: { messageId } });
377
+ if (!delivered?.delivered) throw deliveryFailure(delivered);
378
+ }
379
+ await sender.remember({ engineId: entry.engine, messages });
380
+ return result;
381
+ }
382
+
383
+ return Object.freeze({
384
+ changeoverState,
385
+ nativeChangeover,
386
+ rememberNative,
387
+ recoverDelivery,
388
+ inspectDelivery
389
+ });
390
+ }
391
+
392
+ /** @typedef {ReturnType<typeof createConversationDelivery>} ConversationDelivery */
393
+
394
+ export { createConversationDelivery };