@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
|
@@ -11,13 +11,17 @@ import {
|
|
|
11
11
|
CONTACT_SETTLE_TEXT,
|
|
12
12
|
clearContactMemory as clearContactMemoryStore,
|
|
13
13
|
contactHistoryForLlm,
|
|
14
|
+
createActiveContactTurn,
|
|
14
15
|
deleteContactMessages,
|
|
15
16
|
ensureContactSchema,
|
|
17
|
+
contactPartsFingerprint,
|
|
18
|
+
getContactMessage,
|
|
16
19
|
insertContactMessage,
|
|
17
20
|
listContactMessages,
|
|
18
21
|
listInFlightWatches,
|
|
19
22
|
listWatchesBySession,
|
|
20
23
|
nextContactOrdinal,
|
|
24
|
+
projectActiveContactTurn,
|
|
21
25
|
updateSessionCardStatus,
|
|
22
26
|
upsertContactWatch,
|
|
23
27
|
} from './contact-store.js';
|
|
@@ -31,12 +35,23 @@ import {
|
|
|
31
35
|
filterRegisteredProjects,
|
|
32
36
|
matchesProjectQuery,
|
|
33
37
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
38
|
+
normalizeConnectedModels,
|
|
34
39
|
normalizeRegisteredProjects,
|
|
35
40
|
sanitizeRegisteredProject,
|
|
36
41
|
userTextAuthorizesClearChatHistory,
|
|
37
42
|
} from './contact-tools.js';
|
|
38
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
AssignError,
|
|
45
|
+
ASSIGN_CODES,
|
|
46
|
+
assignSession,
|
|
47
|
+
attachmentScopeKey,
|
|
48
|
+
hasAssignImageParts,
|
|
49
|
+
resolveAssignDirectory,
|
|
50
|
+
resolveAssignWorkerModel,
|
|
51
|
+
sanitizeAssignFileParts,
|
|
52
|
+
} from './assign.js';
|
|
39
53
|
import { runContactTurn as defaultRunContactTurn } from './harness.js';
|
|
54
|
+
import { loadConnectedCatalog } from '../llm/catalog.js';
|
|
40
55
|
|
|
41
56
|
const require = createRequire(import.meta.url);
|
|
42
57
|
const SCHEMA_VERSION = 11;
|
|
@@ -92,6 +107,25 @@ const isTransientMessagesFailure = (result, error) => {
|
|
|
92
107
|
};
|
|
93
108
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
94
109
|
const promptAdmitted = (result) => !result?.error && (result?.response?.status === 204 || result?.status === 204 || result?.data !== undefined || result?.response?.ok === true);
|
|
110
|
+
/** Bound catalog / project list waits so a contact lane cannot hang forever. */
|
|
111
|
+
export const CONTACT_CATALOG_DEADLINE_MS = 8_000;
|
|
112
|
+
const awaitWithDeadline = async (work, ms = CONTACT_CATALOG_DEADLINE_MS, code = 'upstream_error') => {
|
|
113
|
+
let timer = null;
|
|
114
|
+
try {
|
|
115
|
+
return await Promise.race([
|
|
116
|
+
Promise.resolve().then(() => work),
|
|
117
|
+
new Promise((_, reject) => {
|
|
118
|
+
timer = setTimeout(() => {
|
|
119
|
+
const error = new Error(code);
|
|
120
|
+
error.code = code;
|
|
121
|
+
reject(error);
|
|
122
|
+
}, ms);
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
} finally {
|
|
126
|
+
if (timer) clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
95
129
|
|
|
96
130
|
export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, getOpenCodeAuthHeaders, getServerId = async () => null, getAllowedRoots = () => [], listProjects = async () => [], 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 } = {}) => {
|
|
97
131
|
if (!dbPath || !dataDir) return null;
|
|
@@ -100,6 +134,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
100
134
|
const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON');
|
|
101
135
|
let closed = false;
|
|
102
136
|
const shareReservations = new Map();
|
|
137
|
+
// Process-local contact-turn activity. Not durable across server restart —
|
|
138
|
+
// a new process starts empty so working cannot stick green forever. APP
|
|
139
|
+
// restart rehydrates from snapshot while this process still holds the turn.
|
|
140
|
+
const activeContactTurnsByAssistant = new Map();
|
|
103
141
|
db.exec(`CREATE TABLE IF NOT EXISTS assistant_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
104
142
|
CREATE TABLE IF NOT EXISTS assistant_v2 (assistant_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, enabled INTEGER NOT NULL, name TEXT NOT NULL, default_prompt TEXT NOT NULL, workspace_path TEXT, provider_id TEXT NOT NULL, model_id TEXT NOT NULL, agent TEXT, variant TEXT, mode TEXT NOT NULL DEFAULT 'continuous', current_session_id TEXT, session_generation INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, tombstone_at INTEGER);
|
|
105
143
|
CREATE TABLE IF NOT EXISTS assistant_share_operation (operation_id TEXT PRIMARY KEY, assistant_id TEXT NOT NULL, payload_hash TEXT NOT NULL, phase TEXT NOT NULL, session_id TEXT, message_id TEXT, state TEXT NOT NULL, response TEXT, error_code TEXT, attempt INTEGER NOT NULL DEFAULT 0, lease_expires_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
|
|
@@ -168,8 +206,52 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
168
206
|
db.prepare('DELETE FROM assistant_message_backfill WHERE assistant_id=? AND session_id=?').run(assistantID, sessionID);
|
|
169
207
|
db.prepare("UPDATE assistant_message_mirror SET covered=0 WHERE assistant_id=? AND session_id=? AND COALESCE(json_extract(info_json,'$.role'),'')<>'user' AND COALESCE(json_extract(info_json,'$.openchamberAssistantAdmission'),0)<>1").run(assistantID, sessionID);
|
|
170
208
|
};
|
|
209
|
+
const activeContactTurnsFor = (assistantID) => activeContactTurnsByAssistant.get(assistantID) ?? new Map();
|
|
210
|
+
const rememberActiveContactTurn = (assistantID, input) => {
|
|
211
|
+
const next = createActiveContactTurn(input);
|
|
212
|
+
if (!next) return null;
|
|
213
|
+
const turns = activeContactTurnsFor(assistantID);
|
|
214
|
+
turns.set(next.turnID, next);
|
|
215
|
+
activeContactTurnsByAssistant.set(assistantID, turns);
|
|
216
|
+
return next;
|
|
217
|
+
};
|
|
218
|
+
const markActiveContactTurnRunning = (assistantID, turnID) => {
|
|
219
|
+
const turns = activeContactTurnsFor(assistantID);
|
|
220
|
+
const current = turns.get(turnID);
|
|
221
|
+
if (!current) return null;
|
|
222
|
+
if (current.status === 'running') return current;
|
|
223
|
+
const next = createActiveContactTurn({ ...current, status: 'running' });
|
|
224
|
+
if (!next) return null;
|
|
225
|
+
turns.set(turnID, next);
|
|
226
|
+
activeContactTurnsByAssistant.set(assistantID, turns);
|
|
227
|
+
// queued → running must tip snapshot so clients poll authoritative status.
|
|
228
|
+
bump();
|
|
229
|
+
return next;
|
|
230
|
+
};
|
|
231
|
+
const settleActiveContactTurn = (assistantID, turnID, { bumpRevision = false } = {}) => {
|
|
232
|
+
const turns = activeContactTurnsFor(assistantID);
|
|
233
|
+
if (!turns.has(turnID)) {
|
|
234
|
+
if (bumpRevision) bump();
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
turns.delete(turnID);
|
|
238
|
+
if (turns.size === 0) activeContactTurnsByAssistant.delete(assistantID);
|
|
239
|
+
else activeContactTurnsByAssistant.set(assistantID, turns);
|
|
240
|
+
// Error paths may not persist assistant rows; still tip the snapshot so
|
|
241
|
+
// list green dots clear without waiting for a later SQLite write.
|
|
242
|
+
if (bumpRevision) bump();
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
245
|
+
const clearActiveContactTurns = (assistantID) => {
|
|
246
|
+
if (assistantID == null) {
|
|
247
|
+
activeContactTurnsByAssistant.clear();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
activeContactTurnsByAssistant.delete(assistantID);
|
|
251
|
+
};
|
|
171
252
|
const output = (row) => {
|
|
172
253
|
const watches = listInFlightWatches(db, row.assistant_id);
|
|
254
|
+
const activeContactTurn = projectActiveContactTurn(activeContactTurnsFor(row.assistant_id));
|
|
173
255
|
return {
|
|
174
256
|
id: row.assistant_id,
|
|
175
257
|
revision: row.revision,
|
|
@@ -189,7 +271,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
189
271
|
historySessionIDs: historyIDs(row.assistant_id),
|
|
190
272
|
historySessionCount: historyCount(row.assistant_id),
|
|
191
273
|
assignedSessionIDs: watches.map((watch) => watch.sessionID),
|
|
192
|
-
|
|
274
|
+
// Grok-Bot list green dot: contact turn only. Assigned-session busy stays
|
|
275
|
+
// on the session card / assignedSessionIDs and must not keep the avatar lit.
|
|
276
|
+
working: activeContactTurn != null,
|
|
277
|
+
activeContactTurn,
|
|
193
278
|
createdAt: row.created_at,
|
|
194
279
|
updatedAt: row.updated_at,
|
|
195
280
|
tombstoneAt: row.tombstone_at,
|
|
@@ -672,6 +757,31 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
672
757
|
throw error;
|
|
673
758
|
}
|
|
674
759
|
};
|
|
760
|
+
/** Durable failure bubble so APP restart can recover error without SSE. */
|
|
761
|
+
const persistContactTurnFailure = (assistantID, { userMessageID, turnID, error }) => {
|
|
762
|
+
const detail = typeof error === 'string' && error.trim() ? error.trim() : 'upstream_error';
|
|
763
|
+
const failureID = `${userMessageID}:error`;
|
|
764
|
+
if (getContactMessage(db, failureID)) return;
|
|
765
|
+
db.exec('BEGIN IMMEDIATE');
|
|
766
|
+
try {
|
|
767
|
+
insertContactMessage(db, {
|
|
768
|
+
messageID: failureID,
|
|
769
|
+
assistantID,
|
|
770
|
+
role: 'assistant',
|
|
771
|
+
turnID,
|
|
772
|
+
bubbleIndex: 0,
|
|
773
|
+
createdAt: now(),
|
|
774
|
+
ordinal: nextContactOrdinal(db, assistantID),
|
|
775
|
+
status: 'error',
|
|
776
|
+
parts: [{ type: 'text', text: detail }],
|
|
777
|
+
});
|
|
778
|
+
bump();
|
|
779
|
+
db.exec('COMMIT');
|
|
780
|
+
} catch (writeError) {
|
|
781
|
+
db.exec('ROLLBACK');
|
|
782
|
+
throw writeError;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
675
785
|
/** Persist assistant bubbles/cards for an already-admitted user turn (no user row). */
|
|
676
786
|
const persistContactAssistantReply = (assistantID, { userMessageID, bubbles, cards = [], turnID }) => {
|
|
677
787
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -778,17 +888,138 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
778
888
|
const resolveContactTurnSettlement = (messageID, result) => {
|
|
779
889
|
contactTurnSettlement(messageID).resolve(result);
|
|
780
890
|
};
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
891
|
+
const loadAssignCatalog = async (signal) => {
|
|
892
|
+
try {
|
|
893
|
+
// Catalog owns its 8s AbortSignal deadline; optional parent signal is forwarded.
|
|
894
|
+
return await loadConnectedCatalog(client(), {
|
|
895
|
+
timeoutMs: CONTACT_CATALOG_DEADLINE_MS,
|
|
896
|
+
...(signal ? { signal } : {}),
|
|
897
|
+
});
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const wrapped = new AssignError(
|
|
900
|
+
ASSIGN_CODES.UPSTREAM,
|
|
901
|
+
typeof error?.message === 'string' && error.message.trim()
|
|
902
|
+
? error.message.trim()
|
|
903
|
+
: 'Connected model catalog is unavailable.',
|
|
904
|
+
);
|
|
905
|
+
throw wrapped;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
const loadRegisteredProjects = async () => {
|
|
909
|
+
const listed = await awaitWithDeadline(listProjects(), CONTACT_CATALOG_DEADLINE_MS, 'upstream_error');
|
|
910
|
+
if (!Array.isArray(listed)) {
|
|
911
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Registered project catalog failed to load.');
|
|
912
|
+
}
|
|
913
|
+
return listed;
|
|
914
|
+
};
|
|
915
|
+
/**
|
|
916
|
+
* Confirm whether an assign prompt message was admitted on the worker session.
|
|
917
|
+
* Prefer SDK exact lookup (v2.session.message → session.message), then bounded messages list.
|
|
918
|
+
* Returns { found: true|false } for assignSession; throws/unavailable become null via assign.
|
|
919
|
+
*/
|
|
920
|
+
const lookupAssignMessage = async ({ sessionID, messageID, directory, signal } = {}) => {
|
|
921
|
+
if (typeof sessionID !== 'string' || !sessionID || typeof messageID !== 'string' || !messageID) {
|
|
922
|
+
return { found: false };
|
|
923
|
+
}
|
|
924
|
+
const api = client();
|
|
925
|
+
const options = signal ? { signal } : undefined;
|
|
926
|
+
const identity = (record) => (
|
|
927
|
+
record?.info?.id || record?.id || record?.messageID || record?.message?.id || null
|
|
928
|
+
);
|
|
929
|
+
const viaExact = async (fn, target) => {
|
|
930
|
+
if (typeof fn !== 'function') return null;
|
|
931
|
+
try {
|
|
932
|
+
const result = await fn.call(target, { sessionID, messageID, ...(directory ? { directory } : {}) }, options);
|
|
933
|
+
const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status ?? result?.response?.status;
|
|
934
|
+
if (result?.error) {
|
|
935
|
+
if (status === 404) return { found: false };
|
|
936
|
+
if (status === 405 || status === 501) return null;
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
const record = result?.data ?? result;
|
|
940
|
+
const id = identity(record);
|
|
941
|
+
return { found: id === messageID };
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
const v2 = await viaExact(api?.v2?.session?.message, api?.v2?.session);
|
|
947
|
+
if (v2) return v2;
|
|
948
|
+
const legacy = await viaExact(api?.session?.message, api?.session);
|
|
949
|
+
if (legacy) return legacy;
|
|
950
|
+
try {
|
|
951
|
+
const listed = await api.session.messages({
|
|
952
|
+
sessionID,
|
|
953
|
+
...(directory ? { directory } : {}),
|
|
954
|
+
limit: 100,
|
|
955
|
+
}, options);
|
|
956
|
+
if (listed?.error) return { found: false };
|
|
957
|
+
const rows = Array.isArray(listed?.data) ? listed.data : (Array.isArray(listed) ? listed : []);
|
|
958
|
+
const hit = rows.some((row) => identity(row) === messageID);
|
|
959
|
+
return { found: hit };
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
const assignWork = async (row, params = {}) => {
|
|
965
|
+
const fileParts = sanitizeAssignFileParts(params.fileParts || params.parts || []);
|
|
966
|
+
const signal = params.signal;
|
|
967
|
+
const sdkOptions = signal ? { signal } : undefined;
|
|
968
|
+
const explicitModel = Boolean(
|
|
969
|
+
(typeof params.providerID === 'string' && params.providerID.trim())
|
|
970
|
+
|| (typeof params.modelID === 'string' && params.modelID.trim())
|
|
971
|
+
|| (typeof params.model === 'string' && params.model.trim())
|
|
972
|
+
|| (params.model && typeof params.model === 'object'),
|
|
973
|
+
);
|
|
974
|
+
const needsCatalog = explicitModel || hasAssignImageParts(fileParts);
|
|
975
|
+
let catalog = null;
|
|
976
|
+
if (needsCatalog) {
|
|
977
|
+
catalog = await loadAssignCatalog(signal);
|
|
978
|
+
}
|
|
979
|
+
const workerModel = resolveAssignWorkerModel({
|
|
980
|
+
providerID: params.providerID,
|
|
981
|
+
modelID: params.modelID,
|
|
982
|
+
model: params.model,
|
|
983
|
+
fallback: { providerID: row.provider_id, modelID: row.model_id },
|
|
984
|
+
catalog,
|
|
985
|
+
});
|
|
986
|
+
let acceptsImages = workerModel.acceptsImages;
|
|
987
|
+
if (hasAssignImageParts(fileParts) && acceptsImages == null && catalog) {
|
|
988
|
+
const entry = catalog.models.find((item) => (
|
|
989
|
+
item?.providerID === workerModel.providerID && item?.modelID === workerModel.modelID
|
|
990
|
+
));
|
|
991
|
+
acceptsImages = entry?.acceptsImages === true;
|
|
992
|
+
}
|
|
993
|
+
// Worker model is prompt-only. Never mutate the contact assistant row.
|
|
994
|
+
// Forward tool variant + AbortSignal; lookupMessage uses real SDK exact/list paths.
|
|
995
|
+
return assignSession({
|
|
996
|
+
...params,
|
|
997
|
+
fileParts,
|
|
998
|
+
...(params.variant !== undefined ? { variant: params.variant } : {}),
|
|
999
|
+
...(signal ? { signal } : {}),
|
|
1000
|
+
model: {
|
|
1001
|
+
providerID: workerModel.providerID,
|
|
1002
|
+
modelID: workerModel.modelID,
|
|
1003
|
+
source: workerModel.source,
|
|
1004
|
+
acceptsImages,
|
|
1005
|
+
},
|
|
1006
|
+
acceptsImages,
|
|
1007
|
+
assistant: output(row),
|
|
1008
|
+
defaultProjectPath: row.workspace_path,
|
|
1009
|
+
allowedRoots: getAllowedRoots(),
|
|
1010
|
+
managedWorkspaceRoot: path.resolve(dataDir, 'assistant-workspaces'),
|
|
1011
|
+
listWorktrees,
|
|
1012
|
+
createSession: (created) => client().session.create(created, sdkOptions),
|
|
1013
|
+
promptExisting: (prompted) => client().session.promptAsync(prompted, sdkOptions),
|
|
1014
|
+
deleteSession: (deleted) => (
|
|
1015
|
+
typeof client().session.delete === 'function'
|
|
1016
|
+
? client().session.delete(deleted, sdkOptions)
|
|
1017
|
+
: undefined
|
|
1018
|
+
),
|
|
1019
|
+
lookupMessage: (lookup) => lookupAssignMessage({ ...lookup, signal }),
|
|
1020
|
+
messageID: params?.messageID || `msg_assign_${id()}`,
|
|
1021
|
+
});
|
|
1022
|
+
};
|
|
792
1023
|
const matchRegisteredProject = async (directory) => {
|
|
793
1024
|
const projects = await listProjects();
|
|
794
1025
|
const resolved = path.resolve(directory);
|
|
@@ -1034,11 +1265,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1034
1265
|
if (typeof createChatCompletion !== 'function' && runContactTurn === defaultRunContactTurn) fail('upstream_error');
|
|
1035
1266
|
let registeredProjects = [];
|
|
1036
1267
|
try {
|
|
1037
|
-
|
|
1038
|
-
if (!Array.isArray(listed)) {
|
|
1039
|
-
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Registered project catalog failed to load.');
|
|
1040
|
-
}
|
|
1041
|
-
registeredProjects = filterRegisteredProjects(listed);
|
|
1268
|
+
registeredProjects = filterRegisteredProjects(await loadRegisteredProjects());
|
|
1042
1269
|
} catch (error) {
|
|
1043
1270
|
if (error instanceof AssignError || error instanceof AssistantError) throw error;
|
|
1044
1271
|
// Still allow the turn — tools can surface a later catalog failure.
|
|
@@ -1046,12 +1273,35 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1046
1273
|
}
|
|
1047
1274
|
const turnID = messageID;
|
|
1048
1275
|
const assistantSnapshot = output(row);
|
|
1276
|
+
const existingUser = getContactMessage(db, messageID);
|
|
1277
|
+
if (existingUser) {
|
|
1278
|
+
if (existingUser.assistantID !== row.assistant_id || existingUser.role !== 'user') fail('validation_error');
|
|
1279
|
+
const existingFingerprint = contactPartsFingerprint(existingUser.parts);
|
|
1280
|
+
const nextFingerprint = contactPartsFingerprint(
|
|
1281
|
+
Array.isArray(userParts) && userParts.length > 0 ? userParts : [{ type: 'text', text: userText }],
|
|
1282
|
+
);
|
|
1283
|
+
if (existingFingerprint !== nextFingerprint) fail('idempotency_conflict');
|
|
1284
|
+
// Same messageID + payload: replay admission only — never start a second lane turn.
|
|
1285
|
+
return {
|
|
1286
|
+
binding: binding(row),
|
|
1287
|
+
messageID,
|
|
1288
|
+
admitted: true,
|
|
1289
|
+
revision: revision(),
|
|
1290
|
+
replayed: true,
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1049
1293
|
admitContactUser(row.assistant_id, {
|
|
1050
1294
|
userMessageID: messageID,
|
|
1051
1295
|
userText,
|
|
1052
1296
|
userParts,
|
|
1053
1297
|
turnID,
|
|
1054
1298
|
});
|
|
1299
|
+
rememberActiveContactTurn(row.assistant_id, {
|
|
1300
|
+
turnID,
|
|
1301
|
+
messageID,
|
|
1302
|
+
status: 'queued',
|
|
1303
|
+
admittedAt: now(),
|
|
1304
|
+
});
|
|
1055
1305
|
emitContactTurnEvent('openchamber:contact-turn-start', {
|
|
1056
1306
|
assistantID: row.assistant_id,
|
|
1057
1307
|
turnID,
|
|
@@ -1062,6 +1312,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1062
1312
|
const clearChatHistoryAuthorized = userTextAuthorizesClearChatHistory(userText);
|
|
1063
1313
|
const kickTurn = () => {
|
|
1064
1314
|
void inContactTurnLane(row.assistant_id, async () => {
|
|
1315
|
+
if (closed) {
|
|
1316
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1317
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: 'closed' });
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
markActiveContactTurnRunning(row.assistant_id, turnID);
|
|
1065
1321
|
// Load the LLM window when the lane runs — not at admit time — so a
|
|
1066
1322
|
// prior turn's clear-memory cannot leave queued work with stale history.
|
|
1067
1323
|
// Exclude the admitted user row; prompt still receives userText/parts.
|
|
@@ -1078,6 +1334,20 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1078
1334
|
const assignedCards = [];
|
|
1079
1335
|
let contactResetThisTurn = false;
|
|
1080
1336
|
let contactHistoryClearedThisTurn = false;
|
|
1337
|
+
// Current-turn attachments only — never unbounded history image forwarding.
|
|
1338
|
+
const turnFileParts = sanitizeAssignFileParts(
|
|
1339
|
+
(Array.isArray(userParts) ? userParts : []).filter((part) => part?.type === 'file'),
|
|
1340
|
+
);
|
|
1341
|
+
const turnAttachmentScope = attachmentScopeKey(turnFileParts);
|
|
1342
|
+
let connectedModels = [];
|
|
1343
|
+
try {
|
|
1344
|
+
const catalog = await loadAssignCatalog(undefined);
|
|
1345
|
+
connectedModels = normalizeConnectedModels(catalog?.models);
|
|
1346
|
+
} catch {
|
|
1347
|
+
// Catalog failure must not block default-model contact turns.
|
|
1348
|
+
// Explicit worker model / image capability checks still fail closed inside assignWork.
|
|
1349
|
+
connectedModels = [];
|
|
1350
|
+
}
|
|
1081
1351
|
const tools = createContactTools({
|
|
1082
1352
|
assignWork: (params) => assignWork(row, params),
|
|
1083
1353
|
createAssistant: (toolInput) => createAssistant(toolInput),
|
|
@@ -1106,16 +1376,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1106
1376
|
return result;
|
|
1107
1377
|
},
|
|
1108
1378
|
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
1109
|
-
listProjects: async () =>
|
|
1110
|
-
const listed = await listProjects();
|
|
1111
|
-
if (!Array.isArray(listed)) {
|
|
1112
|
-
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Registered project catalog failed to load.');
|
|
1113
|
-
}
|
|
1114
|
-
return listed;
|
|
1115
|
-
},
|
|
1379
|
+
listProjects: async () => loadRegisteredProjects(),
|
|
1116
1380
|
listSessions: (params) => listSessionsWork(params),
|
|
1117
1381
|
currentAssistant: assistantSnapshot,
|
|
1118
1382
|
onCard: (card) => assignedCards.push(card),
|
|
1383
|
+
turnFileParts,
|
|
1384
|
+
turnAttachmentScope,
|
|
1119
1385
|
});
|
|
1120
1386
|
try {
|
|
1121
1387
|
const generated = await runContactTurn({
|
|
@@ -1126,6 +1392,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1126
1392
|
createChatCompletion,
|
|
1127
1393
|
tools,
|
|
1128
1394
|
projects: registeredProjects,
|
|
1395
|
+
connectedModels,
|
|
1129
1396
|
globalEventHub,
|
|
1130
1397
|
onBubbleDelta: (bubbleIndex, delta, done) => {
|
|
1131
1398
|
emitContactTurnEvent('openchamber:contact-bubble-delta', {
|
|
@@ -1138,6 +1405,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1138
1405
|
},
|
|
1139
1406
|
});
|
|
1140
1407
|
if (closed) {
|
|
1408
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1141
1409
|
resolveContactTurnSettlement(messageID, { status: 'error', error: 'closed' });
|
|
1142
1410
|
return;
|
|
1143
1411
|
}
|
|
@@ -1159,6 +1427,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1159
1427
|
].filter((card, index, list) => list.findIndex((item) => contactCardIdentity(item) === contactCardIdentity(card)) === index);
|
|
1160
1428
|
if (bubbles.length === 0 && cards.length === 0) {
|
|
1161
1429
|
const detail = 'Assistant returned no text';
|
|
1430
|
+
try {
|
|
1431
|
+
persistContactTurnFailure(row.assistant_id, {
|
|
1432
|
+
userMessageID: messageID,
|
|
1433
|
+
turnID,
|
|
1434
|
+
error: detail,
|
|
1435
|
+
});
|
|
1436
|
+
settleActiveContactTurn(row.assistant_id, turnID);
|
|
1437
|
+
} catch {
|
|
1438
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1439
|
+
}
|
|
1162
1440
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1163
1441
|
assistantID: row.assistant_id,
|
|
1164
1442
|
turnID,
|
|
@@ -1195,6 +1473,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1195
1473
|
cards,
|
|
1196
1474
|
turnID,
|
|
1197
1475
|
});
|
|
1476
|
+
// persistContactAssistantReply already bumps; clear activity without a second tip.
|
|
1477
|
+
settleActiveContactTurn(row.assistant_id, turnID);
|
|
1198
1478
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1199
1479
|
assistantID: row.assistant_id,
|
|
1200
1480
|
turnID,
|
|
@@ -1214,6 +1494,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1214
1494
|
? error.message.trim()
|
|
1215
1495
|
: (error?.code || 'upstream_error');
|
|
1216
1496
|
const statusCode = error?.code === 'no_provider' ? 'no_provider' : (error?.code || 'upstream_error');
|
|
1497
|
+
try {
|
|
1498
|
+
persistContactTurnFailure(row.assistant_id, {
|
|
1499
|
+
userMessageID: messageID,
|
|
1500
|
+
turnID,
|
|
1501
|
+
error: detail,
|
|
1502
|
+
});
|
|
1503
|
+
settleActiveContactTurn(row.assistant_id, turnID);
|
|
1504
|
+
} catch {
|
|
1505
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1506
|
+
}
|
|
1217
1507
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1218
1508
|
assistantID: row.assistant_id,
|
|
1219
1509
|
turnID,
|
|
@@ -1234,7 +1524,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1234
1524
|
};
|
|
1235
1525
|
// Defer past send() resolution so callers observe admission before assistant rows.
|
|
1236
1526
|
setImmediateFn(kickTurn);
|
|
1237
|
-
return { binding: binding(row), messageID, admitted: true };
|
|
1527
|
+
return { binding: binding(row), messageID, admitted: true, revision: revision() };
|
|
1238
1528
|
};
|
|
1239
1529
|
const contactMessages = (assistantID, query = {}) => {
|
|
1240
1530
|
editable(assistantID);
|
|
@@ -1275,6 +1565,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1275
1565
|
db.exec('BEGIN IMMEDIATE');
|
|
1276
1566
|
try {
|
|
1277
1567
|
deleteContactMessages(db, row.assistant_id);
|
|
1568
|
+
// Active contact turns stay until settle — wipe must not drop the green
|
|
1569
|
+
// dot while the clearing turn is still running.
|
|
1278
1570
|
bump();
|
|
1279
1571
|
db.exec('COMMIT');
|
|
1280
1572
|
} catch (error) {
|
|
@@ -1430,6 +1722,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1430
1722
|
db.prepare('DELETE FROM assistant_message_backfill WHERE assistant_id=?').run(assistantID);
|
|
1431
1723
|
db.prepare('DELETE FROM assistant_scheduled_task WHERE assistant_id=?').run(assistantID);
|
|
1432
1724
|
deleteContactMessages(db, assistantID);
|
|
1725
|
+
clearActiveContactTurns(assistantID);
|
|
1433
1726
|
bump();
|
|
1434
1727
|
db.exec('COMMIT');
|
|
1435
1728
|
return { assistantID, tombstoneAt: now() };
|
|
@@ -1437,5 +1730,5 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1437
1730
|
db.exec('ROLLBACK');
|
|
1438
1731
|
throw error;
|
|
1439
1732
|
}
|
|
1440
|
-
}, ensure, createNew, compact, send, whenContactTurnSettled, abort, captureQueueDeliveryTarget, sendWithCapturedConfig, share, shareOperation, historicalMessages, contactMessages, clearContactMemory, resetContact, appendContactCard, deliverPeerMessage, listAssistantScheduledTasks, processEvent, reportAssignedSessionSettle: reportAssignedSession, reconcile, close: () => { if (!closed) { closed = true; unsubscribeEvents?.(); clearIntervalFn(timer); db.close(); } } };
|
|
1733
|
+
}, ensure, createNew, compact, send, whenContactTurnSettled, abort, captureQueueDeliveryTarget, sendWithCapturedConfig, share, shareOperation, historicalMessages, contactMessages, clearContactMemory, resetContact, appendContactCard, deliverPeerMessage, listAssistantScheduledTasks, processEvent, reportAssignedSessionSettle: reportAssignedSession, reconcile, close: () => { if (!closed) { closed = true; clearActiveContactTurns(); unsubscribeEvents?.(); clearIntervalFn(timer); db.close(); } } };
|
|
1441
1734
|
};
|