@jskit-ai/assistant-core 0.2.7 → 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 +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,16 +1,15 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  import { Buffer } from "node:buffer";
3
3
  import path from "node:path";
4
- import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
5
5
  import { normalizeNullableText } from "./normalize.js";
6
- import { createNativeHistoryExport, readStoppedNativeDatabase, retireNativeConversation } from "./nativeHistoryExport.js";
7
6
 
8
7
  const OPENCODE_RESPONSE_LIMIT_BYTES = 2 * 1024 * 1024;
9
8
  const OPENCODE_CATALOG_LIMIT_BYTES = 32 * 1024 * 1024;
10
9
  const OPENCODE_EVENT_LIMIT_BYTES = 2 * 1024 * 1024;
11
10
 
12
- const text = normalizeNullableText;
13
- const record = value => isRecord(value) ? value : {};
11
+ /** An event reader whose caller does not wait for the stream to open. */
12
+ function reportNoReadiness() {}
14
13
 
15
14
  function openCodeServerError(message, {
16
15
  body = null,
@@ -69,7 +68,7 @@ function parsedJson(value = "") {
69
68
  }
70
69
 
71
70
  function responseErrorMessage(payload = null, fallback = "OpenCode rejected the request.") {
72
- return text(
71
+ return normalizeNullableText(
73
72
  payload?.message ||
74
73
  payload?.error?.message ||
75
74
  payload?.error ||
@@ -89,7 +88,7 @@ function queryString(values = {}) {
89
88
  }
90
89
 
91
90
  function sessionPath(sessionId = "", suffix = "") {
92
- const id = encodeURIComponent(text(sessionId));
91
+ const id = encodeURIComponent(normalizeNullableText(sessionId));
93
92
  if (!id) {
94
93
  throw new TypeError("OpenCode session requests require a session id.");
95
94
  }
@@ -97,7 +96,7 @@ function sessionPath(sessionId = "", suffix = "") {
97
96
  }
98
97
 
99
98
  function stableSessionPath(sessionId = "", suffix = "") {
100
- const id = encodeURIComponent(text(sessionId));
99
+ const id = encodeURIComponent(normalizeNullableText(sessionId));
101
100
  if (!id) {
102
101
  throw new TypeError("OpenCode session requests require a session id.");
103
102
  }
@@ -114,58 +113,13 @@ function normalizedMessageRows(value = null) {
114
113
  return {
115
114
  ...info,
116
115
  content: Array.isArray(message.parts) ? message.parts : [],
117
- type: text(info.role)
116
+ type: normalizeNullableText(info.role)
118
117
  };
119
118
  });
120
119
  }
121
120
 
122
- /** Inspect stopped native storage without starting OpenCode or changing its files. */
123
- async function readOpenCodeConversationDatabase({ databasePath, conversationId, onMessage, signal } = {}) {
124
- if (!path.isAbsolute(databasePath || "") || !/^ses_[a-zA-Z0-9_]{1,256}$/u.test(conversationId || "") ||
125
- typeof onMessage !== "function") throw new TypeError("OpenCode inspection requires its exact database, conversation and message reader.");
126
- const unavailable = () => new Error("OpenCode native storage could not be inspected completely. Keep its writers stopped and inspect its original database before retrying.");
127
- try {
128
- return await readStoppedNativeDatabase(databasePath, async database => {
129
- const session = database.prepare("SELECT * FROM session WHERE id = ?").get(conversationId);
130
- if (!session || session.id !== conversationId || !path.isAbsolute(session.directory || "")) throw unavailable();
131
- if (database.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'session_input'").get() &&
132
- database.prepare("SELECT 1 FROM session_input WHERE session_id = ? AND promoted_seq IS NULL LIMIT 1").get(conversationId)) {
133
- throw new Error("OpenCode has an unpromoted native request. Inspect it before restoring continuity; no request was repeated.");
134
- }
135
- const output = createNativeHistoryExport(async ({ message }) => onMessage(normalizedMessageRows([message])[0]), { signal });
136
- const parts = database.prepare("SELECT id, message_id, session_id, data FROM part WHERE message_id = ? ORDER BY id");
137
- for (const row of database.prepare("SELECT id, session_id, data FROM message WHERE session_id = ? ORDER BY time_created, id").iterate(conversationId)) {
138
- signal?.throwIfAborted();
139
- if (!/^msg_[a-zA-Z0-9_]{1,256}$/u.test(row.id)) throw unavailable();
140
- let bytes = Buffer.byteLength(row.data);
141
- if (bytes > 64 * 1024 * 1024) throw unavailable();
142
- const data = JSON.parse(row.data);
143
- if (!data || typeof data !== "object" || Array.isArray(data) || !["user", "assistant"].includes(data.role)) throw unavailable();
144
- const content = [];
145
- for (const part of parts.iterate(row.id)) {
146
- signal?.throwIfAborted();
147
- bytes += Buffer.byteLength(part.data);
148
- if (bytes > 64 * 1024 * 1024 || part.session_id !== conversationId || part.message_id !== row.id ||
149
- !/^prt_[a-zA-Z0-9_]{1,256}$/u.test(part.id)) throw unavailable();
150
- const value = JSON.parse(part.data);
151
- if (!value || typeof value !== "object" || Array.isArray(value)) throw unavailable();
152
- content.push({ ...value, id: part.id, sessionID: part.session_id, messageID: part.message_id });
153
- }
154
- // Same original MessageV2 storage projection: native columns own IDs.
155
- await output.emit({ type: "message", message: { info: { ...data, id: row.id, sessionID: row.session_id }, parts: content }, text: [] });
156
- }
157
- return { session, ...output.complete() };
158
- }, { signal });
159
- } catch (error) {
160
- if (signal?.aborted) throw signal.reason;
161
- if (String(error?.message || "").startsWith("OpenCode ") ||
162
- String(error?.message || "").startsWith("Native storage ")) throw error;
163
- throw unavailable();
164
- }
165
- }
166
-
167
121
  function providerCapabilityMedia(value = null) {
168
- const source = record(value);
122
+ const source = normalizeObject(value);
169
123
  return Object.fromEntries([
170
124
  "audio",
171
125
  "image",
@@ -176,8 +130,8 @@ function providerCapabilityMedia(value = null) {
176
130
  }
177
131
 
178
132
  function sanitizedOpenCodeModel(model = {}, fallbackId = "") {
179
- const source = record(model);
180
- const id = text(fallbackId);
133
+ const source = normalizeObject(model);
134
+ const id = normalizeNullableText(fallbackId);
181
135
  const context = Number(source.limit?.context);
182
136
  const output = Number(source.limit?.output);
183
137
  return {
@@ -188,16 +142,16 @@ function sanitizedOpenCodeModel(model = {}, fallbackId = "") {
188
142
  reasoning: source.capabilities?.reasoning === true,
189
143
  toolcall: source.capabilities?.toolcall === true
190
144
  },
191
- family: text(source.family),
145
+ family: normalizeNullableText(source.family),
192
146
  id,
193
147
  limit: {
194
148
  ...(Number.isSafeInteger(context) && context > 0 ? { context } : {}),
195
149
  ...(Number.isSafeInteger(output) && output > 0 ? { output } : {})
196
150
  },
197
- name: text(source.name) || id,
198
- release_date: text(source.release_date),
199
- status: text(source.status),
200
- variants: Object.fromEntries(Object.keys(record(source.variants))
151
+ name: normalizeNullableText(source.name) || id,
152
+ release_date: normalizeNullableText(source.release_date),
153
+ status: normalizeNullableText(source.status),
154
+ variants: Object.fromEntries(Object.keys(normalizeObject(source.variants))
201
155
  .filter(Boolean)
202
156
  .map((variantId) => [variantId, {}]))
203
157
  };
@@ -213,17 +167,17 @@ function invalidOpenCodeCatalogError() {
213
167
  }
214
168
 
215
169
  function sanitizedOpenCodeProviderCatalog(value = null) {
216
- const source = record(value);
170
+ const source = normalizeObject(value);
217
171
  if (!Array.isArray(source.all) || source.all.length === 0) {
218
172
  throw invalidOpenCodeCatalogError();
219
173
  }
220
174
  const all = source.all.map((provider) => {
221
- const candidate = record(provider);
222
- const id = text(candidate.id);
175
+ const candidate = normalizeObject(provider);
176
+ const id = normalizeNullableText(candidate.id);
223
177
  const environmentNames = Array.isArray(candidate.env)
224
- ? candidate.env.map(text).filter(Boolean)
178
+ ? candidate.env.map(normalizeNullableText).filter(Boolean)
225
179
  : [];
226
- const models = record(candidate.models);
180
+ const models = normalizeObject(candidate.models);
227
181
  return {
228
182
  apiKeyCompatible: environmentNames.length === 1 || [
229
183
  "amazon-bedrock",
@@ -231,28 +185,28 @@ function sanitizedOpenCodeProviderCatalog(value = null) {
231
185
  ].includes(id),
232
186
  id,
233
187
  models: Object.fromEntries(Object.entries(models)
234
- .map(([modelId, model]) => [text(modelId), sanitizedOpenCodeModel(model, modelId)])
188
+ .map(([modelId, model]) => [normalizeNullableText(modelId), sanitizedOpenCodeModel(model, modelId)])
235
189
  .filter(([modelId, model]) => modelId && model.id)),
236
- name: text(candidate.name) || id
190
+ name: normalizeNullableText(candidate.name) || id
237
191
  };
238
192
  }).filter((provider) => provider.id);
239
193
  if (all.length === 0) {
240
194
  throw invalidOpenCodeCatalogError();
241
195
  }
242
- const defaults = record(source.default);
196
+ const defaults = normalizeObject(source.default);
243
197
  return {
244
198
  all,
245
199
  default: Object.fromEntries(Object.entries(defaults)
246
- .map(([providerId, modelId]) => [text(providerId), text(modelId)])
200
+ .map(([providerId, modelId]) => [normalizeNullableText(providerId), normalizeNullableText(modelId)])
247
201
  .filter(([providerId, modelId]) => providerId && modelId))
248
202
  };
249
203
  }
250
204
 
251
205
  function stablePromptBody(input = {}) {
252
- const model = input?.model && typeof input.model === "object" ? input.model : {};
253
- const modelID = text(model.modelID || model.id);
254
- const providerID = text(model.providerID);
255
- const prompt = typeof input?.prompt?.text === "string" ? input.prompt.text : "";
206
+ const model = input.model && typeof input.model === "object" ? input.model : {};
207
+ const modelID = normalizeNullableText(model.modelID || model.id);
208
+ const providerID = normalizeNullableText(model.providerID);
209
+ const prompt = typeof input.prompt?.text === "string" ? input.prompt.text : "";
256
210
  const content = (input.prompt?.content || []).map(part => {
257
211
  if (part.type === "text" && typeof part.text === "string") return { type: "text", text: part.text };
258
212
  if (part.type === "image" && part.image instanceof Uint8Array && ["image/png", "image/jpeg", "image/webp", "image/gif"].includes(part.mediaType)) {
@@ -264,8 +218,8 @@ function stablePromptBody(input = {}) {
264
218
  throw new TypeError("OpenCode prompt requests require text or attachment content.");
265
219
  }
266
220
  return {
267
- ...(text(input.agent) ? { agent: text(input.agent) } : {}),
268
- ...(text(input.id) ? { messageID: text(input.id) } : {}),
221
+ ...(normalizeNullableText(input.agent) ? { agent: normalizeNullableText(input.agent) } : {}),
222
+ ...(normalizeNullableText(input.id) ? { messageID: normalizeNullableText(input.id) } : {}),
269
223
  ...(modelID && providerID ? { model: { modelID, providerID } } : {}),
270
224
  parts: [...(prompt ? [{ text: prompt, type: "text" }] : []), ...content, ...(input.attachments || [])
271
225
  .filter((attachment) => attachment.contentType?.startsWith("image/"))
@@ -273,7 +227,7 @@ function stablePromptBody(input = {}) {
273
227
  type: "file", mime: attachment.contentType, filename: attachment.fileName,
274
228
  url: pathToFileURL(attachment.path).href
275
229
  }))],
276
- ...(text(model.variant) ? { variant: text(model.variant) } : {})
230
+ ...(normalizeNullableText(model.variant) ? { variant: normalizeNullableText(model.variant) } : {})
277
231
  };
278
232
  }
279
233
 
@@ -282,7 +236,7 @@ function eventSessionId(value = null) {
282
236
  const properties = payload.properties && typeof payload.properties === "object"
283
237
  ? payload.properties
284
238
  : {};
285
- return text(
239
+ return normalizeNullableText(
286
240
  payload.sessionID ||
287
241
  properties.sessionID ||
288
242
  properties.info?.sessionID ||
@@ -298,10 +252,21 @@ function decodeOpenCodeEventData(value = "") {
298
252
  return parsedJson(first) ?? first;
299
253
  }
300
254
 
301
- // OpenCode loads one instance per directory and keeps it, with the credentials it
302
- // read, until that directory is disposed. Every client derived from one server
303
- // shares the record of the directories it has addressed; `defaultDirectory` is
304
- // the server's working directory, which requests without a directory reach.
255
+ /**
256
+ * OpenCode loads one instance per directory and keeps it, with the credentials it
257
+ * read, until that directory is disposed. Every client derived from one server
258
+ * shares the record of the directories it has addressed; `defaultDirectory` is
259
+ * the server's working directory, which requests without a directory reach.
260
+ * @param {object} [options]
261
+ * @param {boolean} [options.allowAttachmentDirectories]
262
+ * @param {string} [options.baseUrl]
263
+ * @param {string} [options.defaultDirectory]
264
+ * @param {string} [options.directory]
265
+ * @param {typeof fetch} [options.fetchImpl]
266
+ * @param {{ addressed: Map<string, number>, sequence: number }} [options.instances]
267
+ * @param {string} [options.password]
268
+ * @param {string} [options.username]
269
+ */
305
270
  function createOpenCodeServerClient({
306
271
  allowAttachmentDirectories = false,
307
272
  baseUrl = "",
@@ -316,14 +281,15 @@ function createOpenCodeServerClient({
316
281
  if (origin.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(origin.hostname)) {
317
282
  throw new TypeError("OpenCode bridge clients require a loopback HTTP server.");
318
283
  }
284
+ // readable-check: allow method-probe - input validation: the fetch implementation a host hands the OpenCode client
319
285
  if (typeof fetchImpl !== "function") {
320
286
  throw new TypeError("OpenCode bridge clients require fetch().");
321
287
  }
322
- const authorization = `Basic ${Buffer.from(`${text(username) || "opencode"}:${String(password)}`).toString("base64")}`;
323
- const scopedDirectory = text(directory);
288
+ const authorization = `Basic ${Buffer.from(`${normalizeNullableText(username) || "opencode"}:${String(password)}`).toString("base64")}`;
289
+ const scopedDirectory = normalizeNullableText(directory);
324
290
 
325
291
  function instanceDirectory(requestDirectory = scopedDirectory) {
326
- const value = text(requestDirectory) || text(defaultDirectory);
292
+ const value = normalizeNullableText(requestDirectory) || normalizeNullableText(defaultDirectory);
327
293
  return value ? path.resolve(value) : "";
328
294
  }
329
295
 
@@ -397,7 +363,7 @@ function createOpenCodeServerClient({
397
363
  return `/session/${encodeURIComponent(conversationId)}`;
398
364
  }
399
365
 
400
- async function *events(sessionId = "", { onReady = null, signal } = {}) {
366
+ async function *events(sessionId = "", { onReady = reportNoReadiness, signal } = {}) {
401
367
  const requestPath = "/event";
402
368
  addressInstance(requestPath, scopedDirectory);
403
369
  const response = await fetchImpl(new URL(requestPath, origin), {
@@ -415,7 +381,7 @@ function createOpenCodeServerClient({
415
381
  status: response.status
416
382
  });
417
383
  }
418
- onReady?.();
384
+ onReady();
419
385
  const decoder = new TextDecoder();
420
386
  const reader = response.body.getReader();
421
387
  let buffered = "";
@@ -442,7 +408,7 @@ function createOpenCodeServerClient({
442
408
  if (next.done) {
443
409
  const finalEvent = dispatch();
444
410
  const finalSessionId = eventSessionId(finalEvent?.data);
445
- if (finalEvent && (!finalSessionId || finalSessionId === text(sessionId))) {
411
+ if (finalEvent && (!finalSessionId || finalSessionId === normalizeNullableText(sessionId))) {
446
412
  yield finalEvent;
447
413
  }
448
414
  return;
@@ -455,7 +421,7 @@ function createOpenCodeServerClient({
455
421
  if (!line) {
456
422
  const value = dispatch();
457
423
  const valueSessionId = eventSessionId(value?.data);
458
- if (value && (!valueSessionId || valueSessionId === text(sessionId))) {
424
+ if (value && (!valueSessionId || valueSessionId === normalizeNullableText(sessionId))) {
459
425
  yield value;
460
426
  }
461
427
  } else if (!line.startsWith(":")) {
@@ -490,203 +456,253 @@ function createOpenCodeServerClient({
490
456
  }
491
457
  }
492
458
 
493
- return Object.freeze({
494
- async allowConversationAttachments(conversationId, attachments) {
495
- const route = storageConversationPath(conversationId);
496
- const { data: session } = await requestStorageResponse(route);
497
- if (session?.id !== conversationId) throw new Error("OpenCode returned another conversation.");
459
+ async function allowConversationAttachments(conversationId, attachments) {
460
+ const route = storageConversationPath(conversationId);
461
+ const { data: session } = await requestStorageResponse(route);
462
+ if (session?.id !== conversationId) throw new Error("OpenCode returned another conversation.");
463
+ const permission = [...(session.permission || [])];
464
+ const previousLength = permission.length;
465
+ for (const attachment of attachments) {
466
+ const pattern = path.join(path.dirname(attachment.path), "*").replaceAll("\\", "/");
467
+ const rule = permission.findLast((item) => item.permission === "external_directory" && item.pattern === pattern);
468
+ if (rule?.action !== "allow") permission.push({ permission: "external_directory", pattern, action: "allow" });
469
+ }
470
+ if (permission.length !== previousLength) {
471
+ await requestStorageResponse(route, { body: { permission } });
472
+ }
473
+ }
474
+
475
+ async function listConversationChildren(conversationId, { signal } = {}) {
476
+ const children = await storageInventory(`${storageConversationPath(conversationId)}/children`, { signal });
477
+ if (children.some((child) => child.parentID !== conversationId)) {
478
+ throw new Error("OpenCode returned an incomplete or invalid child inventory.");
479
+ }
480
+ return children;
481
+ }
482
+
483
+ async function listConversationsForDirectory(directory, { signal } = {}) {
484
+ if (!path.isAbsolute(directory || "")) throw new TypeError("OpenCode inventory requires an absolute native directory.");
485
+ // Use the global persisted inventory: the project-scoped /session
486
+ // listing loses its Git project identity once archived source is gone.
487
+ // Request one beyond our limit to detect a truncated inventory.
488
+ const rows = await storageInventory(`/experimental/session?${new URLSearchParams({ directory, limit: "1001" })}`, { signal });
489
+ if (rows.some((row) => typeof row.directory !== "string")) throw new Error("OpenCode inventory has no native directory.");
490
+ return rows.filter((row) => row.directory === directory);
491
+ }
492
+
493
+ async function readConversationStorage(conversationId, { signal } = {}) {
494
+ // The native storage record does not resolve a live source workspace.
495
+ const { data } = await requestStorageResponse(storageConversationPath(conversationId), { signal });
496
+ if (data?.id !== conversationId || !path.isAbsolute(data.directory || "")) {
497
+ throw new Error("OpenCode returned an invalid native conversation record.");
498
+ }
499
+ return data;
500
+ }
501
+
502
+ async function readConversationStoragePage(conversationId, { before = "", signal } = {}) {
503
+ const route = `${storageConversationPath(conversationId)}/message`;
504
+ if (typeof before !== "string" || before.length > 8192) throw new TypeError("Invalid OpenCode storage cursor.");
505
+ const query = new URLSearchParams({ limit: "1", ...(before ? { before } : {}) });
506
+ const { data, headers } = await requestStorageResponse(`${route}?${query}`, {
507
+ signal, maxBytes: 64 * 1024 * 1024
508
+ });
509
+ const nextCursor = headers.get("x-next-cursor");
510
+ if (!nextCursor && /rel="next"/u.test(headers.get("link") || "")) {
511
+ throw new Error("OpenCode omitted its native history continuation cursor.");
512
+ }
513
+ return { data, nextCursor };
514
+ }
515
+
516
+ async function agents({ directory = "", signal } = {}) {
517
+ return request("GET", `/agent${queryString({ directory })}`, {
518
+ limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
519
+ signal
520
+ });
521
+ }
522
+
523
+ async function authenticateApiKey(modelProviderId = "", apiKey = "", { signal } = {}) {
524
+ const providerId = encodeURIComponent(normalizeNullableText(modelProviderId));
525
+ if (!providerId || !String(apiKey)) {
526
+ throw new TypeError("OpenCode API-key authentication requires a provider id and key.");
527
+ }
528
+ return request("PUT", `/auth/${providerId}`, {
529
+ body: { key: String(apiKey), type: "api" },
530
+ signal
531
+ });
532
+ }
533
+
534
+ async function createSession(input = {}, { signal } = {}) {
535
+ return (await request("POST", "/api/session", { body: input, signal }))?.data || null;
536
+ }
537
+
538
+ async function deleteSession(sessionId = "", { signal } = {}) {
539
+ return request("DELETE", `/session/${encodeURIComponent(normalizeNullableText(sessionId))}`, { signal });
540
+ }
541
+
542
+ async function deleteMessage(sessionId, messageId, { signal } = {}) {
543
+ if (!normalizeNullableText(messageId)) throw new TypeError("OpenCode message deletion requires a message id.");
544
+ return request("DELETE", stableSessionPath(sessionId, `/message/${encodeURIComponent(normalizeNullableText(messageId))}`), { signal });
545
+ }
546
+
547
+ /**
548
+ * The next request to this client's directory loads a new instance, which
549
+ * reads the server's current credentials. A turn in flight there is aborted.
550
+ */
551
+ async function disposeInstance({ signal } = {}) {
552
+ const instance = instanceDirectory();
553
+ const sequence = instances.sequence;
554
+ await request("POST", "/instance/dispose", { signal });
555
+ // A request that reached the directory during disposal may have loaded it again.
556
+ if (instance && (instances.addressed.get(instance) ?? 0) <= sequence) instances.addressed.delete(instance);
557
+ }
558
+
559
+ function forDirectory(nextDirectory = "") {
560
+ const normalizedDirectory = normalizeNullableText(nextDirectory);
561
+ if (!normalizedDirectory) {
562
+ throw new TypeError("OpenCode project clients require a working directory.");
563
+ }
564
+ return createOpenCodeServerClient({
565
+ allowAttachmentDirectories,
566
+ baseUrl: origin.toString(),
567
+ defaultDirectory,
568
+ directory: normalizedDirectory,
569
+ fetchImpl,
570
+ instances,
571
+ password,
572
+ username
573
+ });
574
+ }
575
+
576
+ /** Absolute directories whose loaded instances may hold older credentials. */
577
+ function instanceDirectories() {
578
+ return [...instances.addressed.keys()];
579
+ }
580
+
581
+ async function health({ signal } = {}) {
582
+ return request("GET", "/global/health", { signal });
583
+ }
584
+
585
+ async function prepareDirectory({ signal } = {}) {
586
+ // Global health and the v2 session routes do not initialize the directory
587
+ // used by prompt/abort. Complete that native startup before exposing a
588
+ // conversation whose first Stop would otherwise wait for plugin installs.
589
+ const deadline = AbortSignal.timeout(30_000);
590
+ const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
591
+ boundedSignal.throwIfAborted();
592
+ const result = await request("GET", "/path", { signal: boundedSignal });
593
+ boundedSignal.throwIfAborted();
594
+ if (!normalizeNullableText(result?.directory)) {
595
+ throw openCodeServerError("OpenCode did not confirm its conversation directory is ready.", {
596
+ code: "assistant_opencode_directory_unavailable", method: "GET", path: "/path"
597
+ });
598
+ }
599
+ }
600
+
601
+ async function interrupt(sessionId = "", { signal } = {}) {
602
+ return request("POST", stableSessionPath(sessionId, "/abort"), { signal });
603
+ }
604
+
605
+ async function sessionStatus(sessionId = "", { signal } = {}) {
606
+ const statuses = await request("GET", "/session/status", { signal });
607
+ return statuses?.[normalizeNullableText(sessionId)] || { type: "idle" };
608
+ }
609
+
610
+ async function messages(sessionId = "", input = {}, { signal } = {}) {
611
+ // A limited read is the newest page; `before` continues with older rows.
612
+ const { source, headers } = await requestResponse(
613
+ "GET",
614
+ `${stableSessionPath(sessionId, "/message")}${queryString({ limit: input.limit, before: input.before })}`,
615
+ { signal }
616
+ );
617
+ const nextCursor = headers.get("x-next-cursor");
618
+ return { data: normalizedMessageRows(parsedJson(source)), ...(nextCursor ? { nextCursor } : {}) };
619
+ }
620
+
621
+ async function prompt(sessionId = "", input = {}, { signal } = {}) {
622
+ const body = stablePromptBody(input);
623
+ if (allowAttachmentDirectories && input.attachments?.length) {
624
+ // Native tools check the parent directory when reopening a supplied file.
625
+ // Keep these exceptions on this conversation, alongside its existing rules.
626
+ const session = await request("GET", stableSessionPath(sessionId), { signal });
498
627
  const permission = [...(session.permission || [])];
499
628
  const previousLength = permission.length;
500
- for (const attachment of attachments) {
629
+ for (const attachment of input.attachments) {
501
630
  const pattern = path.join(path.dirname(attachment.path), "*").replaceAll("\\", "/");
502
631
  const rule = permission.findLast((item) => item.permission === "external_directory" && item.pattern === pattern);
503
- if (rule?.action !== "allow") permission.push({ permission: "external_directory", pattern, action: "allow" });
632
+ if (rule?.action !== "allow") {
633
+ permission.push({ permission: "external_directory", pattern, action: "allow" });
634
+ }
504
635
  }
505
636
  if (permission.length !== previousLength) {
506
- await requestStorageResponse(route, { body: { permission } });
507
- }
508
- },
509
- async listConversationChildren(conversationId, { signal } = {}) {
510
- const children = await storageInventory(`${storageConversationPath(conversationId)}/children`, { signal });
511
- if (children.some((child) => child.parentID !== conversationId)) {
512
- throw new Error("OpenCode returned an incomplete or invalid child inventory.");
513
- }
514
- return children;
515
- },
516
- async listConversationsForDirectory(directory, { signal } = {}) {
517
- if (!path.isAbsolute(directory || "")) throw new TypeError("OpenCode inventory requires an absolute native directory.");
518
- // Use the global persisted inventory: the project-scoped /session
519
- // listing loses its Git project identity once archived source is gone.
520
- // Request one beyond our limit to detect a truncated inventory.
521
- const rows = await storageInventory(`/experimental/session?${new URLSearchParams({ directory, limit: "1001" })}`, { signal });
522
- if (rows.some((row) => typeof row.directory !== "string")) throw new Error("OpenCode inventory has no native directory.");
523
- return rows.filter((row) => row.directory === directory);
524
- },
525
- async readConversationStorage(conversationId, { signal } = {}) {
526
- // The native storage record does not resolve a live source workspace.
527
- const { data } = await requestStorageResponse(storageConversationPath(conversationId), { signal });
528
- if (data?.id !== conversationId || !path.isAbsolute(data.directory || "")) {
529
- throw new Error("OpenCode returned an invalid native conversation record.");
530
- }
531
- return data;
532
- },
533
- async readConversationStoragePage(conversationId, { before = "", signal } = {}) {
534
- const route = `${storageConversationPath(conversationId)}/message`;
535
- if (typeof before !== "string" || before.length > 8192) throw new TypeError("Invalid OpenCode storage cursor.");
536
- const query = new URLSearchParams({ limit: "1", ...(before ? { before } : {}) });
537
- const { data, headers } = await requestStorageResponse(`${route}?${query}`, {
538
- signal, maxBytes: 64 * 1024 * 1024
539
- });
540
- const nextCursor = headers.get("x-next-cursor");
541
- if (!nextCursor && /rel="next"/u.test(headers.get("link") || "")) {
542
- throw new Error("OpenCode omitted its native history continuation cursor.");
637
+ await request("PATCH", stableSessionPath(sessionId), { body: { permission }, signal });
543
638
  }
544
- return { data, nextCursor };
545
- },
546
- async agents({ directory = "", signal } = {}) {
547
- return request("GET", `/agent${queryString({ directory })}`, {
548
- limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
549
- signal
550
- });
551
- },
552
- async authenticateApiKey(modelProviderId = "", apiKey = "", { signal } = {}) {
553
- const providerId = encodeURIComponent(text(modelProviderId));
554
- if (!providerId || !String(apiKey)) {
555
- throw new TypeError("OpenCode API-key authentication requires a provider id and key.");
556
- }
557
- return request("PUT", `/auth/${providerId}`, {
558
- body: { key: String(apiKey), type: "api" },
559
- signal
560
- });
561
- },
562
- async createSession(input = {}, { signal } = {}) {
563
- return (await request("POST", "/api/session", { body: input, signal }))?.data || null;
564
- },
565
- async deleteSession(sessionId = "", { signal } = {}) {
566
- return request("DELETE", `/session/${encodeURIComponent(text(sessionId))}`, { signal });
567
- },
568
- async deleteMessage(sessionId, messageId, { signal } = {}) {
569
- if (!text(messageId)) throw new TypeError("OpenCode message deletion requires a message id.");
570
- return request("DELETE", stableSessionPath(sessionId, `/message/${encodeURIComponent(text(messageId))}`), { signal });
571
- },
572
- // The next request to this client's directory loads a new instance, which
573
- // reads the server's current credentials. A turn in flight there is aborted.
574
- async disposeInstance({ signal } = {}) {
575
- const instance = instanceDirectory();
576
- const sequence = instances.sequence;
577
- await request("POST", "/instance/dispose", { signal });
578
- // A request that reached the directory during disposal may have loaded it again.
579
- if (instance && (instances.addressed.get(instance) ?? 0) <= sequence) instances.addressed.delete(instance);
580
- },
581
- events,
582
- forDirectory(nextDirectory = "") {
583
- const normalizedDirectory = text(nextDirectory);
584
- if (!normalizedDirectory) {
585
- throw new TypeError("OpenCode project clients require a working directory.");
586
- }
587
- return createOpenCodeServerClient({
588
- allowAttachmentDirectories,
589
- baseUrl: origin.toString(),
590
- defaultDirectory,
591
- directory: normalizedDirectory,
592
- fetchImpl,
593
- instances,
594
- password,
595
- username
596
- });
597
- },
598
- // Absolute directories whose loaded instances may hold older credentials.
599
- instanceDirectories() {
600
- return [...instances.addressed.keys()];
601
- },
602
- async health({ signal } = {}) {
603
- return request("GET", "/global/health", { signal });
604
- },
605
- async prepareDirectory({ signal } = {}) {
606
- // Global health and the v2 session routes do not initialize the directory
607
- // used by prompt/abort. Complete that native startup before exposing a
608
- // conversation whose first Stop would otherwise wait for plugin installs.
609
- const deadline = AbortSignal.timeout(30_000);
610
- const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
611
- boundedSignal.throwIfAborted();
612
- const result = await request("GET", "/path", { signal: boundedSignal });
613
- boundedSignal.throwIfAborted();
614
- if (!text(result?.directory)) {
615
- throw openCodeServerError("OpenCode did not confirm its conversation directory is ready.", {
616
- code: "assistant_opencode_directory_unavailable", method: "GET", path: "/path"
617
- });
618
- }
619
- },
620
- async interrupt(sessionId = "", { signal } = {}) {
621
- return request("POST", stableSessionPath(sessionId, "/abort"), { signal });
622
- },
623
- async sessionStatus(sessionId = "", { signal } = {}) {
624
- const statuses = await request("GET", "/session/status", { signal });
625
- return statuses?.[text(sessionId)] || { type: "idle" };
626
- },
627
- async messages(sessionId = "", input = {}, { signal } = {}) {
628
- // A limited read is the newest page; `before` continues with older rows.
629
- const { source, headers } = await requestResponse(
630
- "GET",
631
- `${stableSessionPath(sessionId, "/message")}${queryString({ limit: input.limit, before: input.before })}`,
632
- { signal }
633
- );
634
- const nextCursor = headers.get("x-next-cursor");
635
- return { data: normalizedMessageRows(parsedJson(source)), ...(nextCursor ? { nextCursor } : {}) };
636
- },
637
- async prompt(sessionId = "", input = {}, { signal } = {}) {
638
- const body = stablePromptBody(input);
639
- if (allowAttachmentDirectories && input.attachments?.length) {
640
- // Native tools check the parent directory when reopening a supplied file.
641
- // Keep these exceptions on this conversation, alongside its existing rules.
642
- const session = await request("GET", stableSessionPath(sessionId), { signal });
643
- const permission = [...(session.permission || [])];
644
- const previousLength = permission.length;
645
- for (const attachment of input.attachments) {
646
- const pattern = path.join(path.dirname(attachment.path), "*").replaceAll("\\", "/");
647
- const rule = permission.findLast((item) => item.permission === "external_directory" && item.pattern === pattern);
648
- if (rule?.action !== "allow") {
649
- permission.push({ permission: "external_directory", pattern, action: "allow" });
650
- }
651
- }
652
- if (permission.length !== previousLength) {
653
- await request("PATCH", stableSessionPath(sessionId), { body: { permission }, signal });
654
- }
655
- }
656
- await request("POST", stableSessionPath(sessionId, "/prompt_async"), {
657
- body,
658
- signal
659
- });
660
- return {
661
- delivery: text(input.delivery),
662
- id: text(input.id),
663
- sessionID: text(sessionId)
664
- };
665
- },
666
- async providers({ directory = "", signal } = {}) {
667
- return sanitizedOpenCodeProviderCatalog(await request("GET", `/provider${queryString({ directory })}`, {
668
- limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
669
- signal
670
- }));
671
- },
672
- async readSession(sessionId = "", { signal } = {}) {
673
- return (await request("GET", sessionPath(sessionId), { signal }))?.data || null;
674
- },
675
- async removeAuthentication(modelProviderId = "", { signal } = {}) {
676
- return request("DELETE", `/auth/${encodeURIComponent(text(modelProviderId))}`, { signal });
677
- },
678
- async switchAgent(sessionId = "", agent = "", { signal } = {}) {
679
- return request("POST", sessionPath(sessionId, "/agent"), {
680
- body: { agent: text(agent) },
681
- signal
682
- });
683
- },
684
- async switchModel(sessionId = "", model = {}, { signal } = {}) {
685
- return request("POST", sessionPath(sessionId, "/model"), {
686
- body: { model },
687
- signal
688
- });
689
639
  }
640
+ await request("POST", stableSessionPath(sessionId, "/prompt_async"), {
641
+ body,
642
+ signal
643
+ });
644
+ return {
645
+ delivery: normalizeNullableText(input.delivery),
646
+ id: normalizeNullableText(input.id),
647
+ sessionID: normalizeNullableText(sessionId)
648
+ };
649
+ }
650
+
651
+ async function providers({ directory = "", signal } = {}) {
652
+ return sanitizedOpenCodeProviderCatalog(await request("GET", `/provider${queryString({ directory })}`, {
653
+ limitBytes: OPENCODE_CATALOG_LIMIT_BYTES,
654
+ signal
655
+ }));
656
+ }
657
+
658
+ async function readSession(sessionId = "", { signal } = {}) {
659
+ return (await request("GET", sessionPath(sessionId), { signal }))?.data || null;
660
+ }
661
+
662
+ async function removeAuthentication(modelProviderId = "", { signal } = {}) {
663
+ return request("DELETE", `/auth/${encodeURIComponent(normalizeNullableText(modelProviderId))}`, { signal });
664
+ }
665
+
666
+ async function switchAgent(sessionId = "", agent = "", { signal } = {}) {
667
+ return request("POST", sessionPath(sessionId, "/agent"), {
668
+ body: { agent: normalizeNullableText(agent) },
669
+ signal
670
+ });
671
+ }
672
+
673
+ async function switchModel(sessionId = "", model = {}, { signal } = {}) {
674
+ return request("POST", sessionPath(sessionId, "/model"), {
675
+ body: { model },
676
+ signal
677
+ });
678
+ }
679
+
680
+ return Object.freeze({
681
+ allowConversationAttachments,
682
+ listConversationChildren,
683
+ listConversationsForDirectory,
684
+ readConversationStorage,
685
+ readConversationStoragePage,
686
+ agents,
687
+ authenticateApiKey,
688
+ createSession,
689
+ deleteSession,
690
+ deleteMessage,
691
+ disposeInstance,
692
+ events,
693
+ forDirectory,
694
+ instanceDirectories,
695
+ health,
696
+ prepareDirectory,
697
+ interrupt,
698
+ sessionStatus,
699
+ messages,
700
+ prompt,
701
+ providers,
702
+ readSession,
703
+ removeAuthentication,
704
+ switchAgent,
705
+ switchModel
690
706
  });
691
707
  }
692
708
 
@@ -701,97 +717,10 @@ function openCodeAssistantMessageText(message = {}) {
701
717
  return [...new Set(values)].join("\n\n");
702
718
  }
703
719
 
704
- // Native preservation uses the storage API independently of the saved worktree.
705
- // Status still comes from the exact directory-scoped control client.
706
- async function inspectOpenCodeConversationFamily(storageClient, controlClient, binding, { signal } = {}) {
707
- const queue = [binding.conversationId];
708
- const seen = new Set();
709
- const records = [];
710
- for (let index = 0; index < queue.length; index += 1) {
711
- signal?.throwIfAborted();
712
- const conversationId = queue[index];
713
- if (seen.has(conversationId) || queue.length > 1000) throw new Error("OpenCode native family is cyclic or exceeds its inventory limit.");
714
- seen.add(conversationId);
715
- let native;
716
- try { native = await storageClient.readConversationStorage(conversationId, { signal }); }
717
- catch (error) { if (error.statusCode === 404 && index === 0) return []; throw error; }
718
- if (native?.id !== conversationId || native?.directory !== binding.workdir ||
719
- (await controlClient.sessionStatus(conversationId, { signal })).type !== "idle") {
720
- throw new Error("OpenCode retirement requires an idle native family in the exact saved directory.");
721
- }
722
- records.push({ conversationId, workdir: native.directory,
723
- ...(Number.isFinite(native.time?.created) ? { createdAt: new Date(native.time.created).toISOString() } : {}),
724
- ...(Number.isFinite(native.time?.updated) ? { updatedAt: new Date(native.time.updated).toISOString() } : {}) });
725
- const children = await storageClient.listConversationChildren(conversationId, { signal });
726
- for (const child of children) {
727
- if (child.directory !== binding.workdir) throw new Error("OpenCode child directory differs from its saved owner.");
728
- queue.push(child.id);
729
- }
730
- }
731
- return records.sort((left, right) => left.conversationId.localeCompare(right.conversationId));
732
- }
733
-
734
- async function exportOpenCodeNativeHistory(storageClient, controlClient, id, onRecord, { signal: inputSignal } = {}) {
735
- const deadline = AbortSignal.timeout(300_000);
736
- const signal = inputSignal ? AbortSignal.any([inputSignal, deadline]) : deadline;
737
- const output = createNativeHistoryExport(onRecord, { signal });
738
- const info = await storageClient.readConversationStorage(id, { signal });
739
- if (info?.id !== id || (await controlClient.sessionStatus(id, { signal })).type !== "idle") {
740
- throw new Error("OpenCode native export requires the exact idle conversation.");
741
- }
742
- await output.emit({ type: "thread", thread: info, text: [] });
743
- const messageIds = new Set();
744
- const cursors = new Set();
745
- let before = "";
746
- let pages = 0;
747
- while (true) {
748
- signal.throwIfAborted();
749
- if (++pages > 20_000) throw new Error("OpenCode native export exceeded its page limit; history was not retired.");
750
- const response = await storageClient.readConversationStoragePage(id, { before, signal });
751
- if (!Array.isArray(response?.data) || response.data.length > 1 ||
752
- (response.nextCursor !== null && (typeof response.nextCursor !== "string" || !response.nextCursor || response.nextCursor.length > 8192)) ||
753
- (response.data.length === 0 && response.nextCursor !== null)) {
754
- throw new Error("OpenCode returned an incomplete or invalid native message page.");
755
- }
756
- for (const message of response.data) {
757
- const info = message?.info;
758
- if (!/^msg_[a-zA-Z0-9_]{1,256}$/u.test(info?.id) || info.sessionID !== id ||
759
- !["user", "assistant"].includes(info.role) || !Array.isArray(message.parts) || messageIds.has(info.id)) {
760
- throw new Error("OpenCode returned an invalid or duplicate native message.");
761
- }
762
- messageIds.add(info.id);
763
- const content = openCodeAssistantMessageText({ content: message.parts });
764
- await output.emit({ type: "message", message, text: content ? [{ role: info.role, text: content,
765
- branchId: id, messageId: info.id,
766
- ...(info.parentID ? { parentMessageId: info.parentID } : {}),
767
- ...(Number.isFinite(info.time?.created) ? { createdAt: new Date(info.time.created).toISOString() } : {}),
768
- ...(Number.isFinite(info.time?.completed) ? { completedAt: new Date(info.time.completed).toISOString() } : {}),
769
- ...(info.modelID || info.model?.modelID ? { modelId: info.modelID || info.model.modelID } : {}),
770
- ...(info.providerID || info.model?.providerID ? { modelProviderId: info.providerID || info.model.providerID } : {}),
771
- ...(info.agent ? { agent: info.agent } : {}) }] : [] });
772
- }
773
- if (response.nextCursor === null) break;
774
- if (cursors.has(response.nextCursor)) throw new Error("OpenCode repeated a native history cursor.");
775
- cursors.add(response.nextCursor);
776
- before = response.nextCursor;
777
- }
778
- return output.complete();
779
- }
780
-
781
- /** Retire the exact native family after the host's durable preservation. */
782
- async function retireOpenCodeConversationHistory(storageClient, controlClient, binding, options = {}) {
783
- const inspect = () => inspectOpenCodeConversationFamily(storageClient, controlClient, binding, { signal: options.signal });
784
- return retireNativeConversation({ binding, inspect, beforeDelete: options.beforeDelete,
785
- readConversation: async (id) => ({ info: await storageClient.readConversationStorage(id), messages: (await controlClient.messages(id)).data }),
786
- exportConversation: (id, onRecord) => exportOpenCodeNativeHistory(storageClient, controlClient, id, onRecord, { signal: options.signal }),
787
- remove: () => controlClient.deleteSession(binding.conversationId) });
788
- }
789
-
790
720
  export {
791
- readOpenCodeConversationDatabase,
792
- retireOpenCodeConversationHistory,
793
721
  openCodeAssistantMessageText,
794
722
  OPENCODE_RESPONSE_LIMIT_BYTES,
795
723
  createOpenCodeServerClient,
724
+ normalizedMessageRows,
796
725
  readBoundedResponse
797
726
  };