@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.
- package/README.md +13 -11
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +165 -0
- package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
- package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
- package/dist/assets/{DiagramView-C_JfbxNl.js → DiagramView-xuSJTnv7.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
- package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-B1iUwzOz.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-BPtMj3-X.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-CKClHsWN.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-DiA24bnN.js → MultiRunWindow-ChZGob0v.js} +1 -1
- package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
- package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
- package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
- package/dist/assets/{SettingsWindow-XlzRSS7E.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-DdIaaLba.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BbwuLdXU.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-ChzfD7mc.js → appThemeRegistry-COGfxTFV.js} +1 -1
- package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
- package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
- package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
- package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
- package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
- package/dist/assets/es-c52LTHHe.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DyjmxUEN.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
- package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
- package/dist/assets/gitApi-ButwzQ0N.js +9 -0
- package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
- package/dist/assets/ja-C8Ikun3Z.js +15 -0
- package/dist/assets/ko-ClFys5o2.js +15 -0
- package/dist/assets/main-CCE_4y5F.js +2 -0
- package/dist/assets/main-Dn9rpsgl.js +169 -0
- package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
- package/dist/assets/miniChat-BpjxVqgg.js +2 -0
- package/dist/assets/mobile-nMJCaNq1.js +2 -0
- package/dist/assets/{multirun-8fkF0Ji_.js → multirun-BvSG94mW.js} +1 -1
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/{projectMeta--y6tyjrv.js → projectMeta-DOe49dM_.js} +2 -2
- package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
- package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
- package/dist/assets/{runtimeConfig-DYWtBrkW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BSWg99jc.js → runtimeEndpointReset-B6BxhvR0.js} +5 -5
- package/dist/assets/{sessionChangeSummary-BwupJF-Q.js → sessionChangeSummary-VVBlEXuh.js} +1 -1
- package/dist/assets/{sessionLookup-DP2X1qRR.js → sessionLookup-gh-hVRIc.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
- package/dist/assets/{src-DZ60kHBP.css → src-BzX5UI-c.css} +1 -1
- package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
- package/dist/assets/{textarea-BnD799LW.js → textarea-3XR8sUfy.js} +1 -1
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-DWLxpWlS.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/{useDesktopWindowControlsLayout-BGPR_rq6.js → useDesktopWindowControlsLayout-B1GX59-L.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-CbV9_r1s.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
- package/dist/assets/{useSessionAutoCleanup-Dnors7hB.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DQqqrOBU.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-20MYEjha.js +15 -0
- package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +2 -2
- package/server/index.js +107 -0
- package/server/lib/assistants/DOCUMENTATION.md +18 -18
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/projection-history.test.js +207 -0
- package/server/lib/assistants/routes.js +5 -5
- package/server/lib/assistants/service.js +493 -123
- package/server/lib/assistants/service.test.js +203 -133
- package/server/lib/conversations/DOCUMENTATION.md +4 -4
- package/server/lib/conversations/service.js +86 -57
- package/server/lib/conversations/service.test.js +116 -100
- package/server/lib/event-stream/global-hub.js +2 -1
- package/server/lib/llm/DOCUMENTATION.md +71 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +37 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +421 -450
- package/server/lib/llm/generate.test.js +553 -487
- package/server/lib/llm/routes.js +6 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/opencode/DOCUMENTATION.md +28 -5
- package/server/lib/opencode/config-entity-routes.js +84 -13
- package/server/lib/opencode/config-entity-routes.test.js +41 -10
- package/server/lib/opencode/ensure-cli.js +239 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +193 -110
- package/server/lib/opencode/env-runtime.test.js +221 -17
- package/server/lib/opencode/feature-routes-runtime.js +37 -1
- package/server/lib/opencode/lifecycle.js +204 -70
- package/server/lib/opencode/lifecycle.test.js +376 -6
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +87 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/proxy.js +153 -16
- package/server/lib/opencode/proxy.test.js +120 -0
- package/server/lib/opencode/routes.js +25 -32
- package/server/lib/opencode/routes.test.js +51 -52
- package/server/lib/opencode/server-utils-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +6 -7
- package/server/lib/opencode/skill-routes.test.js +36 -5
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +49 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +331 -308
- package/server/lib/scheduled-tasks/runtime.test.js +530 -318
- package/server/lib/session-goal/DOCUMENTATION.md +65 -14
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +231 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +46 -4
- package/server/lib/session-goal/runtime.test.js +62 -1
- package/server/lib/session-index/sync-runtime.js +20 -17
- package/server/lib/session-index/sync-runtime.test.js +9 -8
- package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
- package/server/lib/session-metadata/routes.js +94 -0
- package/server/lib/session-metadata/routes.test.js +111 -0
- package/server/lib/session-metadata/session-metadata-store.js +235 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +204 -71
- package/server/lib/session-turn-pages/routes.test.js +285 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/small-model/DOCUMENTATION.md +5 -4
- package/server/lib/small-model/catalog.js +56 -6
- package/server/lib/small-model/catalog.test.js +47 -0
- package/server/lib/small-model/index.js +3 -3
- package/server/lib/small-model/opencode-session.js +50 -206
- package/server/lib/small-model/opencode-session.test.js +58 -88
- package/server/opencode-proxy.test.js +61 -12
- package/dist/assets/AssistantView-BjvfkSRS.js +0 -3
- package/dist/assets/MarkdownRenderer-Cg82L3fN.js +0 -2
- package/dist/assets/MobileDetailNavigation-s_vvUD_F.js +0 -1
- package/dist/assets/MobileSurface-DHpXHbJp.js +0 -2
- package/dist/assets/SettingsView-l0J7pfYo.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CHpibtno.js +0 -7
- package/dist/assets/insertionBoundaries-Yd1Lorjw.js +0 -1
- package/dist/assets/ja-Bgr8-dOg.js +0 -15
- package/dist/assets/ko-B_Gxh-0l.js +0 -15
- package/dist/assets/main-BoRXxmB3.js +0 -2
- package/dist/assets/main-DCDcdV5o.js +0 -169
- package/dist/assets/miniChat-12fxk5KR.js +0 -2
- package/dist/assets/mobile-CkZem9Ad.js +0 -2
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-COFc0ZDJ.js +0 -2
- package/dist/assets/renderMobileApp-BKzx7JL3.js +0 -28
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useMobileNavigationStore-DrB_awgk.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
|
@@ -1,15 +1,65 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
103
|
-
let upstreamStatus;
|
|
152
|
+
let session;
|
|
104
153
|
const createT = timeoutSignal(CREATE_TIMEOUT_MS);
|
|
105
154
|
try {
|
|
106
|
-
|
|
107
|
-
directory: sanitizedInput.directory,
|
|
155
|
+
session = await client.session.create({
|
|
156
|
+
location: { directory: sanitizedInput.directory },
|
|
108
157
|
...(sanitizedInput.title ? { title: sanitizedInput.title } : {}),
|
|
109
|
-
...(sanitizedInput.
|
|
110
|
-
...(sanitizedInput.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
169
|
-
logger.warn(`[conversations]
|
|
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
|
-
|
|
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
|
|
5
|
-
vi.mock('@opencode
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
312
|
+
mockMake.mockReturnValue({
|
|
299
313
|
session: {
|
|
300
|
-
create: async () =>
|
|
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
|
-
|
|
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('
|
|
329
|
-
|
|
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).
|
|
333
|
-
expect(params.model).
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
364
|
+
// --- prompt usage ---
|
|
347
365
|
|
|
348
|
-
it('uses
|
|
349
|
-
let
|
|
350
|
-
|
|
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 () => ({
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
expect(params.delivery).
|
|
356
|
-
return {
|
|
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(
|
|
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 +
|
|
396
|
+
it('returns success when create + prompt succeed', async () => {
|
|
369
397
|
const sessionData = { id: 'ses_created', title: 'Test' };
|
|
370
|
-
|
|
398
|
+
mockMake.mockReturnValue({
|
|
371
399
|
session: {
|
|
372
400
|
create: async (params) => {
|
|
373
|
-
expect(params.
|
|
374
|
-
|
|
401
|
+
expect(params.location).toEqual({ directory: '/test' });
|
|
402
|
+
expect(params.model).toEqual({ id: 'gpt-4o', providerID: 'openai' });
|
|
403
|
+
return sessionData;
|
|
375
404
|
},
|
|
376
|
-
|
|
405
|
+
prompt: async (params) => {
|
|
377
406
|
expect(params.sessionID).toBe('ses_created');
|
|
378
|
-
expect(params.
|
|
379
|
-
expect(params.
|
|
380
|
-
expect(params.
|
|
381
|
-
|
|
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
|
|
394
|
-
|
|
424
|
+
it('passes agent, variant to session.create and not prompt', async () => {
|
|
425
|
+
mockMake.mockReturnValue({
|
|
395
426
|
session: {
|
|
396
|
-
create: async () =>
|
|
397
|
-
promptAsync: async (params) => {
|
|
427
|
+
create: async (params) => {
|
|
398
428
|
expect(params.agent).toBe('builder');
|
|
399
|
-
expect(params.
|
|
400
|
-
return {
|
|
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
|
-
|
|
451
|
+
mockMake.mockReturnValue({
|
|
416
452
|
session: {
|
|
417
|
-
create: async () =>
|
|
418
|
-
|
|
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
|
-
|
|
468
|
+
mockMake.mockReturnValue({
|
|
437
469
|
session: {
|
|
438
|
-
create: async () => ({
|
|
439
|
-
|
|
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
|
-
|
|
487
|
+
mockMake.mockReturnValue({
|
|
460
488
|
session: {
|
|
461
|
-
create: async () =>
|
|
462
|
-
|
|
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
|
-
|
|
504
|
+
mockMake.mockReturnValue({
|
|
481
505
|
session: {
|
|
482
|
-
create: async () => ({
|
|
483
|
-
|
|
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
|
-
|
|
520
|
+
mockMake.mockReturnValue({
|
|
497
521
|
session: {
|
|
498
|
-
create: async () => ({
|
|
499
|
-
|
|
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
|
-
|
|
534
|
+
mockMake.mockReturnValue({
|
|
515
535
|
session: {
|
|
516
|
-
create: async () => ({
|
|
517
|
-
|
|
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
|
-
|
|
548
|
+
mockMake.mockReturnValue({
|
|
533
549
|
session: {
|
|
534
|
-
create: async () => ({
|
|
535
|
-
|
|
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
|
-
|
|
571
|
+
mockMake.mockReturnValue({
|
|
556
572
|
session: {
|
|
557
|
-
create: async () => ({
|
|
558
|
-
|
|
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
|
-
|
|
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 () => ({
|
|
577
|
-
|
|
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
|
-
|
|
604
|
+
mockMake.mockReturnValue({
|
|
589
605
|
session: {
|
|
590
|
-
create: async () => { createCalled = true; return {
|
|
591
|
-
|
|
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
|
-
|
|
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 {
|
|
624
|
+
return { id: 'ses_to' };
|
|
609
625
|
},
|
|
610
|
-
|
|
626
|
+
prompt: async (_params, opts) => {
|
|
611
627
|
expect(opts.signal).toBeDefined();
|
|
612
|
-
return
|
|
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
|
-
|
|
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
|
|
635
|
-
|
|
650
|
+
it('maps AbortError from prompt to ambiguous=true', async () => {
|
|
651
|
+
mockMake.mockReturnValue({
|
|
636
652
|
session: {
|
|
637
|
-
create: async () => ({
|
|
638
|
-
|
|
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
|
-
|
|
668
|
+
mockMake.mockReturnValue({
|
|
653
669
|
session: {
|
|
654
|
-
create: async () =>
|
|
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
|
-
|
|
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');
|