@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
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
6
6
|
import { validAssistantDeliveryParts } from '../assistant-delivery-parts.js';
|
|
7
7
|
import { reduceBackfillState } from './history-state.js';
|
|
8
8
|
import { createContactMemoryReader } from './memory.js';
|
|
@@ -117,22 +117,98 @@ export class AssistantError extends Error {
|
|
|
117
117
|
const fail = (code, message) => { throw new AssistantError(code, message); };
|
|
118
118
|
const string = (value, max = 10_000, required = false) => { if (value == null && !required) return null; if (typeof value !== 'string' || value.length > max || (required && !value.trim())) fail('validation_error'); return value.trim(); };
|
|
119
119
|
const nonEmptyString = (value, max = 10_000) => typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
120
|
-
const isMissing = (result) =>
|
|
120
|
+
const isMissing = (result) => {
|
|
121
|
+
if (!result || typeof result !== 'object') return false;
|
|
122
|
+
if (result.error?.status === 404 || result.error?.statusCode === 404 || result.status === 404) return true;
|
|
123
|
+
const code = result.error?.code ?? result.error?.type ?? result.error?._tag ?? result._tag ?? result.code;
|
|
124
|
+
return code === 'not_found' || code === 'SessionNotFoundError' || code === 'MessageNotFoundError';
|
|
125
|
+
};
|
|
121
126
|
const messagesErrorStatus = (result) => { const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status; return Number.isFinite(status) ? status : null; };
|
|
127
|
+
const getHttpStatus = (error) => {
|
|
128
|
+
const candidates = [error?.cause?.status, error?.status, error?.response?.status];
|
|
129
|
+
for (const value of candidates) { if (Number.isFinite(value)) return value; }
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
// Real @opencode/client throws declared JSON with `_tag` (no HTTP status).
|
|
133
|
+
const isMissingError = (error) => {
|
|
134
|
+
if (getHttpStatus(error) === 404 || isMissing(error)) return true;
|
|
135
|
+
if (!error || typeof error !== 'object') return false;
|
|
136
|
+
const tag = error._tag ?? error.name ?? error.code ?? error.type ?? error.error?._tag;
|
|
137
|
+
return tag === 'SessionNotFoundError' || tag === 'MessageNotFoundError' || tag === 'not_found';
|
|
138
|
+
};
|
|
139
|
+
const ASSISTANT_SUCCESS_FINISH = new Set(['stop']);
|
|
140
|
+
const ASSISTANT_ERROR_FINISH = new Set(['error', 'length', 'content-filter']);
|
|
141
|
+
const GOAL_ACTIVE_STATUSES = new Set(['active', 'paused']);
|
|
142
|
+
const GOAL_ERROR_STATUSES = new Set(['blocked', 'budgetLimited']);
|
|
122
143
|
const isTransientMessagesFailure = (result, error) => {
|
|
123
144
|
if (error) {
|
|
124
145
|
if (error instanceof AssistantError) return false;
|
|
125
|
-
if (error?.name === 'TypeError' || error?.name === 'FetchError') return true;
|
|
146
|
+
if (error?.reason === 'Transport' || error?.name === 'TypeError' || error?.name === 'FetchError' || error?.name === 'AbortError') return true;
|
|
126
147
|
const code = error?.cause?.code || error?.code;
|
|
127
148
|
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'EPIPE') return true;
|
|
128
|
-
|
|
149
|
+
const status = getHttpStatus(error);
|
|
150
|
+
if (status == null) return false;
|
|
151
|
+
return status === 408 || status === 429 || status >= 500;
|
|
129
152
|
}
|
|
130
153
|
const status = messagesErrorStatus(result);
|
|
131
154
|
if (status == null) return true;
|
|
132
155
|
return status === 408 || status === 429 || status >= 500;
|
|
133
156
|
};
|
|
134
157
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
-
const
|
|
158
|
+
const sessionRecord = (value) => (value && typeof value === 'object' && value.data && value.id == null ? value.data : value);
|
|
159
|
+
const sessionIDOf = (value) => sessionRecord(value)?.id ?? null;
|
|
160
|
+
const sessionDirectoryOf = (value) => {
|
|
161
|
+
const session = sessionRecord(value);
|
|
162
|
+
return session?.location?.directory ?? session?.directory ?? session?.project?.worktree ?? null;
|
|
163
|
+
};
|
|
164
|
+
const toV2PromptInput = (sessionID, parts, messageID) => {
|
|
165
|
+
const text = (parts || []).filter((part) => part?.type === 'text').map((part) => part.text).join('\n');
|
|
166
|
+
const files = (parts || []).filter((part) => part?.type === 'file').map((part) => ({ uri: part.url, ...(part.filename ? { name: part.filename } : {}) }));
|
|
167
|
+
const agents = (parts || []).filter((part) => part?.type === 'agent').map((part) => ({ name: part.name }));
|
|
168
|
+
return { sessionID, id: messageID, text, ...(files.length ? { files } : {}), ...(agents.length ? { agents } : {}), delivery: 'steer' };
|
|
169
|
+
};
|
|
170
|
+
const projectV2Parts = (entry, sessionID) => {
|
|
171
|
+
if (Array.isArray(entry?.parts)) return entry.parts;
|
|
172
|
+
const messageID = entry?.id;
|
|
173
|
+
const parts = [];
|
|
174
|
+
if (typeof entry?.text === 'string' && entry.text) parts.push({ id: `${messageID}:text`, sessionID, messageID, type: 'text', text: entry.text });
|
|
175
|
+
if (Array.isArray(entry?.files)) entry.files.forEach((file, index) => parts.push({ id: `${messageID}:file:${index}`, sessionID, messageID, type: 'file', mime: file?.mime || 'application/octet-stream', url: file?.uri ?? file?.url, ...(file?.name ? { filename: file.name } : {}) }));
|
|
176
|
+
if (Array.isArray(entry?.content)) entry.content.forEach((item, index) => {
|
|
177
|
+
const partID = item?.id || `${messageID}:content:${index}`;
|
|
178
|
+
if (item?.type === 'text') parts.push({ id: partID, sessionID, messageID, type: 'text', text: item.text ?? '' });
|
|
179
|
+
else if (item?.type === 'reasoning') parts.push({ id: partID, sessionID, messageID, type: 'reasoning', text: item.text ?? '' });
|
|
180
|
+
else if (item?.type === 'tool') parts.push({ id: partID, sessionID, messageID, type: 'tool', tool: item.name, callID: item.id, state: item.state ?? {} });
|
|
181
|
+
});
|
|
182
|
+
return parts;
|
|
183
|
+
};
|
|
184
|
+
const projectProjectionEntry = (entry, sessionID) => {
|
|
185
|
+
if (plainObject(entry?.info)) return { info: entry.info, parts: Array.isArray(entry.parts) ? entry.parts : [] };
|
|
186
|
+
if (!plainObject(entry) || !nonEmptyString(entry.id)) return { info: entry, parts: [] };
|
|
187
|
+
const role = entry.type === 'user' || entry.type === 'assistant' ? entry.type : (entry.role ?? entry.type);
|
|
188
|
+
return { info: { id: entry.id, sessionID: nonEmptyString(entry.sessionID) ? entry.sessionID : sessionID, role, time: entry.time ?? { created: 0 }, ...entry }, parts: projectV2Parts(entry, nonEmptyString(entry.sessionID) ? entry.sessionID : sessionID) };
|
|
189
|
+
};
|
|
190
|
+
const projectionEntries = (payload, sessionID) => {
|
|
191
|
+
const items = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : Array.isArray(payload?.data?.items) ? payload.data.items : null;
|
|
192
|
+
if (!items) return null;
|
|
193
|
+
return items.map((entry) => projectProjectionEntry(entry, sessionID));
|
|
194
|
+
};
|
|
195
|
+
const projectionCursor = (result) => { const next = typeof result?.cursor === 'string' ? result.cursor : result?.cursor?.next; return (typeof next === 'string' && next) ? next : (result?.response?.headers?.get?.('x-next-cursor') ?? null); };
|
|
196
|
+
const promptAdmitted = (result) => !result?.error && (result?.response?.status === 204 || result?.status === 204 || result?.id != null || result?.type != null || result?.data !== undefined || result?.response?.ok === true);
|
|
197
|
+
const invokeSession = async (work) => {
|
|
198
|
+
try {
|
|
199
|
+
const value = await work();
|
|
200
|
+
if (isMissing(value)) return { error: { status: 404 }, status: 404 };
|
|
201
|
+
if (value?.error) return value;
|
|
202
|
+
return value;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof AssistantError) throw error;
|
|
205
|
+
if (isMissingError(error)) return { error: { status: 404 }, status: 404 };
|
|
206
|
+
const status = getHttpStatus(error);
|
|
207
|
+
if (status != null) return { error: { status }, status };
|
|
208
|
+
return { error: { status: 500 }, status: 500 };
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
136
212
|
/** Bound catalog / project list waits so a contact lane cannot hang forever. */
|
|
137
213
|
export const CONTACT_CATALOG_DEADLINE_MS = 8_000;
|
|
138
214
|
/** Bound last worker assistant text injected into assigned-session resume prompts. */
|
|
@@ -160,7 +236,7 @@ const awaitWithDeadline = async (work, ms = CONTACT_CATALOG_DEADLINE_MS, code =
|
|
|
160
236
|
}
|
|
161
237
|
};
|
|
162
238
|
|
|
163
|
-
export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, getOpenCodeAuthHeaders, getServerId = async () => null, getAllowedRoots = () => [], listProjects = async () => [], readModelPreferences = async () => null, listScheduledTasks = null, sessionIndexService = null, upsertScheduledTask = null, syncScheduledTaskProject = null, globalEventHub = null, onRevisionTip = null, onContactTurnEvent = null, onContactTurnComplete = null, clock = () => Date.now(), setIntervalFn = setInterval, clearIntervalFn = clearInterval, setImmediateFn = setImmediate, reconcileIntervalMs = 60_000, clientFactory, createChatCompletion = null, runContactTurn = defaultRunContactTurn, listWorktrees = defaultListWorktrees } = {}) => {
|
|
239
|
+
export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, getOpenCodeAuthHeaders, getServerId = async () => null, getAllowedRoots = () => [], listProjects = async () => [], readModelPreferences = async () => null, listScheduledTasks = null, sessionIndexService = null, upsertScheduledTask = null, syncScheduledTaskProject = null, globalEventHub = null, onRevisionTip = null, onContactTurnEvent = null, onContactTurnComplete = null, clock = () => Date.now(), setIntervalFn = setInterval, clearIntervalFn = clearInterval, setImmediateFn = setImmediate, reconcileIntervalMs = 60_000, clientFactory, fetchImpl, createChatCompletion = null, runContactTurn = defaultRunContactTurn, listWorktrees = defaultListWorktrees } = {}) => {
|
|
164
240
|
if (!dbPath || !dataDir) return null;
|
|
165
241
|
const Database = require('better-sqlite3');
|
|
166
242
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
@@ -348,30 +424,29 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
348
424
|
};
|
|
349
425
|
};
|
|
350
426
|
const binding = (row) => ({ sessionID: row.current_session_id, directory: effectiveWorkspace(row), sessionGeneration: row.session_generation });
|
|
351
|
-
const client = () => clientFactory
|
|
427
|
+
const client = () => clientFactory
|
|
428
|
+
? clientFactory()
|
|
429
|
+
: makeOpenCodeV2Client({
|
|
430
|
+
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
431
|
+
authHeaders: getOpenCodeAuthHeaders(),
|
|
432
|
+
});
|
|
352
433
|
const metadata = (row) => ({ openchamber: { assistant: { assistantID: row.assistant_id, name: row.name } } });
|
|
353
|
-
const ARCHIVE_RETRY_MS = 25;
|
|
354
|
-
const archiveCreatedSession = async (sessionID, directory) => {
|
|
355
|
-
const archiveAt = now();
|
|
356
|
-
const update = () => client().session.update({ sessionID, directory, time: { archived: archiveAt } });
|
|
357
|
-
let result = await update();
|
|
358
|
-
if (isMissing(result)) {
|
|
359
|
-
await sleep(ARCHIVE_RETRY_MS);
|
|
360
|
-
result = await update();
|
|
361
|
-
}
|
|
362
|
-
if (result?.error || isMissing(result)) fail('upstream_error');
|
|
363
|
-
};
|
|
364
434
|
const createSession = async (row) => {
|
|
365
435
|
const directory = effectiveWorkspace(row);
|
|
366
|
-
const result = await client().session.create({
|
|
367
|
-
|
|
368
|
-
|
|
436
|
+
const result = await invokeSession(() => client().session.create({
|
|
437
|
+
title: `[Assistant] ${row.name}`,
|
|
438
|
+
location: { directory },
|
|
439
|
+
...(row.agent ? { agent: row.agent } : {}),
|
|
440
|
+
...(row.provider_id && row.model_id ? { model: { id: row.model_id, providerID: row.provider_id, ...(row.variant ? { variant: row.variant } : {}) } } : {}),
|
|
441
|
+
}));
|
|
442
|
+
const sessionID = sessionIDOf(result);
|
|
443
|
+
if (result?.error || !sessionID) fail('upstream_error');
|
|
369
444
|
// Archive before binding so ordinary session lists never flash system sessions.
|
|
370
445
|
// Metadata still isolates the session if archive fails after create.
|
|
371
|
-
|
|
446
|
+
// v2 has no session.update archive-metadata write; local history + title prefix are the equivalent isolation.
|
|
372
447
|
return { sessionID, directory };
|
|
373
448
|
};
|
|
374
|
-
const sessionExists = async (row) => { if (!row.current_session_id) return false; const result = await client().session.get({ sessionID: row.current_session_id
|
|
449
|
+
const sessionExists = async (row) => { if (!row.current_session_id) return false; const result = await invokeSession(() => client().session.get({ sessionID: row.current_session_id })); if (isMissing(result)) return false; if (result.error) fail('upstream_error'); return Boolean(sessionIDOf(result) || result); };
|
|
375
450
|
const replaceBinding = (row, created) => { if (row.current_session_id && row.current_session_id !== created.sessionID) archiveSession(row.assistant_id, row.current_session_id, effectiveWorkspace(row)); const result = db.prepare('UPDATE assistant_v2 SET current_session_id=?,session_generation=session_generation+1,updated_at=? WHERE assistant_id=? AND session_generation=? AND tombstone_at IS NULL').run(created.sessionID, now(), row.assistant_id, row.session_generation); if (result.changes) bump(); return result.changes ? assistant(row.assistant_id) : null; };
|
|
376
451
|
const statelessLanes = new Map();
|
|
377
452
|
const inStatelessLane = (assistantID, task) => {
|
|
@@ -480,8 +555,17 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
480
555
|
db.prepare('UPDATE assistant_message_mirror SET covered=0 WHERE assistant_id=? AND session_id=? AND message_id=?').run(assistantID, sessionID, messageID);
|
|
481
556
|
db.prepare('INSERT INTO assistant_message_backfill(assistant_id,session_id,cursor,complete,updated_at) VALUES (?,?,?,?,?) ON CONFLICT(assistant_id,session_id) DO UPDATE SET cursor=NULL,complete=0,updated_at=excluded.updated_at').run(assistantID, sessionID, null, 0, now());
|
|
482
557
|
};
|
|
483
|
-
|
|
484
|
-
|
|
558
|
+
// Accept bridge/legacy `{ properties }` and native v2 `{ data }` envelopes.
|
|
559
|
+
const eventBody = (payload) => {
|
|
560
|
+
if (!plainObject(payload)) return null;
|
|
561
|
+
const data = plainObject(payload.data) ? payload.data : null;
|
|
562
|
+
const properties = plainObject(payload.properties) ? payload.properties : null;
|
|
563
|
+
if (data && Object.keys(data).length > 0) return data;
|
|
564
|
+
if (properties) return properties;
|
|
565
|
+
return data || properties || null;
|
|
566
|
+
};
|
|
567
|
+
const eventSessionID = (body) => {
|
|
568
|
+
const sessionID = body?.sessionID || body?.sessionId || body?.info?.sessionID || body?.info?.sessionId;
|
|
485
569
|
return nonEmptyString(sessionID) ? sessionID : '';
|
|
486
570
|
};
|
|
487
571
|
const isUserAbort = (error) => error?.name === 'MessageAbortedError';
|
|
@@ -519,21 +603,39 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
519
603
|
if (status === 'cancelled') entry.observed.resume = resume !== false;
|
|
520
604
|
}
|
|
521
605
|
};
|
|
522
|
-
const settleStatusFromEvent = (payload,
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
606
|
+
const settleStatusFromEvent = (payload, body) => {
|
|
607
|
+
const type = typeof payload?.type === 'string' ? payload.type : '';
|
|
608
|
+
if (type === 'session.error' || type === 'session.execution.failed') {
|
|
609
|
+
return isUserAbort(body?.error) ? 'cancelled' : 'error';
|
|
610
|
+
}
|
|
611
|
+
// User interrupt: prefer cancelled so contact resume path can notify.
|
|
612
|
+
if (type === 'session.execution.interrupted') {
|
|
613
|
+
return isUserAbort(body?.error) ? 'cancelled' : 'error';
|
|
614
|
+
}
|
|
615
|
+
if (type === 'session.execution.succeeded') return 'complete';
|
|
616
|
+
if (type === 'session.execution.started' || type === 'session.retry.scheduled') return 'busy';
|
|
617
|
+
if (type === 'question.asked' || type === 'permission.asked'
|
|
618
|
+
|| type === 'session.pending.question' || type === 'session.pending.permission'
|
|
619
|
+
|| type === 'pending.question' || type === 'pending.permission') return 'question';
|
|
620
|
+
if (type === 'session.idle') return 'complete';
|
|
621
|
+
if (type === 'session.status') {
|
|
622
|
+
const statusType = typeof body?.status?.type === 'string' ? body.status.type : typeof body?.info?.type === 'string' ? body.info.type : '';
|
|
623
|
+
if (statusType === 'busy' || statusType === 'retry' || statusType === 'running') return 'busy';
|
|
529
624
|
if (statusType === 'idle') return 'complete';
|
|
530
625
|
}
|
|
531
626
|
return null;
|
|
532
627
|
};
|
|
628
|
+
// Stash assign delivery message IDs until the session card watch is written.
|
|
629
|
+
const pendingAssignMessageIDs = new Map();
|
|
533
630
|
// Assigned-session complete/error/cancelled resumes the contact LLM (no canned settle bubble).
|
|
534
631
|
// Assigned after inContactTurnLane exists; reportAssignedSession only schedules via this ref.
|
|
535
632
|
const assignedSessionResumeRef = { schedule: null };
|
|
536
|
-
const reportAssignedSession = (sessionID, status,
|
|
633
|
+
const reportAssignedSession = (sessionID, status, resumeOrOpts = true) => {
|
|
634
|
+
const opts = plainObject(resumeOrOpts)
|
|
635
|
+
? resumeOrOpts
|
|
636
|
+
: { resume: resumeOrOpts !== false };
|
|
637
|
+
const resume = opts.resume !== false;
|
|
638
|
+
const requireExecutionOwned = opts.requireExecutionOwned === true;
|
|
537
639
|
const inflight = sessionAbortInflight.get(sessionID);
|
|
538
640
|
if (inflight) {
|
|
539
641
|
// Defer watch terminal + continuation until stop_session abort settles.
|
|
@@ -554,6 +656,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
554
656
|
if (watch.status === status || watch.status === 'cancelled') continue;
|
|
555
657
|
// session.idle / status idle follow session.error. Do not rewrite 失败 as 完成.
|
|
556
658
|
if (status === 'complete' && watch.status === 'error') continue;
|
|
659
|
+
// Weak idle signals must not settle a delivery-watermarked watch — old
|
|
660
|
+
// session.idle can race a reuse assign. Formal execution.* owns that path.
|
|
661
|
+
if (requireExecutionOwned && status === 'complete' && watch.messageID) continue;
|
|
557
662
|
const updatedAt = now();
|
|
558
663
|
updateSessionCardStatus(db, { assistantID: watch.assistantID, sessionID, status });
|
|
559
664
|
upsertContactWatch(db, {
|
|
@@ -563,6 +668,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
563
668
|
status,
|
|
564
669
|
updatedAt,
|
|
565
670
|
resumeAllowed: watch.resumeAllowed,
|
|
671
|
+
...(watch.messageID ? { messageID: watch.messageID } : {}),
|
|
566
672
|
});
|
|
567
673
|
// complete/error/user-interrupt cancelled: hand result back to contact LLM.
|
|
568
674
|
// question: card only (worker waiting). stop_session passes resume=false.
|
|
@@ -586,7 +692,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
586
692
|
}
|
|
587
693
|
// processEvent stays sync; resume runs on the contact turn lane asynchronously.
|
|
588
694
|
if (typeof assignedSessionResumeRef.schedule === 'function') {
|
|
589
|
-
for (const
|
|
695
|
+
for (const item of resumes) assignedSessionResumeRef.schedule(item);
|
|
590
696
|
}
|
|
591
697
|
return changed;
|
|
592
698
|
};
|
|
@@ -596,26 +702,134 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
596
702
|
if (typeof session?.type === 'string') return session.type;
|
|
597
703
|
return '';
|
|
598
704
|
};
|
|
705
|
+
const readMessageInfo = (entry) => {
|
|
706
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
707
|
+
if (plainObject(entry.info)) return entry.info;
|
|
708
|
+
return entry;
|
|
709
|
+
};
|
|
710
|
+
const isAssistantMessage = (info) => info && (info.type === 'assistant' || info.role === 'assistant');
|
|
711
|
+
const messageListItems = (result) => {
|
|
712
|
+
if (!result || result.error) return null;
|
|
713
|
+
if (Array.isArray(result.data)) return result.data;
|
|
714
|
+
if (Array.isArray(result)) return result;
|
|
715
|
+
if (Array.isArray(result.data?.items)) return result.data.items;
|
|
716
|
+
return null;
|
|
717
|
+
};
|
|
718
|
+
const activeMapOf = (value) => {
|
|
719
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.error) return null;
|
|
720
|
+
// Tolerate `{ data: { ses_x: … } }` wrappers without treating a bare map as nested data.
|
|
721
|
+
if (value.data && typeof value.data === 'object' && !Array.isArray(value.data) && value.id == null
|
|
722
|
+
&& !Object.prototype.hasOwnProperty.call(value, 'type')) {
|
|
723
|
+
const nested = value.data;
|
|
724
|
+
if (!Array.isArray(nested) && typeof nested === 'object') return nested;
|
|
725
|
+
}
|
|
726
|
+
return value;
|
|
727
|
+
};
|
|
728
|
+
const goalOfSession = (session) => {
|
|
729
|
+
const goal = session?.metadata?.openchamber?.goal;
|
|
730
|
+
if (!goal || typeof goal !== 'object') return null;
|
|
731
|
+
const status = typeof goal.status === 'string' ? goal.status.trim() : '';
|
|
732
|
+
return status ? { status } : null;
|
|
733
|
+
};
|
|
734
|
+
/** Messages that belong to this delivery watermark (assign prompt messageID). */
|
|
735
|
+
const messagesOwnedByDelivery = (messages, deliveryMessageID) => {
|
|
736
|
+
if (!Array.isArray(messages)) return [];
|
|
737
|
+
if (!nonEmptyString(deliveryMessageID)) return messages;
|
|
738
|
+
const rows = messages.map((entry) => ({ entry, info: readMessageInfo(entry) }));
|
|
739
|
+
const delivery = rows.find((row) => row.info?.id === deliveryMessageID);
|
|
740
|
+
if (delivery) {
|
|
741
|
+
const created = typeof delivery.info?.time?.created === 'number' ? delivery.info.time.created : null;
|
|
742
|
+
return rows
|
|
743
|
+
.filter((row) => {
|
|
744
|
+
if (row.info?.id === deliveryMessageID) return true;
|
|
745
|
+
const rowCreated = typeof row.info?.time?.created === 'number' ? row.info.time.created : null;
|
|
746
|
+
if (created != null && rowCreated != null) {
|
|
747
|
+
if (rowCreated > created) return true;
|
|
748
|
+
if (rowCreated < created) return false;
|
|
749
|
+
}
|
|
750
|
+
// Ascending msg_ ids: strictly after the delivery user row.
|
|
751
|
+
return typeof row.info?.id === 'string' && row.info.id > deliveryMessageID;
|
|
752
|
+
})
|
|
753
|
+
.map((row) => row.entry);
|
|
754
|
+
}
|
|
755
|
+
// Delivery not on this page — only keep ids after the watermark.
|
|
756
|
+
return rows
|
|
757
|
+
.filter((row) => typeof row.info?.id === 'string' && row.info.id > deliveryMessageID)
|
|
758
|
+
.map((row) => row.entry);
|
|
759
|
+
};
|
|
599
760
|
const lastAssistantInfo = (messages) => {
|
|
600
|
-
if (!Array.isArray(messages)) return null;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
761
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
762
|
+
// Prefer newest by time.created; fall back to array order (desc list → index 0).
|
|
763
|
+
let best = null;
|
|
764
|
+
let bestCreated = Number.NEGATIVE_INFINITY;
|
|
765
|
+
let bestIndex = -1;
|
|
766
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
767
|
+
const info = readMessageInfo(messages[index]);
|
|
768
|
+
if (!isAssistantMessage(info)) continue;
|
|
769
|
+
const created = typeof info?.time?.created === 'number' ? info.time.created : Number.NEGATIVE_INFINITY;
|
|
770
|
+
if (!best || created > bestCreated || (created === bestCreated && index < bestIndex)) {
|
|
771
|
+
best = info;
|
|
772
|
+
bestCreated = created;
|
|
773
|
+
bestIndex = index;
|
|
774
|
+
}
|
|
604
775
|
}
|
|
776
|
+
if (best) return best;
|
|
777
|
+
// Desc page without timestamps: first assistant from the start.
|
|
778
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
779
|
+
const info = readMessageInfo(messages[index]);
|
|
780
|
+
if (isAssistantMessage(info)) return info;
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
};
|
|
784
|
+
const classifyOwnedAssistantTail = (assistantInfo) => {
|
|
785
|
+
if (!assistantInfo) return null;
|
|
786
|
+
if (assistantInfo.error) {
|
|
787
|
+
// MessageAbortedError → cancelled for contact resume; other errors fail the card.
|
|
788
|
+
return isUserAbort(assistantInfo.error) ? 'cancelled' : 'error';
|
|
789
|
+
}
|
|
790
|
+
const finish = typeof assistantInfo.finish === 'string' ? assistantInfo.finish.trim() : '';
|
|
791
|
+
// Mid-loop tool-calls (even with time.completed) are intermediate steps.
|
|
792
|
+
if (finish === 'tool-calls') return null;
|
|
793
|
+
if (ASSISTANT_ERROR_FINISH.has(finish)) return 'error';
|
|
794
|
+
if (assistantInfo.time?.completed && (!finish || ASSISTANT_SUCCESS_FINISH.has(finish))) return 'complete';
|
|
795
|
+
if (!finish && !assistantInfo.time?.completed) return null;
|
|
605
796
|
return null;
|
|
606
797
|
};
|
|
607
|
-
const inferAssignedSessionSettleStatus = (getResult, messagesResult) => {
|
|
798
|
+
const inferAssignedSessionSettleStatus = (getResult, messagesResult, { deliveryMessageID = null, activeBusy = null } = {}) => {
|
|
608
799
|
if (isMissing(getResult)) return 'complete';
|
|
609
800
|
if (getResult?.error) return null;
|
|
610
|
-
|
|
801
|
+
// Authoritative active membership wins over stale get snapshots.
|
|
802
|
+
if (activeBusy === true) return null;
|
|
803
|
+
const session = sessionRecord(getResult);
|
|
611
804
|
if (session?.error) return isUserAbort(session.error) ? 'cancelled' : 'error';
|
|
805
|
+
const goal = goalOfSession(session);
|
|
806
|
+
// Goal settle is owned by the session-goal reporter — never invent success.
|
|
807
|
+
if (goal && GOAL_ACTIVE_STATUSES.has(goal.status)) return null;
|
|
808
|
+
if (goal && GOAL_ERROR_STATUSES.has(goal.status)) return 'error';
|
|
809
|
+
if (goal && goal.status === 'complete') return 'complete';
|
|
612
810
|
const statusType = sessionStatusType(session);
|
|
613
|
-
if (statusType === 'busy' || statusType === 'retry') return null;
|
|
811
|
+
if (statusType === 'busy' || statusType === 'retry' || statusType === 'running') return null;
|
|
614
812
|
if (isMissing(messagesResult)) return 'complete';
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
|
|
813
|
+
if (messagesResult?.error) return null;
|
|
814
|
+
const items = messageListItems(messagesResult);
|
|
815
|
+
if (!items) return null;
|
|
816
|
+
const owned = messagesOwnedByDelivery(items, deliveryMessageID);
|
|
817
|
+
// Reused session: only pre-delivery assistants remain → wait for this run.
|
|
818
|
+
if (nonEmptyString(deliveryMessageID) && owned.length === 0) return null;
|
|
819
|
+
if (nonEmptyString(deliveryMessageID) && !owned.some((entry) => readMessageInfo(entry)?.id === deliveryMessageID)
|
|
820
|
+
&& !owned.some((entry) => isAssistantMessage(readMessageInfo(entry)))) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const assistantInfo = lastAssistantInfo(owned);
|
|
824
|
+
const fromTail = classifyOwnedAssistantTail(assistantInfo);
|
|
825
|
+
if (fromTail) return fromTail;
|
|
826
|
+
// No owned assistant yet after delivery — keep waiting (superseded / not started).
|
|
827
|
+
if (nonEmptyString(deliveryMessageID) && !assistantInfo) return null;
|
|
828
|
+
// Legacy watches without watermark: idle / session completed may settle.
|
|
829
|
+
if (!nonEmptyString(deliveryMessageID)) {
|
|
830
|
+
if (statusType === 'idle' || session?.time?.completed) return 'complete';
|
|
831
|
+
if (assistantInfo?.time?.completed && assistantInfo.finish !== 'tool-calls') return 'complete';
|
|
832
|
+
}
|
|
619
833
|
return null;
|
|
620
834
|
};
|
|
621
835
|
const resolveWatchDirectory = (watch) => {
|
|
@@ -634,26 +848,50 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
634
848
|
} catch {
|
|
635
849
|
return;
|
|
636
850
|
}
|
|
851
|
+
if (watches.length === 0) return;
|
|
852
|
+
// One active query per reconcile — authoritative busy membership map.
|
|
853
|
+
let activeMap = null;
|
|
854
|
+
let activeKnown = false;
|
|
855
|
+
try {
|
|
856
|
+
const api = client();
|
|
857
|
+
if (typeof api.session?.active === 'function') {
|
|
858
|
+
const raw = await api.session.active();
|
|
859
|
+
activeMap = activeMapOf(raw);
|
|
860
|
+
activeKnown = activeMap != null;
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
activeKnown = false;
|
|
864
|
+
}
|
|
865
|
+
if (closed) return;
|
|
637
866
|
for (const watch of watches) {
|
|
638
867
|
if (closed) return;
|
|
639
868
|
try {
|
|
869
|
+
const activeBusy = activeKnown
|
|
870
|
+
? Object.prototype.hasOwnProperty.call(activeMap, watch.sessionID)
|
|
871
|
+
: null;
|
|
872
|
+
if (activeBusy === true) continue;
|
|
640
873
|
const directory = resolveWatchDirectory(watch);
|
|
641
874
|
if (!directory) continue;
|
|
642
875
|
let getResult;
|
|
643
876
|
try {
|
|
644
|
-
getResult = await client().session.get({ sessionID: watch.sessionID, directory });
|
|
645
|
-
} catch {
|
|
646
|
-
|
|
877
|
+
getResult = await invokeSession(() => client().session.get({ sessionID: watch.sessionID, directory }));
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (isMissingError(error)) getResult = { error: { status: 404 }, status: 404 };
|
|
880
|
+
else continue;
|
|
647
881
|
}
|
|
648
882
|
if (closed) return;
|
|
649
883
|
let messagesResult = null;
|
|
650
884
|
try {
|
|
651
|
-
messagesResult = await client().
|
|
652
|
-
} catch {
|
|
653
|
-
messagesResult =
|
|
885
|
+
messagesResult = await invokeSession(() => client().message.list({ sessionID: watch.sessionID, limit: 100, order: 'desc' }));
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (isMissingError(error)) messagesResult = { error: { status: 404 }, status: 404 };
|
|
888
|
+
else messagesResult = null;
|
|
654
889
|
}
|
|
655
890
|
if (closed) return;
|
|
656
|
-
const status = inferAssignedSessionSettleStatus(getResult, messagesResult
|
|
891
|
+
const status = inferAssignedSessionSettleStatus(getResult, messagesResult, {
|
|
892
|
+
deliveryMessageID: watch.messageID,
|
|
893
|
+
activeBusy,
|
|
894
|
+
});
|
|
657
895
|
if (status) reportAssignedSession(watch.sessionID, status);
|
|
658
896
|
} catch {
|
|
659
897
|
// One failed watch must not block unrelated watches.
|
|
@@ -662,47 +900,69 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
662
900
|
};
|
|
663
901
|
const processEvent = (event) => {
|
|
664
902
|
const payload = event?.payload?.payload ?? event?.payload ?? event;
|
|
665
|
-
|
|
666
|
-
|
|
903
|
+
if (!plainObject(payload) || typeof payload.type !== 'string') return false;
|
|
904
|
+
const body = eventBody(payload);
|
|
905
|
+
// Message lifecycle still requires a body; execution/status may carry sessionID on body only.
|
|
667
906
|
if (payload.type === 'message.updated') {
|
|
668
|
-
|
|
907
|
+
if (!plainObject(body)) return false;
|
|
908
|
+
const info = body.info; const sessionID = info?.sessionID;
|
|
669
909
|
if (!nonEmptyString(sessionID) || !plainObject(info)) return false;
|
|
670
910
|
const cancelled = info.role === 'assistant' && isUserAbort(info.error)
|
|
671
911
|
? reportAssignedSession(sessionID, 'cancelled') : false;
|
|
672
912
|
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { const current = assistant(assistantID)?.current_session_id === sessionID; mirrorMessage(assistantID, sessionID, info, undefined, current); if (!current) invalidateBackfill(assistantID, sessionID); } return cancelled || assistants.length > 0;
|
|
673
913
|
}
|
|
674
914
|
if (payload.type === 'message.part.updated') {
|
|
675
|
-
|
|
915
|
+
if (!plainObject(body)) return false;
|
|
916
|
+
const part = body.part; const sessionID = body.sessionID ?? part?.sessionID;
|
|
676
917
|
if (!nonEmptyString(sessionID) || !plainObject(part)) return false;
|
|
677
918
|
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { db.prepare("DELETE FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? AND part_id GLOB 'oc_asst_admission:*'").run(assistantID, sessionID, part.messageID); mirrorPart(assistantID, sessionID, part); if (assistant(assistantID)?.current_session_id !== sessionID) invalidateBackfill(assistantID, sessionID); } return assistants.length > 0;
|
|
678
919
|
}
|
|
679
920
|
if (payload.type === 'message.removed') {
|
|
680
|
-
|
|
921
|
+
if (!plainObject(body)) return false;
|
|
922
|
+
const sessionID = body.sessionID; const messageID = body.messageID;
|
|
681
923
|
if (!nonEmptyString(sessionID) || !nonEmptyString(messageID)) return false;
|
|
682
924
|
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { db.prepare('DELETE FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=?').run(assistantID, sessionID, messageID); db.prepare('DELETE FROM assistant_message_mirror WHERE assistant_id=? AND session_id=? AND message_id=?').run(assistantID, sessionID, messageID); } return assistants.length > 0;
|
|
683
925
|
}
|
|
684
926
|
if (payload.type === 'message.part.removed') {
|
|
685
|
-
|
|
927
|
+
if (!plainObject(body)) return false;
|
|
928
|
+
const sessionID = body.sessionID; const messageID = body.messageID ?? body.part?.messageID; const partID = body.partID ?? body.part?.id;
|
|
686
929
|
if (!nonEmptyString(sessionID) || !nonEmptyString(messageID) || !nonEmptyString(partID)) return false;
|
|
687
930
|
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { db.prepare('DELETE FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? AND part_id=?').run(assistantID, sessionID, messageID, partID); if (assistant(assistantID)?.current_session_id !== sessionID) invalidateMessageCoverage(assistantID, sessionID, messageID); } return assistants.length > 0;
|
|
688
931
|
}
|
|
932
|
+
const sessionID = plainObject(body) ? eventSessionID(body) : '';
|
|
933
|
+
if (payload.type === 'session.execution.started' || payload.type === 'session.retry.scheduled') {
|
|
934
|
+
if (!sessionID) return false;
|
|
935
|
+
return reportAssignedSession(sessionID, 'busy');
|
|
936
|
+
}
|
|
937
|
+
if (payload.type === 'session.execution.succeeded'
|
|
938
|
+
|| payload.type === 'session.execution.failed'
|
|
939
|
+
|| payload.type === 'session.execution.interrupted') {
|
|
940
|
+
if (!sessionID) return false;
|
|
941
|
+
const status = settleStatusFromEvent(payload, body);
|
|
942
|
+
if (!status) return false;
|
|
943
|
+
const reported = reportAssignedSession(sessionID, status);
|
|
944
|
+
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { if (assistant(assistantID)?.current_session_id !== sessionID) invalidateBackfill(assistantID, sessionID); }
|
|
945
|
+
return reported || assistants.some((assistantID) => assistant(assistantID)?.current_session_id !== sessionID);
|
|
946
|
+
}
|
|
689
947
|
if (payload.type === 'session.idle' || payload.type === 'session.error') {
|
|
690
|
-
const sessionID = eventSessionID(properties);
|
|
691
948
|
if (!sessionID) return false;
|
|
692
|
-
|
|
949
|
+
// session.idle is a weak fallback: watermarked watches need execution.* or reconcile.
|
|
950
|
+
const status = settleStatusFromEvent(payload, body);
|
|
951
|
+
const reported = reportAssignedSession(sessionID, status, { requireExecutionOwned: payload.type === 'session.idle' });
|
|
693
952
|
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { if (assistant(assistantID)?.current_session_id !== sessionID) invalidateBackfill(assistantID, sessionID); }
|
|
694
953
|
return reported || assistants.some((assistantID) => assistant(assistantID)?.current_session_id !== sessionID);
|
|
695
954
|
}
|
|
696
955
|
if (payload.type === 'session.status') {
|
|
697
|
-
|
|
698
|
-
const status = settleStatusFromEvent(payload,
|
|
699
|
-
if (!
|
|
700
|
-
const reported = reportAssignedSession(sessionID, status);
|
|
956
|
+
if (!sessionID) return false;
|
|
957
|
+
const status = settleStatusFromEvent(payload, body);
|
|
958
|
+
if (!status) return false;
|
|
959
|
+
const reported = reportAssignedSession(sessionID, status, { requireExecutionOwned: status === 'complete' });
|
|
701
960
|
const assistants = mappedAssistants(sessionID); for (const assistantID of assistants) { if (assistant(assistantID)?.current_session_id !== sessionID) invalidateBackfill(assistantID, sessionID); }
|
|
702
961
|
return reported || assistants.some((assistantID) => assistant(assistantID)?.current_session_id !== sessionID);
|
|
703
962
|
}
|
|
704
|
-
if (payload.type === 'question.asked' || payload.type === 'permission.asked'
|
|
705
|
-
|
|
963
|
+
if (payload.type === 'question.asked' || payload.type === 'permission.asked'
|
|
964
|
+
|| payload.type === 'session.pending.question' || payload.type === 'session.pending.permission'
|
|
965
|
+
|| payload.type === 'pending.question' || payload.type === 'pending.permission') {
|
|
706
966
|
if (!sessionID) return false;
|
|
707
967
|
return reportAssignedSession(sessionID, 'question');
|
|
708
968
|
}
|
|
@@ -741,7 +1001,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
741
1001
|
const fetchBackfillMessages = async (history, directory, cursor) => {
|
|
742
1002
|
for (let attempt = 0; attempt < BACKFILL_MESSAGES_ATTEMPTS; attempt++) {
|
|
743
1003
|
try {
|
|
744
|
-
const result = await client().
|
|
1004
|
+
const result = await invokeSession(() => client().message.list({ sessionID: history.session_id, limit: BACKFILL_PAGE_SIZE, order: 'desc', ...(cursor ? { cursor } : {}) }));
|
|
745
1005
|
if (!result?.error || isMissing(result)) return result;
|
|
746
1006
|
if (!isTransientMessagesFailure(result, null) || attempt === BACKFILL_MESSAGES_ATTEMPTS - 1) fail('upstream_error');
|
|
747
1007
|
} catch (error) {
|
|
@@ -768,12 +1028,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
768
1028
|
const result = await fetchBackfillMessages(history, directory, cursor);
|
|
769
1029
|
if (isMissing(result)) return completeMissingSessionBackfill(history.assistant_id, history.session_id);
|
|
770
1030
|
if (result?.error) fail('upstream_error');
|
|
771
|
-
const entries =
|
|
1031
|
+
const entries = projectionEntries(result, history.session_id);
|
|
772
1032
|
if (!entries) fail('upstream_error');
|
|
773
1033
|
db.exec('BEGIN IMMEDIATE');
|
|
774
1034
|
try {
|
|
775
1035
|
entries.forEach((entry) => { const info = entry?.info ?? entry; if (nonEmptyString(info?.sessionID) && info.sessionID !== history.session_id) return; const parts = Array.isArray(entry?.parts) ? entry.parts : []; const messageOrdinal = Number.isSafeInteger(info?.time?.created) ? info.time.created : undefined; mirrorMessage(history.assistant_id, history.session_id, info, messageOrdinal, true); const partIDs = parts.filter((part) => nonEmptyString(part?.id)).map((part) => part.id); if (partIDs.length) db.prepare(`DELETE FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? AND part_id NOT IN (${partIDs.map(() => '?').join(',')})`).run(history.assistant_id, history.session_id, info?.id, ...partIDs); else db.prepare('DELETE FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=?').run(history.assistant_id, history.session_id, info?.id); parts.forEach((part, partIndex) => mirrorPart(history.assistant_id, history.session_id, part, partIndex + 1)); });
|
|
776
|
-
const nextCursor = result?.response?.headers?.get('x-next-cursor') ?? null;
|
|
1036
|
+
const nextCursor = projectionCursor(result) ?? result?.response?.headers?.get?.('x-next-cursor') ?? null;
|
|
777
1037
|
const next = reduceBackfillState({ cursor, complete: false }, { type: 'page', nextCursor });
|
|
778
1038
|
// Successful pages only upsert authoritative rows; never delete provisional
|
|
779
1039
|
// event mirrors that the snapshot omitted — only 404 / message.removed /
|
|
@@ -785,6 +1045,31 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
785
1045
|
};
|
|
786
1046
|
const decodeCursor = (value) => { if (value == null || value === '') return null; try { const parsed = parse(Buffer.from(String(value), 'base64url').toString('utf8')); return Number.isSafeInteger(parsed?.sessionOrdinal) && Number.isSafeInteger(parsed?.messageOrdinal) && nonEmptyString(parsed?.messageID) && (parsed.scanSessionOrdinal == null || Number.isSafeInteger(parsed.scanSessionOrdinal)) ? parsed : fail('validation_error'); } catch (error) { if (error instanceof AssistantError) throw error; fail('validation_error'); } };
|
|
787
1047
|
const encodeCursor = (row, scanSessionOrdinal = row.session_ordinal) => Buffer.from(json({ sessionOrdinal: row.session_ordinal, messageOrdinal: row.message_ordinal, messageID: row.message_id, scanSessionOrdinal })).toString('base64url');
|
|
1048
|
+
const loadProjectionSession = async (history, pageLimit = BACKFILL_PAGE_SIZE) => {
|
|
1049
|
+
let directory = history.directory ?? null;
|
|
1050
|
+
if (directory == null) {
|
|
1051
|
+
const session = await invokeSession(() => client().session.get({ sessionID: history.session_id })).catch(() => null);
|
|
1052
|
+
const resolved = resolveArchivedDirectory(history.assistant_id, sessionDirectoryOf(session), session?.data?.directory, session?.data?.project?.worktree);
|
|
1053
|
+
if (resolved) {
|
|
1054
|
+
db.prepare('UPDATE assistant_session_history SET directory=? WHERE assistant_id=? AND session_id=? AND directory IS NULL').run(resolved, history.assistant_id, history.session_id);
|
|
1055
|
+
directory = resolved;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const collected = [];
|
|
1059
|
+
let cursor = null;
|
|
1060
|
+
for (let page = 0; page < BACKFILL_MAX_PAGES; page++) {
|
|
1061
|
+
const result = await fetchBackfillMessages(history, directory, cursor);
|
|
1062
|
+
if (isMissing(result)) return { entries: collected, directory, missing: true };
|
|
1063
|
+
if (result?.error) fail('upstream_error');
|
|
1064
|
+
const entries = projectionEntries(result, history.session_id);
|
|
1065
|
+
if (!entries) fail('upstream_error');
|
|
1066
|
+
collected.push(...entries);
|
|
1067
|
+
cursor = projectionCursor(result);
|
|
1068
|
+
if (!cursor) break;
|
|
1069
|
+
if (collected.length >= pageLimit) break;
|
|
1070
|
+
}
|
|
1071
|
+
return { entries: collected.slice(0, pageLimit), directory, missing: false };
|
|
1072
|
+
};
|
|
788
1073
|
const historicalMessages = async (assistantID, input = {}) => {
|
|
789
1074
|
const row = editable(assistantID); const limit = input.limit == null ? 50 : Number(input.limit); if (!Number.isInteger(limit) || limit < 1 || limit > 100) fail('validation_error'); const before = decodeCursor(input.before);
|
|
790
1075
|
const currentSessionOrdinal = Number(db.prepare('SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_session_history WHERE assistant_id=?').get(row.assistant_id).next);
|
|
@@ -820,22 +1105,50 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
820
1105
|
rows = pageRows();
|
|
821
1106
|
}
|
|
822
1107
|
const page = rows.slice(0, limit); const oldest = page[page.length - 1]; const remaining = nextIncomplete(oldest?.session_ordinal ?? before?.scanSessionOrdinal ?? before?.sessionOrdinal ?? null); const nextCursor = oldest ? (rows.length > limit || remaining ? encodeCursor(oldest, oldest.session_ordinal) : null) : remaining ? encodeCursor({ session_ordinal: remaining.ordinal, message_ordinal: Number.MAX_SAFE_INTEGER, message_id: '\uffff' }, remaining.ordinal) : null;
|
|
823
|
-
|
|
1108
|
+
let ordered = [...page].reverse().map((message) => ({ sessionID: message.session_id, directory: message.directory, info: parse(message.info_json), parts: db.prepare('SELECT part_json FROM assistant_message_part_mirror WHERE assistant_id=? AND session_id=? AND message_id=? ORDER BY ordinal ASC,part_id ASC').all(row.assistant_id, message.session_id, message.message_id).map((part) => parse(part.part_json)) }));
|
|
1109
|
+
// Current binding may still be live-only: if the mirror page has no
|
|
1110
|
+
// current-session rows, read the v2 projection instead of returning empty.
|
|
1111
|
+
if (page.length === 0 && row.current_session_id && !currentIsArchived) {
|
|
1112
|
+
try {
|
|
1113
|
+
const loaded = await loadProjectionSession({ assistant_id: row.assistant_id, session_id: row.current_session_id, directory: effectiveWorkspace(row), ordinal: currentSessionOrdinal }, limit);
|
|
1114
|
+
const live = [];
|
|
1115
|
+
for (const entry of loaded.entries) {
|
|
1116
|
+
const info = entry?.info ?? entry;
|
|
1117
|
+
if (!plainObject(info) || !nonEmptyString(info.id)) continue;
|
|
1118
|
+
if (nonEmptyString(info.sessionID) && info.sessionID !== row.current_session_id) continue;
|
|
1119
|
+
live.push({
|
|
1120
|
+
sessionID: row.current_session_id,
|
|
1121
|
+
directory: loaded.directory,
|
|
1122
|
+
info,
|
|
1123
|
+
parts: Array.isArray(entry?.parts) ? entry.parts : [],
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
ordered = [...live, ...ordered];
|
|
1127
|
+
} catch {
|
|
1128
|
+
// Keep archived/mirror rows; current live projection is best-effort.
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
824
1131
|
return { entries: ordered, nextCursor, complete: nextCursor === null };
|
|
825
1132
|
};
|
|
826
1133
|
for (const current of db.prepare('SELECT * FROM assistant_v2 WHERE current_session_id IS NOT NULL AND tombstone_at IS NULL').all()) void backfillSession({ assistant_id: current.assistant_id, session_id: current.current_session_id, directory: effectiveWorkspace(current) }).catch(() => {});
|
|
827
1134
|
const unsubscribeEvents = typeof globalEventHub?.subscribeEvent === 'function' ? globalEventHub.subscribeEvent(processEvent) : null;
|
|
828
1135
|
const createAssistant = (input) => { const allowed = new Set(['enabled', 'name', 'defaultPrompt', 'providerID', 'modelID', 'agent', 'variant', 'mode', 'workspacePath']); if (!plainObject(input) || Object.keys(input).some((key) => !allowed.has(key))) fail('validation_error'); const mode = input.mode == null ? 'continuous' : input.mode === 'stateless' || input.mode === 'continuous' ? input.mode : fail('validation_error'); const assistantID = id(); const workspacePath = input.workspacePath == null ? null : workspace(input.workspacePath, assistantID); effectiveWorkspace({ assistant_id: assistantID, workspace_path: workspacePath }); const at = now(); db.exec('BEGIN IMMEDIATE'); try { if (Number(db.prepare('SELECT COUNT(*) AS count FROM assistant_v2 WHERE tombstone_at IS NULL').get().count) >= 100) fail('assistant_limit'); db.prepare('INSERT INTO assistant_v2 (assistant_id,revision,enabled,name,default_prompt,workspace_path,provider_id,model_id,agent,variant,mode,current_session_id,session_generation,created_at,updated_at,tombstone_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)').run(assistantID, 1, input.enabled === false ? 0 : 1, string(input.name, 256, true), input.defaultPrompt ? string(input.defaultPrompt, 200_000) : '', workspacePath, string(input.providerID, 256, true), string(input.modelID, 256, true), input.agent == null ? null : string(input.agent, 256), input.variant == null ? null : string(input.variant, 256), mode, null, 0, at, at, null); bump(); db.exec('COMMIT'); return output(assistant(assistantID)); } catch (error) { db.exec('ROLLBACK'); throw error; } };
|
|
829
1136
|
const updateAssistant = async (assistantID, input) => { const row = editable(assistantID); const allowed = new Set(['expectedRevision', 'enabled', 'name', 'defaultPrompt', 'providerID', 'modelID', 'agent', 'variant', 'mode', 'workspacePath']); if (!plainObject(input) || !Number.isInteger(input.expectedRevision) || Object.keys(input).some((key) => !allowed.has(key))) fail('validation_error'); const next = { enabled: input.enabled === undefined ? row.enabled : input.enabled ? 1 : 0, name: input.name === undefined ? row.name : string(input.name, 256, true), prompt: input.defaultPrompt === undefined ? row.default_prompt : string(input.defaultPrompt, 200_000), provider: input.providerID === undefined ? row.provider_id : string(input.providerID, 256, true), model: input.modelID === undefined ? row.model_id : string(input.modelID, 256, true), agent: input.agent === undefined ? row.agent : input.agent === null ? null : string(input.agent, 256), variant: input.variant === undefined ? row.variant : input.variant === null ? null : string(input.variant, 256), mode: input.mode === undefined ? (row.mode === 'stateless' ? 'stateless' : 'continuous') : input.mode === 'continuous' || input.mode === 'stateless' ? input.mode : fail('validation_error'), workspacePath: input.workspacePath === undefined ? row.workspace_path : input.workspacePath == null ? null : workspace(input.workspacePath, assistantID) }; const nextRow = { ...row, workspace_path: next.workspacePath, name: next.name }; effectiveWorkspace(nextRow); const workspaceChanged = next.workspacePath !== row.workspace_path; if (workspaceChanged && row.current_session_id) archiveSession(assistantID, row.current_session_id); const created = workspaceChanged ? await createSession(nextRow) : null; const result = db.prepare('UPDATE assistant_v2 SET enabled=?,name=?,default_prompt=?,provider_id=?,model_id=?,agent=?,variant=?,mode=?,workspace_path=?,current_session_id=?,session_generation=session_generation+?,revision=revision+1,updated_at=? WHERE assistant_id=? AND revision=? AND session_generation=? AND tombstone_at IS NULL').run(next.enabled, next.name, next.prompt, next.provider, next.model, next.agent, next.variant, next.mode, next.workspacePath, created?.sessionID ?? row.current_session_id, workspaceChanged ? 1 : 0, now(), assistantID, input.expectedRevision, row.session_generation); if (!result.changes) fail('revision_conflict'); bump(); return output(assistant(assistantID)); };
|
|
830
|
-
const compact = async (assistantID, input) => { const row = active(assistantID); if (!plainObject(input) || input.sessionID !== row.current_session_id || input.sessionGeneration !== row.session_generation || !row.current_session_id) fail('revision_conflict'); let target = row; let result = await client().session.
|
|
1137
|
+
const compact = async (assistantID, input) => { const row = active(assistantID); if (!plainObject(input) || input.sessionID !== row.current_session_id || input.sessionGeneration !== row.session_generation || !row.current_session_id) fail('revision_conflict'); let target = row; let result = await invokeSession(() => client().session.compact({ sessionID: target.current_session_id })); if (isMissing(result)) { await restoreOnce(row, row.current_session_id, row.session_generation); target = active(assistantID); result = await invokeSession(() => client().session.compact({ sessionID: target.current_session_id })); } if (result?.error || (result?.data !== true && result?.id == null && result?.type !== 'compaction')) fail('upstream_error'); return { binding: binding(active(assistantID)), summarized: true }; };
|
|
831
1138
|
const sendWithConfig = async ({ row, sessionID, directory, config, parts, messageID, restore }) => {
|
|
832
|
-
const sendPrompt = (targetSessionID,
|
|
833
|
-
|
|
1139
|
+
const sendPrompt = async (targetSessionID, targetConfig) => invokeSession(async () => {
|
|
1140
|
+
const api = client();
|
|
1141
|
+
if (typeof api.session.switchAgent === 'function' && targetConfig?.agent) await api.session.switchAgent({ sessionID: targetSessionID, agent: targetConfig.agent });
|
|
1142
|
+
if (typeof api.session.switchModel === 'function' && targetConfig?.model) await api.session.switchModel({ sessionID: targetSessionID, model: { id: targetConfig.model.modelID, providerID: targetConfig.model.providerID, ...(targetConfig.variant ? { variant: targetConfig.variant } : {}) } });
|
|
1143
|
+
if (typeof api.session.instructions?.entry?.put === 'function' && typeof targetConfig?.system === 'string' && targetConfig.system) await api.session.instructions.entry.put({ sessionID: targetSessionID, key: 'system', value: targetConfig.system });
|
|
1144
|
+
return api.session.prompt(toV2PromptInput(targetSessionID, parts, messageID));
|
|
1145
|
+
});
|
|
1146
|
+
let result = await sendPrompt(sessionID, config);
|
|
834
1147
|
if (isMissing(result) && restore) {
|
|
835
1148
|
const restored = await restoreOnce(row, sessionID, row.session_generation);
|
|
836
1149
|
const target = active(row.assistant_id);
|
|
837
1150
|
const targetConfig = configuration(target);
|
|
838
|
-
result = await sendPrompt(restored.sessionID,
|
|
1151
|
+
result = await sendPrompt(restored.sessionID, targetConfig);
|
|
839
1152
|
if (promptAdmitted(result)) mirrorAdmittedUserMessage(target, restored.sessionID, messageID, parts, targetConfig);
|
|
840
1153
|
return { result, binding: restored };
|
|
841
1154
|
}
|
|
@@ -1012,12 +1325,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1012
1325
|
parts: [card],
|
|
1013
1326
|
});
|
|
1014
1327
|
if (card.cardType === 'session' && card.sessionID) {
|
|
1015
|
-
|
|
1328
|
+
const deliveryMessageID = pendingAssignMessageIDs.get(card.sessionID)
|
|
1329
|
+
|| (nonEmptyString(card.messageID) ? card.messageID : null);
|
|
1330
|
+
if (pendingAssignMessageIDs.has(card.sessionID)) pendingAssignMessageIDs.delete(card.sessionID);
|
|
1331
|
+
upsertContactWatch(db, {
|
|
1016
1332
|
assistantID,
|
|
1017
1333
|
sessionID: card.sessionID,
|
|
1018
1334
|
directory: card.directory,
|
|
1019
1335
|
status: card.status === 'error' || card.status === 'question' || card.status === 'complete' ? card.status : 'busy',
|
|
1020
1336
|
updatedAt: now(),
|
|
1337
|
+
...(deliveryMessageID ? { messageID: deliveryMessageID } : {}),
|
|
1021
1338
|
});
|
|
1022
1339
|
}
|
|
1023
1340
|
bump();
|
|
@@ -1115,12 +1432,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1115
1432
|
});
|
|
1116
1433
|
ordinal += 1;
|
|
1117
1434
|
if (card.cardType === 'session' && card.sessionID) {
|
|
1118
|
-
|
|
1435
|
+
const deliveryMessageID = pendingAssignMessageIDs.get(card.sessionID)
|
|
1436
|
+
|| (nonEmptyString(card.messageID) ? card.messageID : null);
|
|
1437
|
+
if (pendingAssignMessageIDs.has(card.sessionID)) pendingAssignMessageIDs.delete(card.sessionID);
|
|
1438
|
+
upsertContactWatch(db, {
|
|
1119
1439
|
assistantID,
|
|
1120
1440
|
sessionID: card.sessionID,
|
|
1121
1441
|
directory: card.directory,
|
|
1122
1442
|
status: card.status === 'error' || card.status === 'question' || card.status === 'complete' ? card.status : 'busy',
|
|
1123
1443
|
updatedAt: now(),
|
|
1444
|
+
...(deliveryMessageID ? { messageID: deliveryMessageID } : {}),
|
|
1124
1445
|
});
|
|
1125
1446
|
}
|
|
1126
1447
|
});
|
|
@@ -1259,16 +1580,19 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1259
1580
|
};
|
|
1260
1581
|
const v2 = await viaExact(api?.v2?.session?.message, api?.v2?.session);
|
|
1261
1582
|
if (v2) return v2;
|
|
1583
|
+
// Official 2.x: session.message.get; shim may also expose callable session.message.
|
|
1584
|
+
const nestedGet = await viaExact(api?.session?.message?.get, api?.session?.message);
|
|
1585
|
+
if (nestedGet) return nestedGet;
|
|
1262
1586
|
const legacy = await viaExact(api?.session?.message, api?.session);
|
|
1263
1587
|
if (legacy) return legacy;
|
|
1264
1588
|
try {
|
|
1265
|
-
const listed = await api.
|
|
1589
|
+
const listed = await api.message.list({
|
|
1266
1590
|
sessionID,
|
|
1267
|
-
...(directory ? { directory } : {}),
|
|
1268
1591
|
limit: 100,
|
|
1592
|
+
order: 'desc',
|
|
1269
1593
|
}, options);
|
|
1270
1594
|
if (listed?.error) return { found: false };
|
|
1271
|
-
const rows = Array.isArray(listed?.data) ? listed.data : (Array.isArray(listed) ? listed : []);
|
|
1595
|
+
const rows = projectionEntries(listed, sessionID) ?? (Array.isArray(listed?.data) ? listed.data : (Array.isArray(listed) ? listed : []));
|
|
1272
1596
|
const hit = rows.some((row) => identity(row) === messageID);
|
|
1273
1597
|
return { found: hit };
|
|
1274
1598
|
} catch {
|
|
@@ -1295,14 +1619,14 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1295
1619
|
const fromSession = extractAssignSessionModel({ session: sessionPayload });
|
|
1296
1620
|
if (fromSession) return fromSession;
|
|
1297
1621
|
try {
|
|
1298
|
-
if (typeof api.
|
|
1299
|
-
const listed = await api.
|
|
1622
|
+
if (typeof api.message?.list !== 'function') return null;
|
|
1623
|
+
const listed = await api.message.list({
|
|
1300
1624
|
sessionID,
|
|
1301
|
-
...(dir ? { directory: dir } : {}),
|
|
1302
1625
|
limit: 40,
|
|
1626
|
+
order: 'desc',
|
|
1303
1627
|
}, options);
|
|
1304
1628
|
if (listed?.error) return null;
|
|
1305
|
-
messages = listed?.data ?? listed;
|
|
1629
|
+
messages = projectionEntries(listed, sessionID) ?? listed?.data ?? listed;
|
|
1306
1630
|
} catch {
|
|
1307
1631
|
return null;
|
|
1308
1632
|
}
|
|
@@ -1456,12 +1780,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1456
1780
|
}
|
|
1457
1781
|
|
|
1458
1782
|
let messagesResult = null;
|
|
1459
|
-
if (includeMessages && typeof client().
|
|
1783
|
+
if (includeMessages && typeof client().message?.list === 'function') {
|
|
1460
1784
|
try {
|
|
1461
|
-
messagesResult = await client().
|
|
1785
|
+
messagesResult = await client().message.list({
|
|
1462
1786
|
sessionID,
|
|
1463
|
-
directory,
|
|
1464
1787
|
limit: 100,
|
|
1788
|
+
order: 'desc',
|
|
1465
1789
|
}, options);
|
|
1466
1790
|
if (messagesResult?.error && !isMissing(messagesResult)) {
|
|
1467
1791
|
// Message fetch failure must not invent status; keep messages null.
|
|
@@ -1499,10 +1823,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1499
1823
|
const { signal } = params;
|
|
1500
1824
|
signal?.throwIfAborted();
|
|
1501
1825
|
const resolved = await resolveReferencedSession({ sessionID: params.sessionID, signal, includeMessages: false });
|
|
1502
|
-
const result = await client().
|
|
1503
|
-
sessionID: resolved.sessionID,
|
|
1504
|
-
...(params.before ? {
|
|
1505
|
-
}
|
|
1826
|
+
const result = await invokeSession(() => client().message.list({
|
|
1827
|
+
sessionID: resolved.sessionID, limit, order: 'desc',
|
|
1828
|
+
...(params.before ? { cursor: params.before } : {}),
|
|
1829
|
+
}));
|
|
1506
1830
|
signal?.throwIfAborted();
|
|
1507
1831
|
if (isMissing(result)) throw new AssignError(ASSIGN_CODES.NOT_FOUND, 'That session no longer exists.');
|
|
1508
1832
|
if (result?.error || !Array.isArray(result?.data)) throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Could not read session messages.');
|
|
@@ -1614,7 +1938,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1614
1938
|
}
|
|
1615
1939
|
// Worker model is prompt-only. Never mutate the contact assistant row.
|
|
1616
1940
|
// Forward tool variant + AbortSignal; lookupMessage uses real SDK exact/list paths.
|
|
1617
|
-
|
|
1941
|
+
const assigned = await assignSession({
|
|
1618
1942
|
...params,
|
|
1619
1943
|
...(reuseScope ? {
|
|
1620
1944
|
sessionID: reuseScope.sessionID,
|
|
@@ -1641,16 +1965,43 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1641
1965
|
allowedRoots: getAllowedRoots(),
|
|
1642
1966
|
managedWorkspaceRoot: path.resolve(dataDir, 'assistant-workspaces'),
|
|
1643
1967
|
listWorktrees,
|
|
1644
|
-
createSession: (created) => client().session.create(
|
|
1645
|
-
|
|
1968
|
+
createSession: (created) => client().session.create({
|
|
1969
|
+
title: created?.title,
|
|
1970
|
+
...(created?.directory ? { location: { directory: created.directory } } : {}),
|
|
1971
|
+
...(created?.agent ? { agent: created.agent } : {}),
|
|
1972
|
+
...(created?.model ? { model: created.model } : {}),
|
|
1973
|
+
...(created?.metadata ? { metadata: created.metadata } : {}),
|
|
1974
|
+
}),
|
|
1975
|
+
promptExisting: async (prompted) => {
|
|
1976
|
+
const api = client();
|
|
1977
|
+
if (typeof api.session.switchAgent === 'function' && prompted.agent) {
|
|
1978
|
+
await api.session.switchAgent({ sessionID: prompted.sessionID, agent: prompted.agent });
|
|
1979
|
+
}
|
|
1980
|
+
if (typeof api.session.switchModel === 'function' && prompted.model) {
|
|
1981
|
+
await api.session.switchModel({
|
|
1982
|
+
sessionID: prompted.sessionID,
|
|
1983
|
+
model: {
|
|
1984
|
+
id: prompted.model.modelID,
|
|
1985
|
+
providerID: prompted.model.providerID,
|
|
1986
|
+
...(prompted.variant ? { variant: prompted.variant } : {}),
|
|
1987
|
+
},
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
return api.session.prompt(toV2PromptInput(prompted.sessionID, prompted.parts, prompted.messageID));
|
|
1991
|
+
},
|
|
1646
1992
|
deleteSession: (deleted) => (
|
|
1647
|
-
typeof client().session.
|
|
1648
|
-
? client().session.
|
|
1993
|
+
typeof client().session.remove === 'function'
|
|
1994
|
+
? client().session.remove({ sessionID: deleted?.sessionID, ...(deleted?.directory ? {} : {}) })
|
|
1649
1995
|
: undefined
|
|
1650
1996
|
),
|
|
1651
1997
|
lookupMessage: (lookup) => lookupAssignMessage({ ...lookup, signal }),
|
|
1652
1998
|
messageID: params?.messageID || `msg_assign_${id()}`,
|
|
1653
1999
|
});
|
|
2000
|
+
// Stash until the session card watch is written so reuse cannot settle on an old tail.
|
|
2001
|
+
if (nonEmptyString(assigned?.sessionID) && nonEmptyString(assigned?.messageID)) {
|
|
2002
|
+
pendingAssignMessageIDs.set(assigned.sessionID, assigned.messageID);
|
|
2003
|
+
}
|
|
2004
|
+
return assigned;
|
|
1654
2005
|
};
|
|
1655
2006
|
/** Watch an existing session only — no prompt. Baseline status avoids false settle resume. */
|
|
1656
2007
|
const watchSessionWork = async (_row, params = {}) => {
|
|
@@ -1675,7 +2026,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1675
2026
|
reused: true,
|
|
1676
2027
|
};
|
|
1677
2028
|
};
|
|
1678
|
-
/** Abort an existing coding session via real OpenCode session.
|
|
2029
|
+
/** Abort an existing coding session via real OpenCode session.interrupt. */
|
|
1679
2030
|
const stopSessionWork = async (_row, params = {}) => {
|
|
1680
2031
|
const sessionID = typeof params.sessionID === 'string' ? params.sessionID.trim() : '';
|
|
1681
2032
|
if (!sessionID) {
|
|
@@ -1690,8 +2041,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1690
2041
|
});
|
|
1691
2042
|
const signal = params.signal;
|
|
1692
2043
|
signal?.throwIfAborted();
|
|
1693
|
-
if (typeof client().session.
|
|
1694
|
-
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session
|
|
2044
|
+
if (typeof client().session.interrupt !== 'function') {
|
|
2045
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session interrupt is unavailable.');
|
|
1695
2046
|
}
|
|
1696
2047
|
const targetID = resolved.sessionID;
|
|
1697
2048
|
// Share one abort lifecycle per worker session (register before await abort).
|
|
@@ -1719,10 +2070,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1719
2070
|
let result;
|
|
1720
2071
|
try {
|
|
1721
2072
|
signal?.throwIfAborted();
|
|
1722
|
-
result = await client().session.
|
|
2073
|
+
result = await invokeSession(() => client().session.interrupt({
|
|
1723
2074
|
sessionID: targetID,
|
|
1724
|
-
|
|
1725
|
-
}, sdkOptions);
|
|
2075
|
+
}));
|
|
1726
2076
|
} catch (error) {
|
|
1727
2077
|
const observed = entry.observed;
|
|
1728
2078
|
if (sessionAbortInflight.get(targetID) === entry) sessionAbortInflight.delete(targetID);
|
|
@@ -1784,27 +2134,42 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1784
2134
|
}
|
|
1785
2135
|
const resolved = await resolveReferencedSession({ sessionID, signal: params.signal, includeMessages: false });
|
|
1786
2136
|
params.signal?.throwIfAborted();
|
|
1787
|
-
const
|
|
1788
|
-
const method = operation === 'steer' ? 'promptAsync' : operation === 'archive' ? 'update' : 'delete';
|
|
1789
|
-
if (typeof session[method] !== 'function') throw new AssignError(ASSIGN_CODES.UPSTREAM, `Session ${operation} is unavailable.`);
|
|
1790
|
-
const request = { sessionID, directory: resolved.directory };
|
|
1791
|
-
if (operation === 'steer') Object.assign(request, {
|
|
1792
|
-
messageID: `msg_steer_${id()}`, delivery: 'steer', parts: [{ type: 'text', text: params.text.trim() }],
|
|
1793
|
-
});
|
|
1794
|
-
if (operation === 'archive') request.time = { archived: now() };
|
|
2137
|
+
const api = client();
|
|
1795
2138
|
let result;
|
|
1796
2139
|
try {
|
|
1797
|
-
|
|
2140
|
+
if (operation === 'steer') {
|
|
2141
|
+
const messageID = `msg_steer_${id()}`;
|
|
2142
|
+
const parts = [{ type: 'text', text: params.text.trim() }];
|
|
2143
|
+
result = await invokeSession(() => api.session.prompt(toV2PromptInput(sessionID, parts, messageID)));
|
|
2144
|
+
if (isMissing(result)) throw new AssignError(ASSIGN_CODES.NOT_FOUND, `No coding session ${sessionID} was found.`);
|
|
2145
|
+
if (result?.error || !promptAdmitted(result)) {
|
|
2146
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, `Session ${operation} was not confirmed. Do not resend automatically.`);
|
|
2147
|
+
}
|
|
2148
|
+
return { sessionID, directory: resolved.directory, operation, messageID, admitted: true };
|
|
2149
|
+
}
|
|
2150
|
+
if (operation === 'archive') {
|
|
2151
|
+
if (typeof api.session.update !== 'function') {
|
|
2152
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session archive is unavailable.');
|
|
2153
|
+
}
|
|
2154
|
+
result = await invokeSession(() => api.session.update({ sessionID, directory: resolved.directory, time: { archived: now() } }));
|
|
2155
|
+
} else if (operation === 'delete') {
|
|
2156
|
+
if (typeof api.session.remove !== 'function') {
|
|
2157
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session delete is unavailable.');
|
|
2158
|
+
}
|
|
2159
|
+
result = await invokeSession(() => api.session.remove({ sessionID }));
|
|
2160
|
+
} else {
|
|
2161
|
+
throw new AssignError(ASSIGN_CODES.VALIDATION, `Unknown session operation ${operation}.`);
|
|
2162
|
+
}
|
|
1798
2163
|
} catch (error) {
|
|
2164
|
+
if (error instanceof AssignError) throw error;
|
|
1799
2165
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, error?.message || `Session ${operation} failed.`);
|
|
1800
2166
|
}
|
|
1801
2167
|
if (isMissing(result)) throw new AssignError(ASSIGN_CODES.NOT_FOUND, `No coding session ${sessionID} was found.`);
|
|
1802
|
-
if (result?.error || result?.data === false
|
|
1803
|
-
// Do not retry an ambiguous prompt submission: it may already be admitted upstream.
|
|
2168
|
+
if (result?.error || result?.data === false) {
|
|
1804
2169
|
throw new AssignError(ASSIGN_CODES.UPSTREAM, `Session ${operation} was not confirmed. Do not resend automatically.`);
|
|
1805
2170
|
}
|
|
1806
|
-
|
|
1807
|
-
return { sessionID, directory: resolved.directory, operation, ...(operation === '
|
|
2171
|
+
reportAssignedSession(sessionID, 'cancelled', false);
|
|
2172
|
+
return { sessionID, directory: resolved.directory, operation, ...(operation === 'archive' ? { archived: true } : { deleted: true }) };
|
|
1808
2173
|
};
|
|
1809
2174
|
const matchRegisteredProject = async (directory) => {
|
|
1810
2175
|
const projects = await listProjects();
|
|
@@ -2192,11 +2557,11 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2192
2557
|
try { return effectiveWorkspace(live); } catch { return null; }
|
|
2193
2558
|
})();
|
|
2194
2559
|
if (resolvedDirectory) {
|
|
2195
|
-
const messagesResult = await client().
|
|
2560
|
+
const messagesResult = await invokeSession(() => client().message.list({
|
|
2196
2561
|
sessionID,
|
|
2197
|
-
directory: resolvedDirectory,
|
|
2198
2562
|
limit: 100,
|
|
2199
|
-
|
|
2563
|
+
order: 'desc',
|
|
2564
|
+
}));
|
|
2200
2565
|
if (!messagesResult?.error) {
|
|
2201
2566
|
const rows = Array.isArray(messagesResult?.data)
|
|
2202
2567
|
? messagesResult.data
|
|
@@ -2959,12 +3324,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2959
3324
|
parts: [card],
|
|
2960
3325
|
});
|
|
2961
3326
|
if (card.cardType === 'session' && card.sessionID) {
|
|
3327
|
+
const deliveryMessageID = pendingAssignMessageIDs.get(card.sessionID)
|
|
3328
|
+
|| (nonEmptyString(card.messageID) ? card.messageID : null);
|
|
3329
|
+
if (pendingAssignMessageIDs.has(card.sessionID)) pendingAssignMessageIDs.delete(card.sessionID);
|
|
2962
3330
|
upsertContactWatch(db, {
|
|
2963
3331
|
assistantID: row.assistant_id,
|
|
2964
3332
|
sessionID: card.sessionID,
|
|
2965
3333
|
directory: card.directory,
|
|
2966
3334
|
status: card.status === 'error' || card.status === 'question' || card.status === 'complete' ? card.status : 'busy',
|
|
2967
3335
|
updatedAt: now(),
|
|
3336
|
+
...(deliveryMessageID ? { messageID: deliveryMessageID } : {}),
|
|
2968
3337
|
});
|
|
2969
3338
|
}
|
|
2970
3339
|
bump();
|
|
@@ -2977,7 +3346,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
2977
3346
|
};
|
|
2978
3347
|
/**
|
|
2979
3348
|
* Read-only inter-assistant DM. Inserts into the recipient's OpenChamber
|
|
2980
|
-
* contact transcript only. Must never call OpenCode
|
|
3349
|
+
* contact transcript only. Must never call OpenCode session.prompt, mutate
|
|
2981
3350
|
* sessions/files/worktrees, or run tools on the recipient's behalf.
|
|
2982
3351
|
*
|
|
2983
3352
|
* Assign opens a worker session on the sender's contact turn — never here.
|
|
@@ -3068,17 +3437,17 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
3068
3437
|
db.prepare('UPDATE assistant_contact_watch SET resume_allowed=0 WHERE assistant_id=?').run(assistantID);
|
|
3069
3438
|
if (interrupted) return { binding: binding(row), aborted: true };
|
|
3070
3439
|
if (!row.current_session_id) fail('revision_conflict');
|
|
3071
|
-
const result = await client().session.
|
|
3440
|
+
const result = await invokeSession(() => client().session.interrupt({ sessionID: row.current_session_id }));
|
|
3072
3441
|
if (isMissing(result)) fail('not_found');
|
|
3073
|
-
if (result
|
|
3442
|
+
if (result?.error) fail('upstream_error');
|
|
3074
3443
|
return { binding: binding(row), aborted: true };
|
|
3075
3444
|
};
|
|
3076
3445
|
const createNew = async (assistantID) => { for (let attempt = 0; attempt < 3; attempt++) { const row = active(assistantID); const created = await createSession(row); const won = replaceBinding(row, created); if (won) return binding(won); } return binding(active(assistantID)); };
|
|
3077
3446
|
const shareOperation = (operationID) => { const row = db.prepare('SELECT * FROM assistant_share_operation WHERE operation_id=?').get(operationID); return row && { operationID: row.operation_id, assistantID: row.assistant_id, sessionID: row.session_id, messageID: row.message_id, state: row.state, phase: row.phase, attempt: row.attempt, leaseExpiresAt: row.lease_expires_at, errorCode: row.error_code }; };
|
|
3078
3447
|
const claim = (operationID, retry = false) => { db.exec('BEGIN IMMEDIATE'); try { const operation = db.prepare('SELECT * FROM assistant_share_operation WHERE operation_id=?').get(operationID); if (!operation) { db.exec('COMMIT'); return null; } const at = now(); const eligible = operation.state === 'failed' ? retry : operation.state === 'running' && operation.lease_expires_at <= at && operation.phase === 'admitted'; if (!eligible || operation.attempt >= SHARE_MAX_ATTEMPTS) { db.exec('COMMIT'); return null; } const result = db.prepare("UPDATE assistant_share_operation SET state='running',phase='submitting',attempt=attempt+1,lease_expires_at=?,error_code=NULL,updated_at=? WHERE operation_id=? AND state=? AND phase=? AND attempt<? AND (state='failed' OR lease_expires_at<=?)").run(at + SHARE_LEASE_MS, at, operationID, operation.state, operation.phase, SHARE_MAX_ATTEMPTS, at); const claimed = result.changes ? db.prepare('SELECT * FROM assistant_share_operation WHERE operation_id=?').get(operationID) : null; db.exec('COMMIT'); return claimed; } catch (error) { db.exec('ROLLBACK'); throw error; } };
|
|
3079
3448
|
const completeOrFail = (operation, errorCode = null) => { const at = now(); if (errorCode) db.prepare("UPDATE assistant_share_operation SET state='failed',phase='admitted',error_code=?,lease_expires_at=NULL,updated_at=? WHERE operation_id=? AND state='running' AND phase='submitting'").run(errorCode, at, operation.operation_id); else db.prepare("UPDATE assistant_share_operation SET state='running',phase='submitted',error_code=NULL,lease_expires_at=?,updated_at=? WHERE operation_id=? AND state='running' AND phase='submitting'").run(at + SHARE_LEASE_MS, at, operation.operation_id); };
|
|
3080
|
-
const submitClaim = async (operation) => { const payload = parse(operation.response); const row = active(operation.assistant_id); try { const config = configuration(row); const
|
|
3081
|
-
const reconcileShareOperations = async () => { const candidates = db.prepare("SELECT * FROM assistant_share_operation WHERE state='running'").all(); for (const operation of candidates) { const row = assistant(operation.assistant_id); if (!row || !operation.session_id || !operation.message_id) continue; try { const result = await client().
|
|
3449
|
+
const submitClaim = async (operation) => { const payload = parse(operation.response); const row = active(operation.assistant_id); try { const config = configuration(row); const sent = await sendWithConfig({ row, sessionID: operation.session_id, directory: effectiveWorkspace(row), config, parts: payload.parts, messageID: operation.message_id, restore: false }); if (!promptAdmitted(sent.result)) fail('upstream_error'); completeOrFail(operation); } catch (error) { completeOrFail(operation, error instanceof AssistantError ? error.code : 'upstream_error'); } };
|
|
3450
|
+
const reconcileShareOperations = async () => { const candidates = db.prepare("SELECT * FROM assistant_share_operation WHERE state='running'").all(); for (const operation of candidates) { const row = assistant(operation.assistant_id); if (!row || !operation.session_id || !operation.message_id) continue; try { const result = await invokeSession(() => client().message.list({ sessionID: operation.session_id, limit: 100, order: 'desc' })); if (result?.error) continue; const messages = projectionEntries(result, operation.session_id) ?? result.data ?? []; const found = Array.isArray(messages) && messages.some((message) => message?.info?.id === operation.message_id || message?.id === operation.message_id); if (found) db.prepare("UPDATE assistant_share_operation SET state='completed',phase='submitted',lease_expires_at=NULL,updated_at=? WHERE operation_id=? AND state='running'").run(now(), operation.operation_id); else if (operation.phase === 'admitted' && operation.lease_expires_at <= now() && operation.attempt >= SHARE_MAX_ATTEMPTS) db.prepare("UPDATE assistant_share_operation SET state='failed',lease_expires_at=NULL,error_code='attempt_limit',updated_at=? WHERE operation_id=? AND state='running' AND phase='admitted'").run(now(), operation.operation_id); else if (operation.phase === 'admitted' && operation.lease_expires_at <= now()) { const claimed = claim(operation.operation_id); if (claimed) void submitClaim(claimed); } else if (operation.phase === 'submitted' && operation.lease_expires_at <= now()) db.prepare("UPDATE assistant_share_operation SET state='unresolved',lease_expires_at=NULL,error_code='message_unresolved',updated_at=? WHERE operation_id=? AND state='running' AND phase='submitted'").run(now(), operation.operation_id); } catch { /* Reconciliation remains retryable until its lease expires. */ } } };
|
|
3082
3451
|
const reconcile = async () => {
|
|
3083
3452
|
if (closed) return;
|
|
3084
3453
|
try {
|
|
@@ -3154,7 +3523,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
3154
3523
|
)),
|
|
3155
3524
|
};
|
|
3156
3525
|
};
|
|
3157
|
-
return { capability: async () => ({ supported: true, enabled: enabled(), revision: revision(), serverInstanceID: await getServerId() }), snapshot, createAssistant, updateAssistant, setEnabled: (input) => { if (!plainObject(input) || typeof input.enabled !== 'boolean' || input.expectedRevision !== revision()) fail('revision_conflict'); db.prepare("UPDATE assistant_meta SET value=? WHERE key='enabled'").run(input.enabled ? '1' : '0'); return { enabled: input.enabled, revision: bump() }; }, removeAssistant: (assistantID, expectedRevision) => {
|
|
3526
|
+
return { capability: async () => ({ supported: true, enabled: enabled(), revision: revision(), serverInstanceID: await getServerId(), sharingAvailable: false, archiveMetadataAvailable: false, sessionMetadataAvailable: false }), snapshot, createAssistant, updateAssistant, setEnabled: (input) => { if (!plainObject(input) || typeof input.enabled !== 'boolean' || input.expectedRevision !== revision()) fail('revision_conflict'); db.prepare("UPDATE assistant_meta SET value=? WHERE key='enabled'").run(input.enabled ? '1' : '0'); return { enabled: input.enabled, revision: bump() }; }, removeAssistant: (assistantID, expectedRevision) => {
|
|
3158
3527
|
db.exec('BEGIN IMMEDIATE');
|
|
3159
3528
|
try {
|
|
3160
3529
|
const result = db.prepare('UPDATE assistant_v2 SET tombstone_at=?,revision=revision+1,updated_at=? WHERE assistant_id=? AND revision=? AND tombstone_at IS NULL').run(now(), now(), assistantID, expectedRevision);
|
|
@@ -3178,6 +3547,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
3178
3547
|
}, ensure, createNew, compact, send, whenContactTurnSettled, abort, captureQueueDeliveryTarget, sendWithCapturedConfig, share, shareOperation, historicalMessages, contactMessages, clearContactMemory, resetContact, markContactRead, appendContactCard, deliverPeerMessage, listAssistantScheduledTasks, putAssistantContactAttachment, getAssistantContactAttachment, migrateContactAttachments, processEvent, reportAssignedSessionSettle: reportAssignedSession, reconcile, close: () => {
|
|
3179
3548
|
cancelAssignedResumes({});
|
|
3180
3549
|
sessionAbortInflight.clear();
|
|
3550
|
+
pendingAssignMessageIDs.clear();
|
|
3181
3551
|
for (const turn of contactControllers.values()) turn.controller.abort();
|
|
3182
3552
|
if (!closed) {
|
|
3183
3553
|
closed = true;
|