@jskit-ai/assistant-core 0.2.7 → 0.2.9

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 +13 -10
  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,388 @@
1
+ // Observing a session's native turn until its final response: the turn's
2
+ // public snapshot, its failure, its event observation, the projection of its
3
+ // messages into the store, and the retained final-answer evidence.
4
+ import {
5
+ waitForOpenCodeFinalResponse, openCodeLastAssistantResult, observeOpenCodeEvents, openCodeMessageRows, openCodeRowsForInput,
6
+ recoverWithoutProjection
7
+ } from "./openCodeTurn.js";
8
+ import { openCodeAssistantMessageText as assistantMessageText } from "./openCodeClient.js";
9
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
10
+ import { normalizeNullableText } from "./normalize.js";
11
+
12
+ /** A projection that keeps no native output identities. */
13
+ function noOutputId() {}
14
+
15
+ /** A turn's public state, or null before it has one. */
16
+ export function openCodeTurnSnapshot(turn = null, threadId = "") {
17
+ const source = isRecord(turn) ? turn : {};
18
+ const active = source.active === true;
19
+ return active || normalizeNullableText(source.id)
20
+ ? {
21
+ active,
22
+ error: normalizeNullableText(source.error),
23
+ id: normalizeNullableText(source.id),
24
+ phase: active && !source.observationError ? normalizeNullableText(source.phase) : "",
25
+ startedAt: normalizeNullableText(source.startedAt),
26
+ state: normalizeNullableText(source.state) || (active ? "active" : "completed"),
27
+ status: source.observationError ? "observation_lost" : normalizeNullableText(source.state) || (active ? "active" : "completed"),
28
+ threadId: normalizeNullableText(source.threadId || threadId),
29
+ updatedAt: normalizeNullableText(source.updatedAt)
30
+ }
31
+ : null;
32
+ }
33
+
34
+ /** Whether a failure reads as a rejected or missing credential. */
35
+ function openCodeCredentialFailure(value = "") {
36
+ const failure = normalizeNullableText(value);
37
+ return (
38
+ /\b(?:401|403)\b/u.test(failure) ||
39
+ (
40
+ /(?:api[-_\s]?key|authentication|authorization|credential|access[-_\s]?token|unauthori[sz]ed|forbidden)/iu.test(failure) &&
41
+ /(?:denied|expired|failed|forbidden|incorrect|invalid|missing|rejected|revoked|unauthori[sz]ed)/iu.test(failure)
42
+ )
43
+ );
44
+ }
45
+
46
+ /** Whether a native error is the provider API's own failure. */
47
+ export function openCodeProviderApiFailure(error) {
48
+ return normalizeNullableText(error?.name) === "APIError";
49
+ }
50
+
51
+ /** Observes a target's native events for a projection that reports their failure and close. */
52
+ export function observePreparedEvents(target, turn, abortController, signal, projection) {
53
+ const observation = observeOpenCodeEvents(target.server.client, target.upstreamSessionId, {
54
+ abortController, signal,
55
+ eventStartedAt: turn ? turn.eventStartedAt : null,
56
+ readError: projection.readError,
57
+ onEvent: summary => projection.onEvent(summary, target)
58
+ });
59
+ void observation.completion.then(() => {
60
+ projection.failure(observation.readFailure());
61
+ projection.closed();
62
+ });
63
+ return {
64
+ close: observation.close,
65
+ readFailure: observation.readFailure,
66
+ waitUntilReady(options) {
67
+ const ready = observation.waitUntilReady(options);
68
+ void ready.catch(projection.failure);
69
+ return ready;
70
+ }
71
+ };
72
+ }
73
+
74
+ /** Projects a turn's native assistant messages and reasoning into the conversation store. */
75
+ export async function writeConversationProjection(sessionId, messages = null, {
76
+ inputMessageId = "",
77
+ streaming = false,
78
+ captureFinalAssistantResult = false
79
+ } = {}, projection) {
80
+ const { outputId: nativeOutputId = noOutputId } = projection;
81
+ let failure = "";
82
+ let finalAssistantResult = null;
83
+ let providerApiFailure = false;
84
+ const rows = (inputMessageId
85
+ ? openCodeRowsForInput(messages, inputMessageId)
86
+ : openCodeMessageRows(messages)).filter((message) => message?.type === "assistant");
87
+ const finalNativeMessage = captureFinalAssistantResult && !streaming
88
+ ? openCodeLastAssistantResult(rows, { readError: projection.readError }).message : null;
89
+ for (const [index, message] of rows.entries()) {
90
+ failure ||= projection.readError(message);
91
+ providerApiFailure ||= openCodeProviderApiFailure(message.error);
92
+ if (message.summary === true) continue;
93
+ const assistantText = assistantMessageText(message);
94
+ const messageId = assistantText ? projection.messageId(message.id, "assistant") : "";
95
+ const outputId = assistantText ? nativeOutputId(message.id, "assistant") : "";
96
+ const inFlight = streaming && index === rows.length - 1;
97
+ const reasoning = (message.content || []).filter((part) => part.type === "reasoning" && normalizeNullableText(part.text))
98
+ .map((part) => ({
99
+ messageId: message.id,
100
+ partId: part.id,
101
+ text: part.text,
102
+ createdAt: message.time?.created,
103
+ complete: Boolean(part.time?.end)
104
+ }));
105
+ await projection.reasoning(reasoning, {
106
+ complete: !inFlight || Boolean(message.time?.completed || message.finish),
107
+ flush: !streaming || Boolean(assistantText && !inFlight)
108
+ });
109
+ if (streaming && assistantText && !inFlight) {
110
+ // A completed round persists as soon as the next one starts, so
111
+ // superseded narration never evaporates from the transcript.
112
+ const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
113
+ messageId,
114
+ ...(outputId ? { outputId } : {}),
115
+ text: assistantText
116
+ });
117
+ projection.store.completeConversationStreamMessage(sessionId, messageId);
118
+ await projection.publishTurn(turn);
119
+ continue;
120
+ }
121
+ if (streaming && assistantText && inFlight) {
122
+ const conversationStream = projection.store.updateConversationStream(sessionId, {
123
+ turnId: inputMessageId,
124
+ messageId,
125
+ ...(outputId ? { outputId } : {}),
126
+ text: assistantText
127
+ });
128
+ if (conversationStream) {
129
+ await projection.publishStream(conversationStream);
130
+ }
131
+ continue;
132
+ }
133
+ if (assistantText && !streaming) {
134
+ const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
135
+ messageId,
136
+ ...(outputId ? { outputId } : {}),
137
+ text: assistantText
138
+ });
139
+ projection.store.completeConversationStreamMessage(sessionId, messageId);
140
+ await projection.publishTurn(turn);
141
+ if (message === finalNativeMessage && !projection.readError(message) &&
142
+ (message.time?.completed || message.finish) && turn) {
143
+ finalAssistantResult = { inputMessageId: normalizeNullableText(inputMessageId), itemId: normalizeNullableText(message.id),
144
+ text: assistantText, conversationTurn: turn };
145
+ }
146
+ }
147
+ }
148
+ return { failure, providerApiFailure, ...(captureFinalAssistantResult ? { finalAssistantResult } : {}) };
149
+ }
150
+
151
+ /**
152
+ * Starts and retires the monitor that observes a session's native turn, and
153
+ * reads the final-answer evidence it retains.
154
+ * @param {{
155
+ * processes: Map<string, object>,
156
+ * monitors: Map<string, Promise<object>>,
157
+ * turns: Map<string, object>,
158
+ * processOwner: import("./openCodeProcessOwner.js").OpenCodeProcessOwner
159
+ * }} options
160
+ */
161
+ export function createOpenCodeTurnMonitor({ processes, monitors, turns, processOwner }) {
162
+ // Evidence belongs to the exact retained native turn. This read never creates,
163
+ // observes, resumes or projects a native conversation.
164
+ function readFinalAssistantResult(key, threadId = "", turnId = "") {
165
+ const turn = turns.get(key);
166
+ const result = turn?.finalAssistantResult;
167
+ const target = turn?.finalAssistantResultTarget;
168
+ if (!target || processes.get(key) !== target || target.abortController.signal.aborted ||
169
+ !turn.finalAssistantResultProcess || processOwner.current !== turn.finalAssistantResultProcess ||
170
+ target.upstreamSessionId !== turn.threadId) return null;
171
+ if (!normalizeNullableText(threadId) || !normalizeNullableText(turnId) || !result || turn.active || turn.state !== "completed" ||
172
+ turn.error || turn.observationError || turn.interruptRequested || turn.interruptAcknowledged ||
173
+ turn.threadId !== normalizeNullableText(threadId) || turn.id !== normalizeNullableText(turnId) ||
174
+ result.threadId !== normalizeNullableText(threadId) || result.turnId !== normalizeNullableText(turnId) ||
175
+ result.inputMessageId !== normalizeNullableText(turn.inputMessageId) || !result.conversationTurn) return null;
176
+ return structuredClone(result);
177
+ }
178
+
179
+ function beginMessageMonitor(key, target, admitted, options, projection) {
180
+ const start = () => {
181
+ target = { ...target };
182
+ const existing = monitors.get(key);
183
+ if (existing) {
184
+ return existing;
185
+ }
186
+ const metadata = projection.prepare(admitted, options);
187
+ const eventStartedAt = Number(admitted.eventStartedAt) || Date.now();
188
+ const startedAt = normalizeNullableText(admitted.startedAt) || new Date(eventStartedAt).toISOString();
189
+ const turn = {
190
+ abortController: new AbortController(),
191
+ admission: options.admission,
192
+ active: true,
193
+ error: "",
194
+ eventStartedAt,
195
+ id: normalizeNullableText(admitted.id),
196
+ inputMessageId: normalizeNullableText(admitted.id),
197
+ startedAt,
198
+ state: options.admission ? "starting" : "active",
199
+ ...metadata.fields,
200
+ threadId: target.upstreamSessionId,
201
+ updatedAt: startedAt
202
+ };
203
+ const monitor = beginMonitor(key, target, turn, projection.create(target, turn, metadata, options));
204
+ void monitor.catch(error => projection.onError(error));
205
+ return monitor;
206
+ };
207
+ // readable-check: allow method-probe - optional operation: a monitor that runs inside its request's context supplies runInContext
208
+ if (typeof projection.runInContext !== "function") return start();
209
+ let monitor, startFailure;
210
+ // Context entry invokes this callback synchronously. Do not return the
211
+ // monitor to an async scope wrapper: retain its identity and error owner.
212
+ const scope = projection.runInContext(() => {
213
+ try { monitor = start(); }
214
+ catch (error) { startFailure = { error }; }
215
+ });
216
+ void Promise.resolve(scope).catch(error => projection.onError(error));
217
+ if (startFailure) throw startFailure.error;
218
+ return monitor;
219
+ }
220
+
221
+ // A turn's failure and whether it is a credential failure. A removed
222
+ // connection's models are gone from its reloaded directory.
223
+ function turnFailure(failure) {
224
+ const providerId = /\bModel not found: ([^/\s]+)\//u.exec(failure)?.[1];
225
+ if (providerId && processOwner.current && !processOwner.current.connections.has(providerId)) {
226
+ return { failure: `${providerId} isn't connected. Connect it again, then send your message again.`, credentialFailure: true };
227
+ }
228
+ return { failure, credentialFailure: openCodeCredentialFailure(failure) };
229
+ }
230
+
231
+ function beginMonitor(key, target, turn, {
232
+ observe, observation, eventReady, readiness, finalResponse,
233
+ writeRun, projectMessages, completeResult, onRetired
234
+ }) {
235
+ delete turn.finalAssistantResult;
236
+ delete turn.finalAssistantResultTarget;
237
+ delete turn.finalAssistantResultProcess;
238
+ const registeredTarget = processes.get(key);
239
+ const finalAssistantResultTarget = registeredTarget &&
240
+ registeredTarget.abortController === target.abortController &&
241
+ registeredTarget.server === target.server &&
242
+ registeredTarget.upstreamSessionId === target.upstreamSessionId ? registeredTarget : null;
243
+ const finalAssistantResultProcess = finalAssistantResultTarget ? processOwner.current : null;
244
+ turns.set(key, turn);
245
+ const releaseDirectory = processOwner.holdDirectory(target.workdir);
246
+ const signal = AbortSignal.any([target.abortController.signal, turn.abortController.signal]);
247
+ const monitor = Promise.resolve().then(async () => {
248
+ const events = observation
249
+ ? observePreparedEvents(target, turn, turn.abortController, signal, observation())
250
+ : observe(turn, signal);
251
+ if (eventReady) {
252
+ void events.waitUntilReady(readiness).then(eventReady.resolve, eventReady.reject);
253
+ }
254
+ let finalState = "completed";
255
+ let failure = "";
256
+ let finalAssistantResult = null;
257
+ let credentialFailure = false;
258
+ let providerApiFailure = false;
259
+ try {
260
+ await turn.admission?.promise;
261
+ signal.throwIfAborted();
262
+ turn.state = "active";
263
+ await writeRun(turn, "active");
264
+ for (;;) {
265
+ const completion = await waitForOpenCodeFinalResponse(
266
+ target.server.client,
267
+ target.upstreamSessionId,
268
+ turn,
269
+ {
270
+ ...finalResponse,
271
+ // Preserve the reasoning-only completion before the native recovery
272
+ // input replaces the projection boundary. Replayed writes are no-ops.
273
+ beforeRecovery: async completion => {
274
+ await projectMessages(completion.messages, { inputMessageId: completion.inputMessageId });
275
+ const { beforeRecovery = recoverWithoutProjection } = finalResponse;
276
+ await beforeRecovery(completion);
277
+ },
278
+ onMessages: async (messages, inputMessageId) => {
279
+ const latest = openCodeLastAssistantResult(messages, { readError: finalResponse.readError }).message;
280
+ const phase = latest?.summary === true && Number(latest.time?.created) >= turn.eventStartedAt &&
281
+ !latest.time?.completed && !latest.finish && !latest.error ? "compacting" : "";
282
+ if (turn.active && !signal.aborted && normalizeNullableText(turn.phase) !== phase) {
283
+ turn.phase = phase;
284
+ turn.updatedAt = new Date().toISOString();
285
+ await writeRun(turn, turn.state);
286
+ }
287
+ return projectMessages(messages, { inputMessageId, streaming: true });
288
+ },
289
+ readFailure: events.readFailure,
290
+ signal
291
+ }
292
+ );
293
+ const admission = turn.admission;
294
+ await admission?.promise;
295
+ signal.throwIfAborted();
296
+ if (admission !== turn.admission || completion.inputMessageId !== normalizeNullableText(turn.inputMessageId)) {
297
+ continue;
298
+ }
299
+ const projection = await projectMessages(completion.messages, {
300
+ inputMessageId: completion.inputMessageId,
301
+ captureFinalAssistantResult: true
302
+ });
303
+ await turn.admission?.promise;
304
+ signal.throwIfAborted();
305
+ if (admission !== turn.admission || completion.inputMessageId !== normalizeNullableText(turn.inputMessageId)) {
306
+ continue;
307
+ }
308
+ failure = projection.failure || normalizeNullableText(events.readFailure()?.message);
309
+ providerApiFailure = projection.providerApiFailure || openCodeProviderApiFailure(events.readFailure());
310
+ if (!failure && !turn.interruptRequested && !normalizeNullableText(completion.result?.text)) {
311
+ failure = "OpenCode finished without a user-facing final response. Please send your message again.";
312
+ finalState = "failed";
313
+ } else if (failure) {
314
+ ({ failure, credentialFailure } = turnFailure(failure));
315
+ finalState = "failed";
316
+ } else if (turn.interruptRequested) {
317
+ finalState = "interrupted";
318
+ }
319
+ const published = projection.finalAssistantResult;
320
+ if (finalState === "completed" && !failure && published &&
321
+ published.inputMessageId === completion.inputMessageId &&
322
+ published.itemId === completion.result?.turnId &&
323
+ normalizeNullableText(published.text) === normalizeNullableText(completion.result?.text)) {
324
+ finalAssistantResult = { ...published, threadId: turn.threadId, turnId: turn.id };
325
+ }
326
+ break;
327
+ }
328
+ } catch (error) {
329
+ if (turn.interruptAcknowledged) {
330
+ finalState = "interrupted";
331
+ } else {
332
+ const cause = signal.aborted ? signal.reason : error;
333
+ ({ failure, credentialFailure } = turnFailure(normalizeNullableText(cause?.message) || "OpenCode turn failed."));
334
+ providerApiFailure = openCodeProviderApiFailure(cause);
335
+ finalState = target.abortController.signal.aborted
336
+ ? "cancelled"
337
+ : "failed";
338
+ if (!target.abortController.signal.aborted) {
339
+ turn.observationError = failure;
340
+ try {
341
+ await writeRun(turn, turn.state, failure);
342
+ } catch {
343
+ // A failed store must not prevent stopping native work.
344
+ }
345
+ try {
346
+ await processOwner.stopUnobservedSession(target, turn.threadId);
347
+ } catch (stopError) {
348
+ failure = `${failure} ${stopError.message}`;
349
+ finalState = "active";
350
+ }
351
+ }
352
+ }
353
+ } finally {
354
+ // Claim retirement before awaited cleanup/checkpointing. A new Send
355
+ // joins this monitor's cleanup instead of steering an unobserved turn.
356
+ turn.active = finalState === "active";
357
+ await events.close();
358
+ if (credentialFailure || finalState === "failed") {
359
+ failure = await completeResult(turn, { credentialFailure, providerApiFailure, finalState, failure });
360
+ }
361
+ turn.error = failure;
362
+ turn.state = finalState;
363
+ turn.finalAssistantResult = finalState === "completed" && !failure ? finalAssistantResult : null;
364
+ turn.finalAssistantResultTarget = turn.finalAssistantResult ? finalAssistantResultTarget : null;
365
+ turn.finalAssistantResultProcess = turn.finalAssistantResult ? finalAssistantResultProcess : null;
366
+ turn.updatedAt = new Date().toISOString();
367
+ await writeRun(turn, finalState, failure).catch(() => null);
368
+ }
369
+ return openCodeTurnSnapshot(turn, target.upstreamSessionId);
370
+ }).finally(() => {
371
+ if (monitors.get(key) === monitor) {
372
+ monitors.delete(key);
373
+ }
374
+ releaseDirectory();
375
+ onRetired();
376
+ });
377
+ monitors.set(key, monitor);
378
+ return monitor;
379
+ }
380
+
381
+ return Object.freeze({
382
+ beginMessageMonitor,
383
+ beginMonitor,
384
+ readFinalAssistantResult
385
+ });
386
+ }
387
+
388
+ /** @typedef {ReturnType<typeof createOpenCodeTurnMonitor>} OpenCodeTurnMonitor */