@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
@@ -1,14 +1,20 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
1
2
  import { setTimeout as delay } from "node:timers/promises";
2
3
  import { openCodeAssistantMessageText as assistantMessageText } from "./openCodeClient.js";
3
- import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
4
5
  import { normalizeNullableText } from "./normalize.js";
5
6
 
6
- const text = normalizeNullableText;
7
7
  export const OPENCODE_INTERRUPT_TIMEOUT_MS = 5_000;
8
8
  const OPENCODE_EVENT_READY_TIMEOUT_MS = 120_000;
9
9
 
10
+ /** A final-answer recovery whose caller keeps no projection of the reasoning-only completion. */
11
+ export function recoverWithoutProjection() {}
12
+
13
+ /** A detached turn whose caller keeps no state after observation closes. */
14
+ function ignoreObservationClose() {}
15
+
10
16
  export function openCodeDetachedPrompt(input = {}) {
11
- const prompt = text(input.prompt || input.message);
17
+ const prompt = normalizeNullableText(input.prompt || input.message);
12
18
  if (!input.outputSchema) {
13
19
  return prompt;
14
20
  }
@@ -26,45 +32,44 @@ export function openCodeStructuredOutput(value = "") {
26
32
  return match ? match[1].trim() : original;
27
33
  }
28
34
 
29
- const record = value => isRecord(value) ? value : {};
30
35
 
31
36
  function openCodeEventSummary(event = {}) {
32
- const payload = record(event.data);
33
- const properties = record(payload.properties);
34
- const data = Object.keys(properties).length ? properties : record(payload.data);
35
- const info = record(data.info);
36
- const part = record(data.part);
37
- const model = record(info.model || data.model);
37
+ const payload = normalizeObject(event.data);
38
+ const properties = normalizeObject(payload.properties);
39
+ const data = Object.keys(properties).length ? properties : normalizeObject(payload.data);
40
+ const info = normalizeObject(data.info);
41
+ const part = normalizeObject(data.part);
42
+ const model = normalizeObject(info.model || data.model);
38
43
  return {
39
- agent: text(info.agent || data.agent),
44
+ agent: normalizeNullableText(info.agent || data.agent),
40
45
  at: Number(data.timestamp || part.time?.start || part.time?.created) || Date.now(),
41
- eventId: text(event.id || payload.id || part.id),
42
- messageId: text(part.messageID || data.messageID || info.id),
43
- modelId: text(model.id || model.modelID || info.modelID),
44
- modelProviderId: text(model.providerID || info.providerID),
45
- partId: text(part.id || data.partID),
46
+ eventId: normalizeNullableText(event.id || payload.id || part.id),
47
+ messageId: normalizeNullableText(part.messageID || data.messageID || info.id),
48
+ modelId: normalizeNullableText(model.id || model.modelID || info.modelID),
49
+ modelProviderId: normalizeNullableText(model.providerID || info.providerID),
50
+ partId: normalizeNullableText(part.id || data.partID),
46
51
  ...(part.type === "text" && typeof part.text === "string" ? { textSnapshot: part.text } : {}),
47
52
  ...(payload.type === "message.part.delta" && data.field === "text" && typeof data.delta === "string"
48
53
  ? { textDelta: data.delta } : {}),
49
- partType: text(part.type) || (
50
- text(payload.type) === "session.next.reasoning.ended"
54
+ partType: normalizeNullableText(part.type) || (
55
+ normalizeNullableText(payload.type) === "session.next.reasoning.ended"
51
56
  ? "reasoning"
52
- : text(payload.type) === "session.next.text.ended"
57
+ : normalizeNullableText(payload.type) === "session.next.text.ended"
53
58
  ? "text"
54
59
  : ""
55
60
  ),
56
- text: ["reasoning", "text"].includes(text(part.type))
57
- ? text(part.text || data.delta).slice(0, 32_000)
58
- : ["session.next.text.ended", "session.next.reasoning.ended"].includes(text(payload.type))
59
- ? text(data.text).slice(0, 32_000)
61
+ text: ["reasoning", "text"].includes(normalizeNullableText(part.type))
62
+ ? normalizeNullableText(part.text || data.delta).slice(0, 32_000)
63
+ : ["session.next.text.ended", "session.next.reasoning.ended"].includes(normalizeNullableText(payload.type))
64
+ ? normalizeNullableText(data.text).slice(0, 32_000)
60
65
  : "",
61
- tool: text(part.tool || data.tool),
62
- type: text(payload.type || event.event)
66
+ tool: normalizeNullableText(part.tool || data.tool),
67
+ type: normalizeNullableText(payload.type || event.event)
63
68
  };
64
69
  }
65
70
 
66
71
  async function consumeOpenCodeEvents(client, conversationId, {
67
- eventStartedAt = null, onEvent = null, onError = null, onReady = null,
72
+ eventStartedAt = null, onEvent = null, onError = null, onReady,
68
73
  readError = openCodeMessageError, signal
69
74
  } = {}) {
70
75
  for await (const event of client.events(conversationId, { onReady, signal })) {
@@ -73,23 +78,21 @@ async function consumeOpenCodeEvents(client, conversationId, {
73
78
  const current = eventStartedAt === null || Number(summary.at) >= Number(eventStartedAt);
74
79
  if (
75
80
  current && summary.type === "session.error" &&
76
- text(event.data?.properties?.sessionID) === conversationId &&
81
+ normalizeNullableText(event.data?.properties?.sessionID) === conversationId &&
77
82
  typeof onError === "function"
78
83
  ) {
79
- const failure = record(event.data?.properties?.error);
84
+ const failure = normalizeObject(event.data?.properties?.error);
80
85
  onError(Object.assign(new Error(readError({ error: failure }) || "OpenCode turn failed."), {
81
- name: text(failure.name) || "Error"
86
+ name: normalizeNullableText(failure.name) || "Error"
82
87
  }));
83
88
  }
84
89
  if (summary.type && current && typeof onEvent === "function") {
85
90
  await onEvent(summary, event);
86
91
  }
87
92
  }
88
- const error = new Error("OpenCode's event connection ended before observation was closed.");
89
- error.code = "assistant_opencode_observation_lost";
90
- error.details = {};
91
- error.statusCode = 503;
92
- throw error;
93
+ throw new AppError(503, "OpenCode's event connection ended before observation was closed.", {
94
+ code: "assistant_opencode_observation_lost", details: {}
95
+ });
93
96
  }
94
97
 
95
98
  export function observeOpenCodeEvents(client, conversationId, {
@@ -115,9 +118,9 @@ export function observeOpenCodeEvents(client, conversationId, {
115
118
  completion: events,
116
119
  readFailure: () => eventFailure,
117
120
  waitUntilReady({ timeoutMs = OPENCODE_EVENT_READY_TIMEOUT_MS, timeoutError } = {}) {
118
- const timeout = setTimeout(() => eventReady.reject(timeoutError || Object.assign(
119
- new Error("OpenCode's event connection did not become ready."),
120
- { code: "assistant_opencode_events_timeout", details: {}, statusCode: 504 }
121
+ const timeout = setTimeout(() => eventReady.reject(timeoutError || new AppError(504,
122
+ "OpenCode's event connection did not become ready.",
123
+ { code: "assistant_opencode_events_timeout", details: {} }
121
124
  )), timeoutMs);
122
125
  void eventReady.promise.then(() => clearTimeout(timeout), () => clearTimeout(timeout));
123
126
  return eventReady.promise;
@@ -129,8 +132,8 @@ export function observeOpenCodeEvents(client, conversationId, {
129
132
  };
130
133
  }
131
134
 
132
- export function openCodeMessageError(message = {}) {
133
- return text(message?.error?.message || message?.error?.data?.message || message?.error?.name || message?.error);
135
+ export function openCodeMessageError(message) {
136
+ return normalizeNullableText(message?.error?.message || message?.error?.data?.message || message?.error?.name || message?.error);
134
137
  }
135
138
 
136
139
  export async function inspectOpenCodeMessageAdmission(client, conversationId, inputMessageId, {
@@ -146,7 +149,7 @@ export async function inspectOpenCodeMessageAdmission(client, conversationId, in
146
149
  const messages = await readOpenCodeMessagesUntil(client, conversationId, hasMessage(inputMessageId), { signal: readSignal });
147
150
  if (wait) signal.throwIfAborted();
148
151
  const accepted = openCodeMessageRows(messages).some((message) =>
149
- message.type === "user" && text(message.id) === inputMessageId);
152
+ message.type === "user" && normalizeNullableText(message.id) === inputMessageId);
150
153
  if (accepted || !wait) return { accepted, messages };
151
154
  } catch {
152
155
  if (!wait) return null;
@@ -223,7 +226,7 @@ export async function readOpenCodeMessagesUntil(client, conversationId, found, {
223
226
  }
224
227
  }
225
228
 
226
- const hasMessage = id => rows => rows.some(message => text(message?.id) === text(id));
229
+ const hasMessage = id => rows => rows.some(message => normalizeNullableText(message?.id) === normalizeNullableText(id));
227
230
 
228
231
  export function openCodeMessageRows(value = null) {
229
232
  const rows = Array.isArray(value) ? value : Array.isArray(value?.data) ? value.data : [];
@@ -239,7 +242,7 @@ export function openCodeMessageRows(value = null) {
239
242
  export function openCodePersistentConversationMessages(rows, conversationMessageId) {
240
243
  return rows.filter((row) => row.type === "assistant").flatMap((row) => {
241
244
  const complete = Boolean(row.time?.completed || row.finish || row.error);
242
- const reasoning = (row.content || []).filter((part) => part.type === "reasoning" && text(part.text))
245
+ const reasoning = (row.content || []).filter((part) => part.type === "reasoning" && normalizeNullableText(part.text))
243
246
  .map((part, index) => ({ id: conversationMessageId(row.id, part.id || index, "reasoning"), role: "thinking", text: part.text, complete }));
244
247
  const answer = assistantMessageText(row);
245
248
  return [...reasoning, ...(answer ? [{ id: conversationMessageId(row.id, "assistant"), role: "assistant", text: answer, complete }] : [])];
@@ -278,13 +281,13 @@ export async function projectOpenCodeConversationMessages(messages, expectedId,
278
281
  }
279
282
  }
280
283
  const result = openCodeLastAssistantResult(rows);
281
- return { failure: result.error, providerApiFailure: text(result.message?.error?.name) === "APIError" };
284
+ return { failure: result.error, providerApiFailure: normalizeNullableText(result.message?.error?.name) === "APIError" };
282
285
  }
283
286
 
284
287
  // Native automatic compaction and its continuation belong to the admitted turn.
285
288
  export function openCodeRowsForInput(value = null, inputMessageId = "") {
286
289
  const rows = openCodeMessageRows(value);
287
- const index = rows.findIndex((message) => text(message?.id) === text(inputMessageId));
290
+ const index = rows.findIndex((message) => normalizeNullableText(message?.id) === normalizeNullableText(inputMessageId));
288
291
  if (index < 0) {
289
292
  return [];
290
293
  }
@@ -309,7 +312,7 @@ export function openCodeRowsForInput(value = null, inputMessageId = "") {
309
312
  function openCodeMessageResultForInput(value = null, inputMessageId = "", { readError = openCodeMessageError } = {}) {
310
313
  const rows = openCodeRowsForInput(value, inputMessageId);
311
314
  if (!rows.length && !openCodeMessageRows(value).some((message) => (
312
- text(message?.id) === text(inputMessageId)
315
+ normalizeNullableText(message?.id) === normalizeNullableText(inputMessageId)
313
316
  ))) {
314
317
  return null;
315
318
  }
@@ -323,7 +326,7 @@ function openCodeMessageResultForInput(value = null, inputMessageId = "", { read
323
326
  (result.message?.time?.completed || result.message?.finish))),
324
327
  error: result.error,
325
328
  text: result.text,
326
- turnId: text(result.message?.id)
329
+ turnId: normalizeNullableText(result.message?.id)
327
330
  };
328
331
  }
329
332
 
@@ -349,7 +352,7 @@ function latestOpenCodeMessageResult(value, readError) {
349
352
  (result.message.time?.completed || result.message.finish))),
350
353
  error: result.error,
351
354
  text: result.text,
352
- turnId: text(result.message.id)
355
+ turnId: normalizeNullableText(result.message.id)
353
356
  };
354
357
  }
355
358
 
@@ -361,18 +364,18 @@ export async function waitForOpenCodeMessages(client, conversationId = "", input
361
364
  } = {}) {
362
365
  const resolveInputMessageId = typeof inputMessageId === "function"
363
366
  ? inputMessageId
364
- : () => text(inputMessageId);
367
+ : () => normalizeNullableText(inputMessageId);
365
368
  let completedInputMessageId = null;
366
369
  while (true) {
367
370
  signal?.throwIfAborted();
368
- const expectedInputMessageId = text(resolveInputMessageId());
371
+ const expectedInputMessageId = normalizeNullableText(resolveInputMessageId());
369
372
  const messages = await readOpenCodeMessagesUntil(client, conversationId,
370
373
  expectedInputMessageId ? hasMessage(expectedInputMessageId) : () => true, { signal });
371
- if (expectedInputMessageId !== text(resolveInputMessageId())) continue;
374
+ if (expectedInputMessageId !== normalizeNullableText(resolveInputMessageId())) continue;
372
375
  if (typeof onMessages === "function") {
373
376
  await onMessages(messages, expectedInputMessageId);
374
377
  }
375
- if (expectedInputMessageId !== text(resolveInputMessageId())) continue;
378
+ if (expectedInputMessageId !== normalizeNullableText(resolveInputMessageId())) continue;
376
379
  const result = expectedInputMessageId
377
380
  ? openCodeMessageResultForInput(messages, expectedInputMessageId, { readError })
378
381
  : latestOpenCodeMessageResult(messages, readError);
@@ -383,7 +386,7 @@ export async function waitForOpenCodeMessages(client, conversationId = "", input
383
386
  // An assistant message can finish with tool calls while the native turn
384
387
  // continues. Only an idle session acknowledges the final answer.
385
388
  if (result?.complete && (await client.sessionStatus(conversationId, { signal })).type === "idle" &&
386
- expectedInputMessageId === text(resolveInputMessageId())) {
389
+ expectedInputMessageId === normalizeNullableText(resolveInputMessageId())) {
387
390
  if (completedInputMessageId === expectedInputMessageId) {
388
391
  return { messages, result, inputMessageId: expectedInputMessageId };
389
392
  }
@@ -396,7 +399,7 @@ export async function waitForOpenCodeMessages(client, conversationId = "", input
396
399
  }
397
400
 
398
401
  export async function waitForOpenCodeFinalResponse(client, conversationId, turn, {
399
- agent, model, recoveryMessageId, beforeRecovery = null, ...options
402
+ agent, model, recoveryMessageId, beforeRecovery = recoverWithoutProjection, ...options
400
403
  } = {}) {
401
404
  const waitForCompletion = async () => {
402
405
  for (;;) {
@@ -408,7 +411,7 @@ export async function waitForOpenCodeFinalResponse(client, conversationId, turn,
408
411
  const admission = turn.admission;
409
412
  await admission?.promise;
410
413
  options.signal?.throwIfAborted();
411
- if (admission !== turn.admission || completion.inputMessageId !== text(turn.inputMessageId)) {
414
+ if (admission !== turn.admission || completion.inputMessageId !== normalizeNullableText(turn.inputMessageId)) {
412
415
  continue;
413
416
  }
414
417
  return completion;
@@ -418,12 +421,12 @@ export async function waitForOpenCodeFinalResponse(client, conversationId, turn,
418
421
  const completion = await waitForCompletion();
419
422
  // Work around https://github.com/anomalyco/opencode/issues/37073. Some
420
423
  // reasoning models complete successfully without a user-facing text part.
421
- if (turn.interruptRequested || completion.result?.error || text(completion.result?.text)) return completion;
422
- await beforeRecovery?.(completion);
424
+ if (turn.interruptRequested || completion.result?.error || normalizeNullableText(completion.result?.text)) return completion;
425
+ await beforeRecovery(completion);
423
426
  const admission = turn.admission;
424
427
  await admission?.promise;
425
428
  options.signal?.throwIfAborted();
426
- if (admission !== turn.admission || completion.inputMessageId !== text(turn.inputMessageId)) {
429
+ if (admission !== turn.admission || completion.inputMessageId !== normalizeNullableText(turn.inputMessageId)) {
427
430
  continue;
428
431
  }
429
432
  // The async native prompt ACK does not acknowledge user-row creation.
@@ -437,7 +440,7 @@ export async function waitForOpenCodeFinalResponse(client, conversationId, turn,
437
440
  signal: receiptSignal, wait: true, readFailure: options.readFailure
438
441
  }).then(observation => {
439
442
  const message = openCodeMessageRows(observation.messages).find(message =>
440
- message.type === "user" && text(message.id) === recoveryMessageId);
443
+ message.type === "user" && normalizeNullableText(message.id) === recoveryMessageId);
441
444
  recoveryAdmission.resolve(message);
442
445
  return message;
443
446
  }, error => {
@@ -458,8 +461,8 @@ export async function waitForOpenCodeFinalResponse(client, conversationId, turn,
458
461
  }, { signal: options.signal });
459
462
  await receipt;
460
463
  options.signal?.throwIfAborted();
461
- if (admission === turn.admission && completion.inputMessageId === text(turn.inputMessageId)) {
462
- turn.inputMessageId = text(admitted?.id) || recoveryMessageId;
464
+ if (admission === turn.admission && completion.inputMessageId === normalizeNullableText(turn.inputMessageId)) {
465
+ turn.inputMessageId = normalizeNullableText(admitted?.id) || recoveryMessageId;
463
466
  turn.updatedAt = new Date().toISOString();
464
467
  }
465
468
  } catch (error) {
@@ -472,10 +475,13 @@ export async function waitForOpenCodeFinalResponse(client, conversationId, turn,
472
475
  }
473
476
  }
474
477
 
475
- // Production detached-turn lifetime. Applications project the completed native
476
- // result before observation is closed, then publish/checkpoint their own state.
477
- export async function observeOpenCodeTurnCompletion(runtime, target, conversationId, turn, {
478
- events, signal, timeoutMs, finalResponse, projectResult = value => value, afterClose, ...waitOptions
478
+ /**
479
+ * Production detached-turn lifetime. Applications project the completed native
480
+ * result before observation is closed, then publish/checkpoint their own state.
481
+ * @param {import("./openCodeProcessOwner.js").OpenCodeProcessOwner} processOwner
482
+ */
483
+ export async function observeOpenCodeTurnCompletion(processOwner, target, conversationId, turn, {
484
+ events, signal, timeoutMs, finalResponse, projectResult = value => value, afterClose = ignoreObservationClose, ...waitOptions
479
485
  } = {}) {
480
486
  const turnAbort = turn.abortController;
481
487
  let stopped = true;
@@ -494,7 +500,7 @@ export async function observeOpenCodeTurnCompletion(runtime, target, conversatio
494
500
  if (turn.interrupted) return { interrupted: true };
495
501
  if (!target.abortController.signal.aborted) {
496
502
  try {
497
- await runtime.stopUnobservedSession(target, conversationId);
503
+ await processOwner.stopUnobservedSession(target, conversationId);
498
504
  } catch (stopError) {
499
505
  stopped = false;
500
506
  throw Object.assign(new Error(`${failure.message} ${stopError.message}`, { cause: failure }), { cleanupFailed: true });
@@ -507,7 +513,7 @@ export async function observeOpenCodeTurnCompletion(runtime, target, conversatio
507
513
  turn.active = !stopped;
508
514
  turn.mayBeRunning = !stopped;
509
515
  }
510
- await afterClose?.({ stopped, outcome });
516
+ await afterClose({ stopped, outcome });
511
517
  }
512
518
  }
513
519
 
@@ -518,10 +524,10 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
518
524
  completeResult, close
519
525
  } = {}) {
520
526
  const { inputMessageId, readError,
521
- createError = (reason, message, details) => Object.assign(new Error(message ||
522
- (reason === "unreadable" ? "The exact OpenCode turn did not produce a readable result." : "OpenCode did not finish the turn.")),
523
- { code: `assistant_opencode_turn_${reason}`, details,
524
- statusCode: ["thread_mismatch", "fresh_thread_required", "unrelated_history"].includes(reason) ? 409 : 502 })
527
+ createError = (reason, message, details) => new AppError(
528
+ ["thread_mismatch", "fresh_thread_required", "unrelated_history"].includes(reason) ? 409 : 502,
529
+ message || (reason === "unreadable" ? "The exact OpenCode turn did not produce a readable result." : "OpenCode did not finish the turn."),
530
+ { code: `assistant_opencode_turn_${reason}`, details })
525
531
  } = input;
526
532
  if (
527
533
  (expectedThreadId && expectedThreadId !== conversationId) ||
@@ -538,10 +544,10 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
538
544
  let existingMessages;
539
545
  if (requireFreshHistory) {
540
546
  existingMessages = openCodeMessageRows(await readOpenCodeMessagesUntil(client, conversationId, rows => rows.some(
541
- message => message?.type === "user" && text(message.id) !== inputMessageId
547
+ message => message?.type === "user" && normalizeNullableText(message.id) !== inputMessageId
542
548
  )));
543
549
  const unrelatedUserMessage = existingMessages.find((message) => (
544
- message?.type === "user" && text(message.id) !== inputMessageId
550
+ message?.type === "user" && normalizeNullableText(message.id) !== inputMessageId
545
551
  ));
546
552
  if (unrelatedUserMessage) {
547
553
  throw createError(
@@ -594,7 +600,7 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
594
600
  }
595
601
  throw createError(
596
602
  "failed",
597
- text(error?.message),
603
+ normalizeNullableText(error?.message),
598
604
  {
599
605
  inputAccepted,
600
606
  threadId: conversationId
@@ -628,7 +634,7 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
628
634
  reconciled: !admitted,
629
635
  text: result.text,
630
636
  threadId: conversationId,
631
- turnId: result.turnId || text(admitted?.id)
637
+ turnId: result.turnId || normalizeNullableText(admitted?.id)
632
638
  };
633
639
  if (completeResult) await completeResult(completion);
634
640
  const proof = close ? await close() : undefined;