@openchambery/web 1.19.11 → 1.19.13-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/dist/assets/{AgentAvatar-CX0a1rKE.js → AgentAvatar-CiQIIM0d.js} +1 -1
- package/dist/assets/AssistantView-YmeyxehG.js +2 -0
- package/dist/assets/{DiagramView-BQO1cBLs.js → DiagramView-BpZb-G9P.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CTlNE4Hg.js → FadeInOnReveal-nf6-ZCrs.js} +1 -1
- package/dist/assets/{Icon-BYM02QyU.js → Icon-CeKHYyKZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-wpeGdciL.js +2 -0
- package/dist/assets/{MarkdownRendererImpl--takeMXN.js → MarkdownRendererImpl-Peu0GVSQ.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-DGSqWcPN.js → MarkstreamRendererImpl-B-odd2Gz.js} +1 -1
- package/dist/assets/MobileDetailNavigation-CtYke_1C.js +1 -0
- package/dist/assets/{MobileShareBridge-D-BUjACe.js → MobileShareBridge-DIfOl8EZ.js} +3 -3
- package/dist/assets/MobileSurface-_KoX75Ff.js +2 -0
- package/dist/assets/{MultiRunWindow-BNR9ZWmD.js → MultiRunWindow-BT5Dwgzu.js} +1 -1
- package/dist/assets/{OnboardingScreen-DS2nugLX.js → OnboardingScreen-BPusUgFG.js} +1 -1
- package/dist/assets/{OverlayScrollbar-KUpvYHdv.js → OverlayScrollbar-DUghqFGg.js} +1 -1
- package/dist/assets/{SettingsView-DGerg-C2.js → SettingsView-BwQpbIZl.js} +11 -11
- package/dist/assets/{SettingsWindow-CSpd5JbJ.js → SettingsWindow-DfEGfyr8.js} +1 -1
- package/dist/assets/{TerminalView-DS-oUxJr.js → TerminalView-8402vpcO.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Q1k9-IaY.js → ToolOutputDialog-B5RdkhsG.js} +1 -1
- package/dist/assets/{appThemeRegistry-CG7D-3-q.js → appThemeRegistry-CIZ-shMD.js} +1 -1
- package/dist/assets/{button-C0cth9tl.js → button-DFPOdZ0g.js} +1 -1
- package/dist/assets/{clipboard-DJ4nQEuY.js → clipboard-C05anBwU.js} +1 -1
- package/dist/assets/{collapsible-DFT9cE80.js → collapsible-oGbEvbe1.js} +1 -1
- package/dist/assets/{context-menu-CAp74NfV.js → context-menu-DGz4_2hs.js} +1 -1
- package/dist/assets/{desktopHosts-Dc2PS66M.js → desktopHosts-BVJXQq1S.js} +1 -1
- package/dist/assets/{desktopRelayRestore-T9y0orqa.js → desktopRelayRestore-RDfuj1M7.js} +1 -1
- package/dist/assets/{diagram-BryzMuta.js → diagram-Csk5GpDC.js} +1 -1
- package/dist/assets/{dialog-CgbV5ljg.js → dialog-rrqB-H6q.js} +1 -1
- package/dist/assets/{es-BHqnSjoZ.js → es-FE0sJ9EC.js} +1 -1
- package/dist/assets/{fileQueries-BY-9OWzp.js → fileQueries-BcdWOPlD.js} +1 -1
- package/dist/assets/{fr-BfrlJY9i.js → fr-DNDCSfGt.js} +1 -1
- package/dist/assets/{gitApi-Cr-joUEw.js → gitApi-_P0FsoXe.js} +1 -1
- package/dist/assets/{input-DFLpqjGR.js → input-BO3r20op.js} +1 -1
- package/dist/assets/{insertionBoundaries-X0xbikAA.js → insertionBoundaries-CWoioTdE.js} +1 -1
- package/dist/assets/{ja-DP-7qt-x.js → ja-BDwrk0cM.js} +1 -1
- package/dist/assets/{ko-E2B5vGQP.js → ko-BeeU2o4I.js} +1 -1
- package/dist/assets/{main-D0L7fIg3.js → main-CLj_WS1o.js} +2 -2
- package/dist/assets/{main-BI7fNQoi.js → main-CnSZ89pD.js} +4 -4
- package/dist/assets/{markdown-worker-CTsrtpPh.js → markdown-worker-CZWE1F1M.js} +1 -1
- package/dist/assets/{markdownHeightCache-CYDlisxr.js → markdownHeightCache-Cdd3hkMl.js} +1 -1
- package/dist/assets/{markdownTheme-DyJ4Gfcj.js → markdownTheme-DIB8WVmz.js} +1 -1
- package/dist/assets/miniChat-B9c3y2pD.js +2 -0
- package/dist/assets/{mobile-OcCu_0mG.js → mobile-DvbXUfNS.js} +2 -2
- package/dist/assets/{multirun-B0G_BEIN.js → multirun-CxhTsmRq.js} +1 -1
- package/dist/assets/{persistence-DdUac14I.js → persistence-CVTWFaqe.js} +2 -2
- package/dist/assets/{pl-r8X5gRM5.js → pl-BYuzpcI5.js} +1 -1
- package/dist/assets/{platform-B4KKLnZ5.js → platform-BY_Gtsqr.js} +1 -1
- package/dist/assets/{projectMeta-BNOzn1QS.js → projectMeta-Dl5iPCB6.js} +1 -1
- package/dist/assets/{pt-BR-DcpdXCf5.js → pt-BR-Bn7cwEzv.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BREc3uhX.js → renderElectronMiniChatApp-CkJ51qCh.js} +2 -2
- package/dist/assets/renderMobileApp-DcjQPhWw.js +28 -0
- package/dist/assets/{runtimeConfig-P8nVMYzE.js → runtimeConfig-Bsk2kWsW.js} +1 -1
- package/dist/assets/runtimeEndpointReset-BBLFgl85.js +147 -0
- package/dist/assets/{sessionChangeSummary-hVby3sJk.js → sessionChangeSummary-DDVT7O5J.js} +1 -1
- package/dist/assets/{sessionLookup-C_9-9ggp.js → sessionLookup-C8VZn0gJ.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-C8_9kHqu.js → sortable-tabs-strip-s7UUxsa6.js} +1 -1
- package/dist/assets/src-BpHDMgud.css +2 -0
- package/dist/assets/{store-BDFdnk6l.js → store-n94dSb3M.js} +2 -2
- package/dist/assets/{terminalApi-947l7Ra3.js → terminalApi-C54_rKEK.js} +1 -1
- package/dist/assets/{textarea-Cgo_nWUU.js → textarea-BjX-KhMN.js} +1 -1
- package/dist/assets/{tooltip-BifLJ8o0.js → tooltip-CQGqGAG_.js} +1 -1
- package/dist/assets/{uk-Di0vl9V-.js → uk-CwpbHEa3.js} +1 -1
- package/dist/assets/{url-Pk6tQ1EZ.js → url-BMjqccCo.js} +1 -1
- package/dist/assets/{useAppFontEffects-CEmOsrr6.js → useAppFontEffects-BIFw36SA.js} +5 -5
- package/dist/assets/{useAssistantUIStore-me6s63b2.js → useAssistantUIStore-Cg9bsT4b.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-CxaK-zXh.js → useDesktopWindowControlsLayout-DoygEcN4.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-BIwv5r5l.js → useEffectiveDirectory-DJSYYkVp.js} +1 -1
- package/dist/assets/{useFontPreferences-BCOBlaAd.js → useFontPreferences-B5_apbBC.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DHm5kZPx.js → useMobileNavigationStore-DuTu0Mzh.js} +5 -5
- package/dist/assets/{useSessionAutoCleanup-CPLP9KWH.js → useSessionAutoCleanup-C-L9teva.js} +1 -1
- package/dist/assets/{useUIStore-CuaA8-Hp.js → useUIStore-DwpQTjLO.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-CZ4ZTWEH.js → useWorkerHighlightedLines-DxTfgdEi.js} +1 -1
- package/dist/assets/{utils-BiK3tBGM.js → utils-81nUI_xp.js} +1 -1
- package/dist/assets/{zh-CN-coFCfeQE.js → zh-CN-BpzFZ-LU.js} +1 -1
- package/dist/assets/{zh-TW-oDRZ1-yb.js → zh-TW-Bopgc5Eo.js} +1 -1
- package/dist/index.html +9 -9
- package/dist/mini-chat.html +9 -9
- package/dist/mobile.html +9 -9
- package/package.json +1 -1
- package/server/index.js +19 -0
- package/server/lib/assistants/DOCUMENTATION.md +21 -6
- package/server/lib/assistants/assign.js +455 -30
- package/server/lib/assistants/assign.test.js +351 -0
- package/server/lib/assistants/contact-active-turn.http.test.js +134 -0
- package/server/lib/assistants/contact-store.js +64 -0
- package/server/lib/assistants/contact-store.test.js +12 -0
- package/server/lib/assistants/contact-tools.js +216 -25
- package/server/lib/assistants/contact-tools.test.js +167 -0
- package/server/lib/assistants/contracts.js +2 -2
- package/server/lib/assistants/harness.js +20 -3
- package/server/lib/assistants/harness.test.js +120 -1
- package/server/lib/assistants/pi-tools.js +50 -3
- package/server/lib/assistants/pi-tools.test.js +25 -0
- package/server/lib/assistants/service.js +320 -27
- package/server/lib/assistants/service.test.js +581 -9
- package/server/lib/llm/DOCUMENTATION.md +31 -18
- package/server/lib/llm/catalog.js +91 -16
- package/server/lib/llm/catalog.test.js +152 -11
- package/server/lib/llm/generate.js +43 -7
- package/server/lib/llm/generate.test.js +98 -1
- package/server/lib/llm/temp-directory.js +34 -13
- package/server/lib/llm/temp-directory.test.js +57 -0
- package/server/lib/notifications/APNS.md +20 -4
- package/server/lib/notifications/DOCUMENTATION.md +5 -0
- package/server/lib/notifications/apns-runtime.js +136 -18
- package/server/lib/notifications/apns-runtime.test.js +128 -0
- package/server/lib/notifications/apns-titles.js +21 -1
- package/server/lib/notifications/live-activity-refresh-runtime.js +185 -0
- package/server/lib/notifications/live-activity-refresh-runtime.test.js +242 -0
- package/server/lib/notifications/runtime.js +93 -2
- package/server/lib/notifications/runtime.test.js +167 -4
- package/server/lib/opencode/settings-helpers.js +12 -0
- package/server/lib/opencode/settings-runtime.js +16 -0
- package/server/lib/opencode/settings-runtime.test.js +21 -0
- package/server/lib/scheduled-tasks/runtime.js +21 -0
- package/server/lib/scheduled-tasks/runtime.test.js +24 -0
- package/dist/assets/AssistantView-BwrcqJzu.js +0 -2
- package/dist/assets/MarkdownRenderer-C49bfcHx.js +0 -2
- package/dist/assets/MobileDetailNavigation-LZU2vFAs.js +0 -1
- package/dist/assets/MobileSurface-OAJjAQCZ.js +0 -2
- package/dist/assets/miniChat-BJ9AclZX.js +0 -2
- package/dist/assets/renderMobileApp-C4igZ42L.js +0 -28
- package/dist/assets/runtimeEndpointReset-6lVRspis.js +0 -147
- package/dist/assets/src-B-P5qIBc.css +0 -2
|
@@ -3,10 +3,13 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
export class AssignError extends Error {
|
|
6
|
-
constructor(code, message) {
|
|
6
|
+
constructor(code, message, details = {}) {
|
|
7
7
|
super(message);
|
|
8
8
|
this.name = 'AssignError';
|
|
9
9
|
this.code = code;
|
|
10
|
+
if (details.ambiguous === true) this.ambiguous = true;
|
|
11
|
+
if (typeof details.sessionID === 'string' && details.sessionID) this.sessionID = details.sessionID;
|
|
12
|
+
if (typeof details.messageID === 'string' && details.messageID) this.messageID = details.messageID;
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -16,12 +19,24 @@ export const ASSIGN_CODES = Object.freeze({
|
|
|
16
19
|
WORKTREE_NOT_FOUND: 'worktree_not_found',
|
|
17
20
|
VALIDATION: 'validation_error',
|
|
18
21
|
UPSTREAM: 'upstream_error',
|
|
22
|
+
MODEL_NOT_FOUND: 'model_not_found',
|
|
23
|
+
MODEL_AMBIGUOUS: 'model_ambiguous',
|
|
24
|
+
IMAGE_NOT_SUPPORTED: 'image_not_supported',
|
|
25
|
+
NO_PROVIDER: 'no_provider',
|
|
26
|
+
PROMPT_AMBIGUOUS: 'prompt_ambiguous',
|
|
19
27
|
});
|
|
20
28
|
|
|
21
29
|
export const PROJECT_REQUIRED_MESSAGE = 'No registered project is configured. Add a project in Settings before assigning work. Do not use assistant-workspaces.';
|
|
22
30
|
|
|
31
|
+
/** Bounded deadlines for worker session side effects (Promise.race + optional signal). */
|
|
32
|
+
export const ASSIGN_CREATE_TIMEOUT_MS = 30_000;
|
|
33
|
+
export const ASSIGN_PROMPT_TIMEOUT_MS = 45_000;
|
|
34
|
+
export const ASSIGN_DELETE_TIMEOUT_MS = 10_000;
|
|
35
|
+
export const ASSIGN_LOOKUP_TIMEOUT_MS = 8_000;
|
|
36
|
+
|
|
23
37
|
const contained = (candidate, root) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
24
38
|
|
|
39
|
+
/** Soft trim for optional path/title fields — missing stays null. */
|
|
25
40
|
const trim = (value, max = 10_000) => {
|
|
26
41
|
if (typeof value !== 'string') return null;
|
|
27
42
|
const next = value.trim();
|
|
@@ -29,11 +44,93 @@ const trim = (value, max = 10_000) => {
|
|
|
29
44
|
return next;
|
|
30
45
|
};
|
|
31
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Fail-closed string when the caller *provided* a field (not undefined).
|
|
49
|
+
* Objects, blank strings, and over-max lengths throw — never silent null fallback.
|
|
50
|
+
*/
|
|
51
|
+
export function requireProvidedString(value, { field, max }) {
|
|
52
|
+
if (value === undefined) return null;
|
|
53
|
+
if (value === null) {
|
|
54
|
+
throw new AssignError(
|
|
55
|
+
ASSIGN_CODES.VALIDATION,
|
|
56
|
+
`assign_session ${field} must be a non-empty string when provided.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (typeof value !== 'string') {
|
|
60
|
+
throw new AssignError(
|
|
61
|
+
ASSIGN_CODES.VALIDATION,
|
|
62
|
+
`assign_session ${field} must be a string (got ${typeof value}).`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const next = value.trim();
|
|
66
|
+
if (!next) {
|
|
67
|
+
throw new AssignError(
|
|
68
|
+
ASSIGN_CODES.VALIDATION,
|
|
69
|
+
`assign_session ${field} must be a non-empty string when provided.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (next.length > max) {
|
|
73
|
+
throw new AssignError(
|
|
74
|
+
ASSIGN_CODES.VALIDATION,
|
|
75
|
+
`assign_session ${field} exceeds ${max} characters.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
32
81
|
const promptAdmitted = (result) => (
|
|
33
82
|
!result?.error
|
|
34
83
|
&& (result?.response?.status === 204 || result?.status === 204 || result?.data !== undefined || result?.response?.ok === true)
|
|
35
84
|
);
|
|
36
85
|
|
|
86
|
+
const isTransportError = (error) => {
|
|
87
|
+
if (!error) return false;
|
|
88
|
+
if (error.name === 'TypeError' || error.name === 'FetchError' || error.name === 'AbortError') return true;
|
|
89
|
+
const code = error.cause?.code || error.code;
|
|
90
|
+
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'EPIPE' || code === 'ABORT_ERR') {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (error.timedOut === true) return true;
|
|
94
|
+
if (typeof error.message === 'string' && /timed out|aborted|network|fetch failed/i.test(error.message)) return true;
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const resultHttpStatus = (result) => {
|
|
99
|
+
const status = result?.response?.status
|
|
100
|
+
?? result?.error?.status
|
|
101
|
+
?? result?.error?.statusCode
|
|
102
|
+
?? result?.status;
|
|
103
|
+
return Number.isFinite(status) ? status : null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Ambiguous: transport/timeout/5xx/no status — prompt may already be admitted upstream. */
|
|
107
|
+
export function isAmbiguousPromptFailure(error, result) {
|
|
108
|
+
if (error && isTransportError(error)) return true;
|
|
109
|
+
if (error?.timedOut === true) return true;
|
|
110
|
+
if (!result && error) return isTransportError(error);
|
|
111
|
+
if (!result) return true;
|
|
112
|
+
if (promptAdmitted(result)) return false;
|
|
113
|
+
const status = resultHttpStatus(result);
|
|
114
|
+
if (status == null) return true;
|
|
115
|
+
return status === 408 || status === 429 || status >= 500;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function raceDeadline(work, ms, label) {
|
|
119
|
+
let timer;
|
|
120
|
+
const timeout = new Promise((_, reject) => {
|
|
121
|
+
timer = setTimeout(() => {
|
|
122
|
+
const error = new Error(`${label} timed out after ${ms}ms`);
|
|
123
|
+
error.code = ASSIGN_CODES.UPSTREAM;
|
|
124
|
+
error.timedOut = true;
|
|
125
|
+
reject(error);
|
|
126
|
+
}, ms);
|
|
127
|
+
timer.unref?.();
|
|
128
|
+
});
|
|
129
|
+
return Promise.race([work, timeout]).finally(() => {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
37
134
|
function normalizeProjectRoots(allowedRoots = []) {
|
|
38
135
|
const seen = new Set();
|
|
39
136
|
const roots = [];
|
|
@@ -131,11 +228,253 @@ export function resolveAssignDirectory({
|
|
|
131
228
|
return resolveExisting(match.path);
|
|
132
229
|
}
|
|
133
230
|
|
|
231
|
+
/** Safe OpenCode file parts only — no base64 logging, no path inventing. */
|
|
232
|
+
export function sanitizeAssignFileParts(parts = []) {
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const part of Array.isArray(parts) ? parts : []) {
|
|
235
|
+
if (!part || typeof part !== 'object' || part.type !== 'file') continue;
|
|
236
|
+
const mime = trim(part.mime, 256);
|
|
237
|
+
const url = typeof part.url === 'string' && part.url.trim() ? part.url.trim() : '';
|
|
238
|
+
if (!mime || !url) continue;
|
|
239
|
+
const filename = trim(part.filename, 512);
|
|
240
|
+
out.push(filename ? { type: 'file', mime, url, filename } : { type: 'file', mime, url });
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Stable attachment fingerprint for same-turn assign dedup (no file bodies). */
|
|
246
|
+
export function attachmentScopeKey(parts = []) {
|
|
247
|
+
return sanitizeAssignFileParts(parts).map((part) => (
|
|
248
|
+
crypto.createHash('sha256').update(JSON.stringify(part)).digest('hex')
|
|
249
|
+
));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function buildAssignParts({ prompt, fileParts = [] }) {
|
|
253
|
+
const text = trim(prompt, 200_000);
|
|
254
|
+
if (!text) {
|
|
255
|
+
throw new AssignError(ASSIGN_CODES.VALIDATION, 'assign_session requires a coding prompt for the worker session.');
|
|
256
|
+
}
|
|
257
|
+
const files = sanitizeAssignFileParts(fileParts);
|
|
258
|
+
return [{ type: 'text', text }, ...files];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function hasAssignImageParts(parts = []) {
|
|
262
|
+
return sanitizeAssignFileParts(parts).some((part) => String(part.mime).toLowerCase().startsWith('image/'));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolve worker model for assign.
|
|
267
|
+
* - No explicit selection → assistant default (no catalog required).
|
|
268
|
+
* - Explicit selection → must resolve uniquely against the connected catalog; never silent fallback.
|
|
269
|
+
* - Provided but illegal/blank/overlong/conflicting fields fail closed (validation_error).
|
|
270
|
+
*/
|
|
271
|
+
export function resolveAssignWorkerModel({
|
|
272
|
+
providerID,
|
|
273
|
+
modelID,
|
|
274
|
+
model,
|
|
275
|
+
fallback = {},
|
|
276
|
+
catalog = null,
|
|
277
|
+
} = {}) {
|
|
278
|
+
const explicitProvider = requireProvidedString(providerID, { field: 'providerID', max: 256 });
|
|
279
|
+
const explicitModel = requireProvidedString(modelID, { field: 'modelID', max: 256 });
|
|
280
|
+
const combined = requireProvidedString(model, { field: 'model', max: 512 });
|
|
281
|
+
const hasExplicit = Boolean(explicitProvider || explicitModel || combined);
|
|
282
|
+
|
|
283
|
+
const fallbackProvider = trim(fallback.providerID, 256);
|
|
284
|
+
const fallbackModel = trim(fallback.modelID, 256);
|
|
285
|
+
|
|
286
|
+
if (!hasExplicit) {
|
|
287
|
+
if (!fallbackProvider || !fallbackModel) {
|
|
288
|
+
throw new AssignError(ASSIGN_CODES.VALIDATION, 'Assistant is missing a connected provider/model.');
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
providerID: fallbackProvider,
|
|
292
|
+
modelID: fallbackModel,
|
|
293
|
+
source: 'assistant',
|
|
294
|
+
name: null,
|
|
295
|
+
acceptsImages: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!catalog || !Array.isArray(catalog.models)) {
|
|
300
|
+
throw new AssignError(
|
|
301
|
+
ASSIGN_CODES.UPSTREAM,
|
|
302
|
+
'Connected model catalog is unavailable. Cannot resolve an explicit worker model.',
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let wantedProvider = explicitProvider;
|
|
307
|
+
let wantedModel = explicitModel;
|
|
308
|
+
let fromCombinedProvider = null;
|
|
309
|
+
let fromCombinedModel = null;
|
|
310
|
+
|
|
311
|
+
if (combined) {
|
|
312
|
+
if (combined.includes('/')) {
|
|
313
|
+
const slash = combined.indexOf('/');
|
|
314
|
+
fromCombinedProvider = combined.slice(0, slash).trim();
|
|
315
|
+
fromCombinedModel = combined.slice(slash + 1).trim();
|
|
316
|
+
if (!fromCombinedProvider || !fromCombinedModel) {
|
|
317
|
+
throw new AssignError(
|
|
318
|
+
ASSIGN_CODES.VALIDATION,
|
|
319
|
+
'assign_session model must be provider/modelID when it contains a slash.',
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
} else if (!wantedModel) {
|
|
323
|
+
wantedModel = combined;
|
|
324
|
+
} else {
|
|
325
|
+
// model without slash while modelID already set — treat as conflicting alias only if different
|
|
326
|
+
if (combined !== wantedModel && combined.toLowerCase() !== wantedModel.toLowerCase()) {
|
|
327
|
+
throw new AssignError(
|
|
328
|
+
ASSIGN_CODES.VALIDATION,
|
|
329
|
+
'assign_session model conflicts with modelID.',
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (fromCombinedProvider && fromCombinedModel) {
|
|
336
|
+
if (wantedProvider && wantedProvider !== fromCombinedProvider) {
|
|
337
|
+
throw new AssignError(
|
|
338
|
+
ASSIGN_CODES.VALIDATION,
|
|
339
|
+
'assign_session providerID conflicts with model provider/modelID.',
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (wantedModel && wantedModel !== fromCombinedModel) {
|
|
343
|
+
throw new AssignError(
|
|
344
|
+
ASSIGN_CODES.VALIDATION,
|
|
345
|
+
'assign_session modelID conflicts with model provider/modelID.',
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
wantedProvider = wantedProvider || fromCombinedProvider;
|
|
349
|
+
wantedModel = wantedModel || fromCombinedModel;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (wantedProvider && !wantedModel && !fromCombinedModel && !combined) {
|
|
353
|
+
// provider only — may be ambiguous across that provider's models
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const models = catalog.models.filter((entry) => entry && typeof entry === 'object');
|
|
357
|
+
const matches = models.filter((entry) => {
|
|
358
|
+
const entryProvider = trim(entry.providerID, 256);
|
|
359
|
+
const entryModel = trim(entry.modelID, 256);
|
|
360
|
+
const entryName = trim(entry.name, 512);
|
|
361
|
+
if (!entryProvider || !entryModel) return false;
|
|
362
|
+
if (wantedProvider && wantedModel) {
|
|
363
|
+
return entryProvider === wantedProvider && entryModel === wantedModel;
|
|
364
|
+
}
|
|
365
|
+
if (wantedProvider && !wantedModel) {
|
|
366
|
+
return entryProvider === wantedProvider;
|
|
367
|
+
}
|
|
368
|
+
if (!wantedProvider && wantedModel) {
|
|
369
|
+
const needle = wantedModel.toLowerCase();
|
|
370
|
+
return entryModel.toLowerCase() === needle
|
|
371
|
+
|| (entryName && entryName.toLowerCase() === needle)
|
|
372
|
+
|| `${entryProvider}/${entryModel}`.toLowerCase() === needle;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (matches.length === 0) {
|
|
378
|
+
const label = wantedProvider && wantedModel
|
|
379
|
+
? `${wantedProvider}/${wantedModel}`
|
|
380
|
+
: (wantedModel || wantedProvider || combined || 'model');
|
|
381
|
+
throw new AssignError(
|
|
382
|
+
ASSIGN_CODES.MODEL_NOT_FOUND,
|
|
383
|
+
`No connected OpenCode model matches ${label}. Use a model from the connected catalog.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (matches.length > 1) {
|
|
387
|
+
const options = matches
|
|
388
|
+
.slice(0, 8)
|
|
389
|
+
.map((entry) => `${entry.providerID}/${entry.modelID}`)
|
|
390
|
+
.join(', ');
|
|
391
|
+
throw new AssignError(
|
|
392
|
+
ASSIGN_CODES.MODEL_AMBIGUOUS,
|
|
393
|
+
`Several connected models match that selection (${options}). Pass providerID and modelID.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const chosen = matches[0];
|
|
398
|
+
return {
|
|
399
|
+
providerID: chosen.providerID,
|
|
400
|
+
modelID: chosen.modelID,
|
|
401
|
+
source: 'explicit',
|
|
402
|
+
name: typeof chosen.name === 'string' ? chosen.name : null,
|
|
403
|
+
acceptsImages: chosen.acceptsImages === true,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Variant strategy for the worker prompt:
|
|
409
|
+
* - Explicit tool variant wins (fail-closed if illegal).
|
|
410
|
+
* - Default/same-as-assistant model may reuse the contact assistant variant.
|
|
411
|
+
* - Cross-model explicit worker never reuses the contact variant (unsupported elsewhere).
|
|
412
|
+
*/
|
|
413
|
+
export function resolveAssignWorkerVariant({
|
|
414
|
+
source,
|
|
415
|
+
workerProviderID,
|
|
416
|
+
workerModelID,
|
|
417
|
+
assistant = {},
|
|
418
|
+
variant,
|
|
419
|
+
} = {}) {
|
|
420
|
+
if (variant !== undefined) {
|
|
421
|
+
if (variant === null) return null;
|
|
422
|
+
return requireProvidedString(variant, { field: 'variant', max: 256 });
|
|
423
|
+
}
|
|
424
|
+
const assistantVariant = trim(assistant.variant, 256);
|
|
425
|
+
if (!assistantVariant) return null;
|
|
426
|
+
const sameModel = trim(assistant.providerID, 256) === trim(workerProviderID, 256)
|
|
427
|
+
&& trim(assistant.modelID, 256) === trim(workerModelID, 256);
|
|
428
|
+
if (source === 'assistant' || sameModel) return assistantVariant;
|
|
429
|
+
// Cross-model: do not forward a contact-only variant onto a different worker model.
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function assertAssignFileCapability({ fileParts = [], acceptsImages = null } = {}) {
|
|
434
|
+
if (!hasAssignImageParts(fileParts)) return;
|
|
435
|
+
if (acceptsImages === true) return;
|
|
436
|
+
throw new AssignError(
|
|
437
|
+
ASSIGN_CODES.IMAGE_NOT_SUPPORTED,
|
|
438
|
+
'The worker model does not accept images. Choose a vision-capable connected model, or assign without image attachments.',
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function bestEffortDeleteSession(deleteSession, { sessionID, directory }) {
|
|
443
|
+
if (typeof deleteSession !== 'function' || !sessionID) return;
|
|
444
|
+
try {
|
|
445
|
+
await raceDeadline(
|
|
446
|
+
deleteSession({ sessionID, directory }),
|
|
447
|
+
ASSIGN_DELETE_TIMEOUT_MS,
|
|
448
|
+
'assign session.delete',
|
|
449
|
+
);
|
|
450
|
+
} catch {
|
|
451
|
+
// Cleanup is best-effort; surface the original prompt failure.
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function lookupAdmittedMessage(lookupMessage, { sessionID, messageID, directory }) {
|
|
456
|
+
if (typeof lookupMessage !== 'function' || !sessionID || !messageID) return null;
|
|
457
|
+
try {
|
|
458
|
+
const found = await raceDeadline(
|
|
459
|
+
lookupMessage({ sessionID, messageID, directory }),
|
|
460
|
+
ASSIGN_LOOKUP_TIMEOUT_MS,
|
|
461
|
+
'assign message lookup',
|
|
462
|
+
);
|
|
463
|
+
if (found === true) return true;
|
|
464
|
+
if (found && typeof found === 'object' && (found.admitted === true || found.found === true || found.id || found.messageID)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
} catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
134
473
|
export async function assignSession(input = {}) {
|
|
135
|
-
|
|
136
|
-
if (!prompt) {
|
|
474
|
+
if (input.prompt === undefined || input.prompt === null) {
|
|
137
475
|
throw new AssignError(ASSIGN_CODES.VALIDATION, 'assign_session requires a coding prompt for the worker session.');
|
|
138
476
|
}
|
|
477
|
+
const codingPrompt = requireProvidedString(input.prompt, { field: 'prompt', max: 200_000 });
|
|
139
478
|
|
|
140
479
|
const projectDirectory = resolveAssignDirectory({
|
|
141
480
|
projectPath: input.projectPath,
|
|
@@ -164,17 +503,59 @@ export async function assignSession(input = {}) {
|
|
|
164
503
|
}
|
|
165
504
|
|
|
166
505
|
const assistant = input.assistant && typeof input.assistant === 'object' ? input.assistant : {};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
506
|
+
let resolvedModel;
|
|
507
|
+
if (input.model && typeof input.model === 'object' && !Array.isArray(input.model)) {
|
|
508
|
+
const objectProvider = trim(input.model.providerID, 256);
|
|
509
|
+
const objectModel = trim(input.model.modelID, 256);
|
|
510
|
+
if (!objectProvider || !objectModel) {
|
|
511
|
+
throw new AssignError(
|
|
512
|
+
ASSIGN_CODES.VALIDATION,
|
|
513
|
+
'assign_session model object requires providerID and modelID.',
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
resolvedModel = {
|
|
517
|
+
providerID: objectProvider,
|
|
518
|
+
modelID: objectModel,
|
|
519
|
+
source: input.model.source || 'explicit',
|
|
520
|
+
acceptsImages: input.model.acceptsImages === true ? true : input.model.acceptsImages === false ? false : null,
|
|
521
|
+
};
|
|
522
|
+
} else {
|
|
523
|
+
if (input.model !== undefined && input.model !== null && typeof input.model !== 'string') {
|
|
524
|
+
throw new AssignError(
|
|
525
|
+
ASSIGN_CODES.VALIDATION,
|
|
526
|
+
'assign_session model must be a string or resolved {providerID,modelID} object.',
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
resolvedModel = resolveAssignWorkerModel({
|
|
530
|
+
providerID: input.providerID,
|
|
531
|
+
modelID: input.modelID,
|
|
532
|
+
model: input.model,
|
|
533
|
+
fallback: {
|
|
534
|
+
providerID: assistant.providerID,
|
|
535
|
+
modelID: assistant.modelID,
|
|
536
|
+
},
|
|
537
|
+
catalog: input.catalog,
|
|
538
|
+
});
|
|
171
539
|
}
|
|
172
540
|
|
|
541
|
+
const fileParts = sanitizeAssignFileParts(input.fileParts || input.parts || []);
|
|
542
|
+
assertAssignFileCapability({
|
|
543
|
+
fileParts,
|
|
544
|
+
acceptsImages: input.acceptsImages == null ? resolvedModel.acceptsImages : input.acceptsImages,
|
|
545
|
+
});
|
|
546
|
+
|
|
173
547
|
const sessionID = trim(input.sessionID, 256);
|
|
174
|
-
const title = trim(input.title, 256) ||
|
|
548
|
+
const title = trim(input.title, 256) || codingPrompt.slice(0, 80);
|
|
175
549
|
const messageID = trim(input.messageID, 256) || `msg_assign_${crypto.randomUUID()}`;
|
|
176
|
-
const model = { providerID, modelID };
|
|
177
|
-
const parts =
|
|
550
|
+
const model = { providerID: resolvedModel.providerID, modelID: resolvedModel.modelID };
|
|
551
|
+
const parts = buildAssignParts({ prompt: codingPrompt, fileParts });
|
|
552
|
+
const workerVariant = resolveAssignWorkerVariant({
|
|
553
|
+
source: resolvedModel.source,
|
|
554
|
+
workerProviderID: model.providerID,
|
|
555
|
+
workerModelID: model.modelID,
|
|
556
|
+
assistant,
|
|
557
|
+
variant: input.variant,
|
|
558
|
+
});
|
|
178
559
|
// Subscriber identity lives on `assigned`, not `assistant`.
|
|
179
560
|
// `openchamber.assistant.assistantID` marks a hidden system binding.
|
|
180
561
|
const metadata = {
|
|
@@ -192,34 +573,78 @@ export async function assignSession(input = {}) {
|
|
|
192
573
|
model,
|
|
193
574
|
parts,
|
|
194
575
|
...(trim(assistant.agent, 256) ? { agent: trim(assistant.agent, 256) } : {}),
|
|
195
|
-
...(
|
|
576
|
+
...(workerVariant ? { variant: workerVariant } : {}),
|
|
196
577
|
};
|
|
197
578
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Worker prompt is unavailable.');
|
|
201
|
-
}
|
|
202
|
-
const prompted = await input.promptExisting({ ...promptInput, sessionID });
|
|
203
|
-
if (!promptAdmitted(prompted)) {
|
|
204
|
-
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Failed to submit the prompt to the existing session.');
|
|
205
|
-
}
|
|
206
|
-
return { sessionID, directory, title, status: 'busy', reused: true, messageID, branch: branchName };
|
|
207
|
-
}
|
|
579
|
+
const createTimeoutMs = Number.isFinite(input.createTimeoutMs) ? input.createTimeoutMs : ASSIGN_CREATE_TIMEOUT_MS;
|
|
580
|
+
const promptTimeoutMs = Number.isFinite(input.promptTimeoutMs) ? input.promptTimeoutMs : ASSIGN_PROMPT_TIMEOUT_MS;
|
|
208
581
|
|
|
209
|
-
if (typeof input.
|
|
582
|
+
if (typeof input.promptExisting !== 'function' || (!sessionID && typeof input.createSession !== 'function')) {
|
|
210
583
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Worker session APIs are unavailable.');
|
|
211
584
|
}
|
|
212
585
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
586
|
+
let targetSessionID = sessionID;
|
|
587
|
+
if (!targetSessionID) {
|
|
588
|
+
let created;
|
|
589
|
+
try {
|
|
590
|
+
created = await raceDeadline(
|
|
591
|
+
input.createSession({ directory, title, metadata }),
|
|
592
|
+
createTimeoutMs,
|
|
593
|
+
'assign session.create',
|
|
594
|
+
);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
throw error instanceof AssignError
|
|
597
|
+
? error
|
|
598
|
+
: new AssignError(ASSIGN_CODES.UPSTREAM, error?.timedOut ? error.message : 'Failed to create a coding session.');
|
|
599
|
+
}
|
|
600
|
+
targetSessionID = created?.data?.id || created?.id;
|
|
601
|
+
if (created?.error || !targetSessionID) {
|
|
602
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Failed to create a coding session.');
|
|
603
|
+
}
|
|
217
604
|
}
|
|
218
605
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
606
|
+
let prompted;
|
|
607
|
+
let promptError;
|
|
608
|
+
try {
|
|
609
|
+
prompted = await raceDeadline(
|
|
610
|
+
input.promptExisting({ ...promptInput, sessionID: targetSessionID }),
|
|
611
|
+
promptTimeoutMs,
|
|
612
|
+
'assign promptAsync',
|
|
613
|
+
);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
promptError = error;
|
|
222
616
|
}
|
|
223
617
|
|
|
224
|
-
|
|
618
|
+
let promptStatus = 'admitted';
|
|
619
|
+
if (promptError || !promptAdmitted(prompted)) {
|
|
620
|
+
if (!isAmbiguousPromptFailure(promptError, prompted)) {
|
|
621
|
+
if (!sessionID) await bestEffortDeleteSession(input.deleteSession, { sessionID: targetSessionID, directory });
|
|
622
|
+
throw promptError instanceof AssignError
|
|
623
|
+
? promptError
|
|
624
|
+
: new AssignError(ASSIGN_CODES.UPSTREAM, 'Failed to submit the prompt to the worker session.');
|
|
625
|
+
}
|
|
626
|
+
const found = await lookupAdmittedMessage(input.lookupMessage, { sessionID: targetSessionID, messageID, directory });
|
|
627
|
+
// Keep the worker while admission is unknown; retry uses the same identities.
|
|
628
|
+
if (found !== true) {
|
|
629
|
+
throw new AssignError(
|
|
630
|
+
ASSIGN_CODES.PROMPT_AMBIGUOUS,
|
|
631
|
+
'Worker prompt admission is unknown. Resume with the same sessionID and messageID.',
|
|
632
|
+
{ ambiguous: true, sessionID: targetSessionID, messageID },
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
promptStatus = 'confirmed';
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
sessionID: targetSessionID,
|
|
640
|
+
directory,
|
|
641
|
+
title,
|
|
642
|
+
status: 'busy',
|
|
643
|
+
reused: Boolean(sessionID),
|
|
644
|
+
messageID,
|
|
645
|
+
branch: branchName,
|
|
646
|
+
model,
|
|
647
|
+
attachmentCount: fileParts.length,
|
|
648
|
+
promptStatus,
|
|
649
|
+
};
|
|
225
650
|
}
|