@openchambery/web 1.19.20 → 2.0.0-beta.1

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 (189) hide show
  1. package/README.md +13 -11
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-C_JfbxNl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-B1iUwzOz.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-BPtMj3-X.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-CKClHsWN.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-DiA24bnN.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-XlzRSS7E.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-DdIaaLba.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BbwuLdXU.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-ChzfD7mc.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-DyjmxUEN.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/{multirun-8fkF0Ji_.js → multirun-BvSG94mW.js} +1 -1
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/{projectMeta--y6tyjrv.js → projectMeta-DOe49dM_.js} +2 -2
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-DYWtBrkW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BSWg99jc.js → runtimeEndpointReset-B6BxhvR0.js} +5 -5
  52. package/dist/assets/{sessionChangeSummary-BwupJF-Q.js → sessionChangeSummary-VVBlEXuh.js} +1 -1
  53. package/dist/assets/{sessionLookup-DP2X1qRR.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/{src-DZ60kHBP.css → src-BzX5UI-c.css} +1 -1
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/{textarea-BnD799LW.js → textarea-3XR8sUfy.js} +1 -1
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-DWLxpWlS.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/{useDesktopWindowControlsLayout-BGPR_rq6.js → useDesktopWindowControlsLayout-B1GX59-L.js} +1 -1
  63. package/dist/assets/{useEffectiveDirectory-CbV9_r1s.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-Dnors7hB.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-DQqqrOBU.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BjvfkSRS.js +0 -3
  168. package/dist/assets/MarkdownRenderer-Cg82L3fN.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-s_vvUD_F.js +0 -1
  170. package/dist/assets/MobileSurface-DHpXHbJp.js +0 -2
  171. package/dist/assets/SettingsView-l0J7pfYo.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CHpibtno.js +0 -7
  174. package/dist/assets/insertionBoundaries-Yd1Lorjw.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-BoRXxmB3.js +0 -2
  178. package/dist/assets/main-DCDcdV5o.js +0 -169
  179. package/dist/assets/miniChat-12fxk5KR.js +0 -2
  180. package/dist/assets/mobile-CkZem9Ad.js +0 -2
  181. package/dist/assets/pl-DW2XJGC4.js +0 -15
  182. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  183. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  184. package/dist/assets/renderElectronMiniChatApp-COFc0ZDJ.js +0 -2
  185. package/dist/assets/renderMobileApp-BKzx7JL3.js +0 -28
  186. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  187. package/dist/assets/useMobileNavigationStore-DrB_awgk.js +0 -413
  188. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  189. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
@@ -1,15 +1,65 @@
1
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
1
+ import { OpenCode } from '@opencode/client';
2
2
 
3
3
  // --- error classification ---
4
4
 
5
5
  const isTransportError = (error) => {
6
6
  if (!error) return true;
7
+ if (error.reason === 'Transport') return true;
7
8
  if (error.name === 'TypeError' || error.name === 'FetchError' || error.name === 'AbortError') return true;
9
+ if (error.cause?.name === 'TypeError' || error.cause?.name === 'FetchError' || error.cause?.name === 'AbortError') return true;
8
10
  const code = error.cause?.code || error.code;
9
11
  if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'EPIPE') return true;
10
12
  return false;
11
13
  };
12
14
 
15
+ // v2 client throws raw data or ClientError; recover HTTP status when present.
16
+ const getHttpStatus = (error) => {
17
+ const candidates = [error?.cause?.status, error?.status, error?.response?.status];
18
+ for (const value of candidates) {
19
+ if (Number.isFinite(value)) return value;
20
+ }
21
+ return undefined;
22
+ };
23
+
24
+ const classifyPromptFailure = (error) => {
25
+ if (isTransportError(error)) {
26
+ return { ambiguous: true, status: undefined };
27
+ }
28
+ const status = getHttpStatus(error);
29
+ if (Number.isFinite(status)) {
30
+ return { ambiguous: isRetryableHttpStatus(status), status };
31
+ }
32
+ // Declared v2 HTTP errors (400/401/404/409) throw JSON without a status field.
33
+ return { ambiguous: false, status: 400 };
34
+ };
35
+
36
+ // Map sanitized parts onto the official v2 prompt body (text/files/agents).
37
+ const toV2PromptInput = (sanitizedInput, sessionID) => {
38
+ const text = sanitizedInput.parts
39
+ .filter((part) => part.type === 'text')
40
+ .map((part) => part.text)
41
+ .join('\n');
42
+ const files = sanitizedInput.parts
43
+ .filter((part) => part.type === 'file')
44
+ .map((part) => ({
45
+ uri: part.url,
46
+ ...(part.filename ? { name: part.filename } : {}),
47
+ }));
48
+ const agents = sanitizedInput.parts
49
+ .filter((part) => part.type === 'agent')
50
+ .map((part) => ({ name: part.name }));
51
+
52
+ return {
53
+ sessionID,
54
+ id: sanitizedInput.messageID,
55
+ text,
56
+ ...(files.length > 0 ? { files } : {}),
57
+ ...(agents.length > 0 ? { agents } : {}),
58
+ ...(sanitizedInput.metadata ? { metadata: sanitizedInput.metadata } : {}),
59
+ delivery: 'steer',
60
+ };
61
+ };
62
+
13
63
  const isRetryableHttpStatus = (status) => {
14
64
  if (!Number.isFinite(status)) return true;
15
65
  return status === 408 || status === 429 || status >= 500;
@@ -84,7 +134,7 @@ export const createConversationsService = (deps) => {
84
134
 
85
135
  const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
86
136
  const authHeaders = getOpenCodeAuthHeaders();
87
- return createOpencodeClient({ baseUrl, headers: authHeaders });
137
+ return OpenCode.make({ baseUrl, headers: authHeaders });
88
138
  };
89
139
 
90
140
  const createAndPrompt = async ({ sanitizedInput } = {}) => {
@@ -99,19 +149,34 @@ export const createConversationsService = (deps) => {
99
149
  const client = clientOrError;
100
150
 
101
151
  // Phase 1: create session (30s timeout)
102
- let sessionResult;
103
- let upstreamStatus;
152
+ let session;
104
153
  const createT = timeoutSignal(CREATE_TIMEOUT_MS);
105
154
  try {
106
- sessionResult = await client.session.create({
107
- directory: sanitizedInput.directory,
155
+ session = await client.session.create({
156
+ location: { directory: sanitizedInput.directory },
108
157
  ...(sanitizedInput.title ? { title: sanitizedInput.title } : {}),
109
- ...(sanitizedInput.parentID ? { parentID: sanitizedInput.parentID } : {}),
110
- ...(sanitizedInput.metadata ? { metadata: sanitizedInput.metadata } : {}),
158
+ ...(sanitizedInput.agent ? { agent: sanitizedInput.agent } : {}),
159
+ ...(sanitizedInput.model ? {
160
+ model: {
161
+ id: sanitizedInput.model.modelID,
162
+ providerID: sanitizedInput.model.providerID,
163
+ ...(sanitizedInput.variant ? { variant: sanitizedInput.variant } : {}),
164
+ },
165
+ } : {}),
111
166
  }, {
112
167
  signal: createT.signal,
113
168
  });
114
169
  } catch (error) {
170
+ const status = getHttpStatus(error);
171
+ if (Number.isFinite(status) && !isTransportError(error)) {
172
+ logger.warn(`[conversations] session.create client error (${status})`);
173
+ return {
174
+ ok: false,
175
+ phase: 'create',
176
+ error: safeErrorMessage('create'),
177
+ status,
178
+ };
179
+ }
115
180
  logger.warn('[conversations] session.create transport error');
116
181
  return {
117
182
  ok: false,
@@ -122,19 +187,7 @@ export const createConversationsService = (deps) => {
122
187
  createT.clear();
123
188
  }
124
189
 
125
- upstreamStatus = sessionResult?.response?.status;
126
-
127
- if (sessionResult.error) {
128
- logger.warn(`[conversations] session.create SDK error (${upstreamStatus || 'no status'})`);
129
- return {
130
- ok: false,
131
- phase: 'create',
132
- error: safeErrorMessage('create'),
133
- ...(Number.isFinite(upstreamStatus) ? { status: upstreamStatus } : {}),
134
- };
135
- }
136
-
137
- const sessionID = sessionResult?.data?.id;
190
+ const sessionID = session?.id;
138
191
  if (!sessionID) {
139
192
  logger.warn('[conversations] session.create returned no session ID');
140
193
  return {
@@ -144,29 +197,17 @@ export const createConversationsService = (deps) => {
144
197
  };
145
198
  }
146
199
 
147
- const session = sessionResult?.data;
148
-
149
- // Phase 2: promptAsync (45s timeout, 204, does not wait for model completion)
200
+ // Phase 2: v2 prompt + delivery=steer (45s timeout). Host only orchestrates
201
+ // create + first prompt; it does not cache message body as conversation content.
150
202
  let promptResult;
151
203
  const promptT = timeoutSignal(PROMPT_TIMEOUT_MS);
152
204
  try {
153
- promptResult = await client.session.promptAsync({
154
- sessionID,
155
- directory: sanitizedInput.directory,
156
- messageID: sanitizedInput.messageID,
157
- model: {
158
- providerID: sanitizedInput.model.providerID,
159
- modelID: sanitizedInput.model.modelID,
160
- },
161
- ...(sanitizedInput.agent ? { agent: sanitizedInput.agent } : {}),
162
- ...(sanitizedInput.variant ? { variant: sanitizedInput.variant } : {}),
163
- parts: sanitizedInput.parts,
164
- }, {
205
+ promptResult = await client.session.prompt(toV2PromptInput(sanitizedInput, sessionID), {
165
206
  signal: promptT.signal,
166
207
  });
167
208
  } catch (error) {
168
- const ambiguous = isTransportError(error);
169
- logger.warn(`[conversations] promptAsync throw (ambiguous=${ambiguous})`);
209
+ const { ambiguous, status } = classifyPromptFailure(error);
210
+ logger.warn(`[conversations] prompt throw (ambiguous=${ambiguous})`);
170
211
  if (ambiguous) {
171
212
  safeMark(markUserMessageSent, sessionID, logger);
172
213
  }
@@ -177,37 +218,25 @@ export const createConversationsService = (deps) => {
177
218
  messageID: sanitizedInput.messageID,
178
219
  ambiguous,
179
220
  error: safeErrorMessage('prompt'),
221
+ ...(Number.isFinite(status) ? { status } : {}),
180
222
  };
181
223
  } finally {
182
224
  promptT.clear();
183
225
  }
184
226
 
185
227
  // SDK result shape { data, error, response }
186
- if (promptResult.error) {
187
- const httpStatus = promptResult.response?.status;
188
- const ambiguous = isAmbiguousPromptError(promptResult.error, promptResult.response);
189
- logger.warn(`[conversations] promptAsync SDK error (${httpStatus || 'no status'}, ambiguous=${ambiguous})`);
190
- if (ambiguous) {
191
- safeMark(markUserMessageSent, sessionID, logger);
192
- }
193
- return {
194
- ok: false,
195
- phase: 'prompt',
196
- session,
197
- messageID: sanitizedInput.messageID,
198
- ambiguous,
199
- error: safeErrorMessage('prompt'),
200
- ...(Number.isFinite(httpStatus) ? { status: httpStatus } : {}),
201
- };
202
- }
203
-
204
- // promptAsync 204 success
228
+ // prompt success — return inbox item when present; never persist parts/text.
205
229
  safeMark(markUserMessageSent, sessionID, logger);
206
230
 
231
+ const inbox = promptResult && typeof promptResult === 'object'
232
+ ? promptResult
233
+ : undefined;
234
+
207
235
  return {
208
236
  ok: true,
209
237
  session,
210
238
  messageID: sanitizedInput.messageID,
239
+ ...(inbox && (inbox.id || inbox.type) ? { inbox } : {}),
211
240
  };
212
241
  };
213
242
 
@@ -1,15 +1,29 @@
1
1
  import { describe, expect, it, vi, beforeEach } from 'vitest';
2
2
  import { validateConversationInput } from './validation.js';
3
3
 
4
- // Mock the SDK module before any imports
5
- vi.mock('@opencode-ai/sdk/v2', () => ({
6
- createOpencodeClient: vi.fn(),
4
+ // Mock the official v2 client before any imports
5
+ vi.mock('@opencode/client', () => ({
6
+ OpenCode: { make: vi.fn() },
7
7
  }));
8
8
 
9
9
  // Lazy import to allow vi.mock to take effect
10
10
  const { createConversationsService } = await import('./service.js');
11
11
 
12
- const mockCreateOpencodeClient = (await import('@opencode-ai/sdk/v2')).createOpencodeClient;
12
+ const mockMake = (await import('@opencode/client')).OpenCode.make;
13
+
14
+ const clientError = (reason, cause) => {
15
+ const error = new Error(reason);
16
+ error.name = 'ClientError';
17
+ error.reason = reason;
18
+ if (cause !== undefined) error.cause = cause;
19
+ return error;
20
+ };
21
+
22
+ const httpError = (status, message) => {
23
+ const error = new Error(message);
24
+ error.status = status;
25
+ return error;
26
+ };
13
27
 
14
28
  describe('conversations validation', () => {
15
29
  it('rejects non-object body', () => {
@@ -270,7 +284,7 @@ describe('conversations service', () => {
270
284
  mockBuildUrl.mockReturnValue('http://127.0.0.1:4096/');
271
285
  mockGetHeaders.mockReturnValue({ Authorization: 'Basic test' });
272
286
  mockWaitReady.mockResolvedValue(undefined);
273
- mockCreateOpencodeClient.mockReset();
287
+ mockMake.mockReset();
274
288
  });
275
289
 
276
290
  const baseInput = (overrides = {}) => ({
@@ -295,9 +309,9 @@ describe('conversations service', () => {
295
309
  // --- create failures ---
296
310
 
297
311
  it('returns create failure with safe error when SDK returns error', async () => {
298
- mockCreateOpencodeClient.mockReturnValue({
312
+ mockMake.mockReturnValue({
299
313
  session: {
300
- create: async () => ({ data: null, error: new Error('internal: port 4096 unreachable'), response: { status: 500 } }),
314
+ create: async () => { throw clientError('UnexpectedStatus', { status: 500 }); },
301
315
  },
302
316
  });
303
317
  const service = createService();
@@ -311,7 +325,7 @@ describe('conversations service', () => {
311
325
  });
312
326
 
313
327
  it('returns create failure on transport error during create', async () => {
314
- mockCreateOpencodeClient.mockReturnValue({
328
+ mockMake.mockReturnValue({
315
329
  session: {
316
330
  create: async () => { throw new Error('Connection refused'); },
317
331
  },
@@ -325,15 +339,19 @@ describe('conversations service', () => {
325
339
 
326
340
  // --- create: no agent/model ---
327
341
 
328
- it('does NOT pass agent/model/variant to session.create', async () => {
329
- mockCreateOpencodeClient.mockReturnValue({
342
+ it('passes agent/model/variant to session.create', async () => {
343
+ mockMake.mockReturnValue({
330
344
  session: {
331
345
  create: async (params) => {
332
- expect(params.agent).toBeUndefined();
333
- expect(params.model).toBeUndefined();
334
- return { data: { id: 'ses_ok' }, error: null, response: { status: 200 } };
346
+ expect(params.agent).toBe('builder');
347
+ expect(params.model).toEqual({
348
+ id: 'gpt-4o',
349
+ providerID: 'openai',
350
+ variant: 'v1',
351
+ });
352
+ return { id: 'ses_ok' };
335
353
  },
336
- promptAsync: async () => ({ data: undefined, error: null, response: { status: 204 } }),
354
+ prompt: async () => undefined,
337
355
  },
338
356
  });
339
357
  const service = createService();
@@ -343,42 +361,55 @@ describe('conversations service', () => {
343
361
  expect(result.ok).toBe(true);
344
362
  });
345
363
 
346
- // --- promptAsync usage ---
364
+ // --- prompt usage ---
347
365
 
348
- it('uses promptAsync (not prompt) and does not pass delivery', async () => {
349
- let promptAsyncCalled = false;
350
- mockCreateOpencodeClient.mockReturnValue({
366
+ it('uses prompt with delivery=steer and does not cache message body', async () => {
367
+ let promptCalled = false;
368
+ mockMake.mockReturnValue({
351
369
  session: {
352
- create: async () => ({ data: { id: 'ses_pa' }, error: null, response: { status: 200 } }),
353
- promptAsync: async (params) => {
354
- promptAsyncCalled = true;
355
- expect(params.delivery).toBeUndefined();
356
- return { data: undefined, error: null, response: { status: 204 } };
370
+ create: async () => ({ id: 'ses_pa' }),
371
+ prompt: async (params) => {
372
+ promptCalled = true;
373
+ expect(params.delivery).toBe('steer');
374
+ return {
375
+ id: 'msg_test',
376
+ sessionID: 'ses_pa',
377
+ type: 'user',
378
+ delivery: 'steer',
379
+ payload: { text: 'hello' },
380
+ timeCreated: 1,
381
+ };
357
382
  },
358
383
  },
359
384
  });
360
385
  const service = createService();
361
386
  const result = await service.createAndPrompt({ sanitizedInput: baseInput() });
362
- expect(promptAsyncCalled).toBe(true);
387
+ expect(promptCalled).toBe(true);
363
388
  expect(result.ok).toBe(true);
389
+ expect(result.inbox).toEqual(expect.objectContaining({ id: 'msg_test', delivery: 'steer' }));
390
+ expect(result.parts).toBeUndefined();
391
+ expect(result.text).toBeUndefined();
364
392
  });
365
393
 
366
394
  // --- success ---
367
395
 
368
- it('returns success when create + promptAsync succeed', async () => {
396
+ it('returns success when create + prompt succeed', async () => {
369
397
  const sessionData = { id: 'ses_created', title: 'Test' };
370
- mockCreateOpencodeClient.mockReturnValue({
398
+ mockMake.mockReturnValue({
371
399
  session: {
372
400
  create: async (params) => {
373
- expect(params.directory).toBe('/test');
374
- return { data: sessionData, error: null, response: { status: 200 } };
401
+ expect(params.location).toEqual({ directory: '/test' });
402
+ expect(params.model).toEqual({ id: 'gpt-4o', providerID: 'openai' });
403
+ return sessionData;
375
404
  },
376
- promptAsync: async (params) => {
405
+ prompt: async (params) => {
377
406
  expect(params.sessionID).toBe('ses_created');
378
- expect(params.messageID).toBe('msg_test');
379
- expect(params.model).toEqual({ providerID: 'openai', modelID: 'gpt-4o' });
380
- expect(params.parts).toEqual([{ type: 'text', text: 'hello' }]);
381
- return { data: undefined, error: null, response: { status: 204 } };
407
+ expect(params.id).toBe('msg_test');
408
+ expect(params.text).toBe('hello');
409
+ expect(params.delivery).toBe('steer');
410
+ expect(params.model).toBeUndefined();
411
+ expect(params.parts).toBeUndefined();
412
+ return undefined;
382
413
  },
383
414
  },
384
415
  });
@@ -390,14 +421,19 @@ describe('conversations service', () => {
390
421
  expect(mockMarkSent).toHaveBeenCalledWith('ses_created');
391
422
  });
392
423
 
393
- it('passes agent, variant to promptAsync', async () => {
394
- mockCreateOpencodeClient.mockReturnValue({
424
+ it('passes agent, variant to session.create and not prompt', async () => {
425
+ mockMake.mockReturnValue({
395
426
  session: {
396
- create: async () => ({ data: { id: 'ses_avd' }, error: null, response: { status: 200 } }),
397
- promptAsync: async (params) => {
427
+ create: async (params) => {
398
428
  expect(params.agent).toBe('builder');
399
- expect(params.variant).toBe('fast');
400
- return { data: undefined, error: null, response: { status: 204 } };
429
+ expect(params.model).toEqual({ id: 'gpt-4o', providerID: 'openai', variant: 'fast' });
430
+ return { id: 'ses_avd' };
431
+ },
432
+ prompt: async (params) => {
433
+ expect(params.agent).toBeUndefined();
434
+ expect(params.variant).toBeUndefined();
435
+ expect(params.delivery).toBe('steer');
436
+ return undefined;
401
437
  },
402
438
  },
403
439
  });
@@ -412,14 +448,10 @@ describe('conversations service', () => {
412
448
 
413
449
  it('returns prompt failure with ambiguous=false for 400 SDK error, safe error, no mark', async () => {
414
450
  const sessionData = { id: 'ses_400' };
415
- mockCreateOpencodeClient.mockReturnValue({
451
+ mockMake.mockReturnValue({
416
452
  session: {
417
- create: async () => ({ data: sessionData, error: null, response: { status: 200 } }),
418
- promptAsync: async () => ({
419
- data: null,
420
- error: new Error('path /foo/bar does not exist'),
421
- response: { status: 400 },
422
- }),
453
+ create: async () => sessionData,
454
+ prompt: async () => { throw httpError(400, 'path /foo/bar does not exist'); },
423
455
  },
424
456
  });
425
457
  const service = createService();
@@ -433,14 +465,10 @@ describe('conversations service', () => {
433
465
  });
434
466
 
435
467
  it('returns prompt failure with ambiguous=false for 404, no mark', async () => {
436
- mockCreateOpencodeClient.mockReturnValue({
468
+ mockMake.mockReturnValue({
437
469
  session: {
438
- create: async () => ({ data: { id: 'ses_404' }, error: null, response: { status: 200 } }),
439
- promptAsync: async () => ({
440
- data: null,
441
- error: new Error('Session not found'),
442
- response: { status: 404 },
443
- }),
470
+ create: async () => ({ id: 'ses_404' }),
471
+ prompt: async () => { throw httpError(404, 'Session not found'); },
444
472
  },
445
473
  });
446
474
  const service = createService();
@@ -456,14 +484,10 @@ describe('conversations service', () => {
456
484
 
457
485
  it('returns ambiguous=true for 503, safe error, marks activity', async () => {
458
486
  const sessionData = { id: 'ses_503' };
459
- mockCreateOpencodeClient.mockReturnValue({
487
+ mockMake.mockReturnValue({
460
488
  session: {
461
- create: async () => ({ data: sessionData, error: null, response: { status: 200 } }),
462
- promptAsync: async () => ({
463
- data: null,
464
- error: new Error('Service Unavailable'),
465
- response: { status: 503 },
466
- }),
489
+ create: async () => sessionData,
490
+ prompt: async () => { throw clientError('UnexpectedStatus', { status: 503 }); },
467
491
  },
468
492
  });
469
493
  const service = createService();
@@ -477,10 +501,10 @@ describe('conversations service', () => {
477
501
  });
478
502
 
479
503
  it('returns ambiguous=true for transport throw, marks activity', async () => {
480
- mockCreateOpencodeClient.mockReturnValue({
504
+ mockMake.mockReturnValue({
481
505
  session: {
482
- create: async () => ({ data: { id: 'ses_transport' }, error: null, response: { status: 200 } }),
483
- promptAsync: async () => { throw new TypeError('fetch failed'); },
506
+ create: async () => ({ id: 'ses_transport' }),
507
+ prompt: async () => { throw new TypeError('fetch failed'); },
484
508
  },
485
509
  });
486
510
  const service = createService();
@@ -493,14 +517,10 @@ describe('conversations service', () => {
493
517
  });
494
518
 
495
519
  it('returns ambiguous=true for 408, marks activity', async () => {
496
- mockCreateOpencodeClient.mockReturnValue({
520
+ mockMake.mockReturnValue({
497
521
  session: {
498
- create: async () => ({ data: { id: 'ses_408' }, error: null, response: { status: 200 } }),
499
- promptAsync: async () => ({
500
- data: null,
501
- error: new Error('Request Timeout'),
502
- response: { status: 408 },
503
- }),
522
+ create: async () => ({ id: 'ses_408' }),
523
+ prompt: async () => { throw clientError('UnexpectedStatus', { status: 408 }); },
504
524
  },
505
525
  });
506
526
  const service = createService();
@@ -511,14 +531,10 @@ describe('conversations service', () => {
511
531
  });
512
532
 
513
533
  it('returns ambiguous=true for 429, marks activity', async () => {
514
- mockCreateOpencodeClient.mockReturnValue({
534
+ mockMake.mockReturnValue({
515
535
  session: {
516
- create: async () => ({ data: { id: 'ses_429' }, error: null, response: { status: 200 } }),
517
- promptAsync: async () => ({
518
- data: null,
519
- error: new Error('Too Many Requests'),
520
- response: { status: 429 },
521
- }),
536
+ create: async () => ({ id: 'ses_429' }),
537
+ prompt: async () => { throw clientError('UnexpectedStatus', { status: 429 }); },
522
538
  },
523
539
  });
524
540
  const service = createService();
@@ -529,10 +545,10 @@ describe('conversations service', () => {
529
545
  });
530
546
 
531
547
  it('does NOT mark on non-transport, non-ambiguous throw', async () => {
532
- mockCreateOpencodeClient.mockReturnValue({
548
+ mockMake.mockReturnValue({
533
549
  session: {
534
- create: async () => ({ data: { id: 'ses_perm' }, error: null, response: { status: 200 } }),
535
- promptAsync: async () => { throw new Error('Some permanent failure'); },
550
+ create: async () => ({ id: 'ses_perm' }),
551
+ prompt: async () => { throw new Error('Some permanent failure'); },
536
552
  },
537
553
  });
538
554
  const service = createService();
@@ -552,10 +568,10 @@ describe('conversations service', () => {
552
568
  markUserMessageSent: throwingMark,
553
569
  waitForOpenCodeReady: mockWaitReady,
554
570
  });
555
- mockCreateOpencodeClient.mockReturnValue({
571
+ mockMake.mockReturnValue({
556
572
  session: {
557
- create: async () => ({ data: { id: 'ses_safe' }, error: null, response: { status: 200 } }),
558
- promptAsync: async () => ({ data: undefined, error: null, response: { status: 204 } }),
573
+ create: async () => ({ id: 'ses_safe' }),
574
+ prompt: async () => undefined,
559
575
  },
560
576
  });
561
577
  const result = await svc.createAndPrompt({ sanitizedInput: baseInput() });
@@ -568,13 +584,13 @@ describe('conversations service', () => {
568
584
  it('passes directory/headers through buildOpenCodeUrl and getOpenCodeAuthHeaders', async () => {
569
585
  mockBuildUrl.mockReturnValue('http://custom:9999/');
570
586
  mockGetHeaders.mockReturnValue({ Authorization: 'Bearer secret123' });
571
- mockCreateOpencodeClient.mockImplementation((config) => {
587
+ mockMake.mockImplementation((config) => {
572
588
  expect(config.baseUrl).toBe('http://custom:9999');
573
589
  expect(config.headers).toEqual({ Authorization: 'Bearer secret123' });
574
590
  return {
575
591
  session: {
576
- create: async () => ({ data: { id: 'ses_url' }, error: null, response: { status: 200 } }),
577
- promptAsync: async () => ({ data: undefined, error: null, response: { status: 204 } }),
592
+ create: async () => ({ id: 'ses_url' }),
593
+ prompt: async () => undefined,
578
594
  },
579
595
  };
580
596
  });
@@ -585,10 +601,10 @@ describe('conversations service', () => {
585
601
 
586
602
  it('awaits waitForOpenCodeReady before calling SDK', async () => {
587
603
  let createCalled = false;
588
- mockCreateOpencodeClient.mockReturnValue({
604
+ mockMake.mockReturnValue({
589
605
  session: {
590
- create: async () => { createCalled = true; return { data: { id: 'ses_w' }, error: null, response: { status: 200 } }; },
591
- promptAsync: async () => ({ data: undefined, error: null, response: { status: 204 } }),
606
+ create: async () => { createCalled = true; return { id: 'ses_w' }; },
607
+ prompt: async () => undefined,
592
608
  },
593
609
  });
594
610
  const service = createService();
@@ -600,16 +616,16 @@ describe('conversations service', () => {
600
616
  // --- internal timeouts ---
601
617
 
602
618
  it('uses internal timeout signal for session.create', async () => {
603
- mockCreateOpencodeClient.mockReturnValue({
619
+ mockMake.mockReturnValue({
604
620
  session: {
605
621
  create: async (_params, opts) => {
606
622
  expect(opts.signal).toBeDefined();
607
623
  expect(opts.signal.aborted).toBe(false);
608
- return { data: { id: 'ses_to' }, error: null, response: { status: 200 } };
624
+ return { id: 'ses_to' };
609
625
  },
610
- promptAsync: async (_params, opts) => {
626
+ prompt: async (_params, opts) => {
611
627
  expect(opts.signal).toBeDefined();
612
- return { data: undefined, error: null, response: { status: 204 } };
628
+ return undefined;
613
629
  },
614
630
  },
615
631
  });
@@ -619,7 +635,7 @@ describe('conversations service', () => {
619
635
  });
620
636
 
621
637
  it('maps AbortError from create to create failure', async () => {
622
- mockCreateOpencodeClient.mockReturnValue({
638
+ mockMake.mockReturnValue({
623
639
  session: {
624
640
  create: async () => { throw new DOMException('Aborted', 'AbortError'); },
625
641
  },
@@ -631,11 +647,11 @@ describe('conversations service', () => {
631
647
  expect(result.error).toBe('Failed to create session');
632
648
  });
633
649
 
634
- it('maps AbortError from promptAsync to ambiguous=true', async () => {
635
- mockCreateOpencodeClient.mockReturnValue({
650
+ it('maps AbortError from prompt to ambiguous=true', async () => {
651
+ mockMake.mockReturnValue({
636
652
  session: {
637
- create: async () => ({ data: { id: 'ses_pto' }, error: null, response: { status: 200 } }),
638
- promptAsync: async () => { throw new DOMException('Aborted', 'AbortError'); },
653
+ create: async () => ({ id: 'ses_pto' }),
654
+ prompt: async () => { throw new DOMException('Aborted', 'AbortError'); },
639
655
  },
640
656
  });
641
657
  const service = createService();
@@ -649,9 +665,9 @@ describe('conversations service', () => {
649
665
  // --- create failure no session ID ---
650
666
 
651
667
  it('returns create failure when no session ID in response', async () => {
652
- mockCreateOpencodeClient.mockReturnValue({
668
+ mockMake.mockReturnValue({
653
669
  session: {
654
- create: async () => ({ data: null, error: null, response: { status: 200 } }),
670
+ create: async () => null,
655
671
  },
656
672
  });
657
673
  const service = createService();
@@ -127,7 +127,8 @@ export function createGlobalMessageStreamHub({
127
127
  buildUrl: () => {
128
128
  buildUrlFailed = false;
129
129
  try {
130
- return new URL(buildOpenCodeUrl('/global/event', ''));
130
+ // v2 serves the event stream at `/api/event`; `/api/global/event` is gone.
131
+ return new URL(buildOpenCodeUrl('/event', ''));
131
132
  } catch {
132
133
  buildUrlFailed = true;
133
134
  throw new Error('OpenCode service unavailable');