@openchambery/web 1.19.12 → 1.19.13-beta.2
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-mbvX7YlW.js} +1 -1
- package/dist/assets/AssistantView-D5c557Oe.js +2 -0
- package/dist/assets/DiagramView-CjNUrHsU.js +1 -0
- package/dist/assets/FadeInOnReveal-moBbL8Ll.js +1 -0
- package/dist/assets/{Icon-BYM02QyU.js → Icon-CkzzhaLr.js} +1 -1
- package/dist/assets/MarkdownRenderer-BHfbAwVO.js +2 -0
- package/dist/assets/MarkdownRendererImpl-CkBoUibc.js +7 -0
- package/dist/assets/{MarkstreamRendererImpl-BFdJzxia.js → MarkstreamRendererImpl-BrXUOI9g.js} +1 -1
- package/dist/assets/MobileDetailNavigation-aY9-HwBN.js +1 -0
- package/dist/assets/MobileShareBridge-CRt_8DmM.js +4 -0
- package/dist/assets/MobileSurface-CIhtF-CU.js +2 -0
- package/dist/assets/{MultiRunWindow-C3p3lj3U.js → MultiRunWindow-DDq6C8t7.js} +1 -1
- package/dist/assets/OnboardingScreen-j0iCyH7f.js +1 -0
- package/dist/assets/{OverlayScrollbar-KUpvYHdv.js → OverlayScrollbar-CDC5B3RH.js} +1 -1
- package/dist/assets/SettingsView-DFWWMvAj.js +25 -0
- package/dist/assets/{SettingsWindow-8IDuPZA5.js → SettingsWindow-CnYfBplB.js} +1 -1
- package/dist/assets/TerminalView-D5CQFVVm.js +8 -0
- package/dist/assets/ToolOutputDialog-VMI7TQFT.js +10 -0
- package/dist/assets/{appThemeRegistry-B87OLIgc.js → appThemeRegistry-UIrP0-5h.js} +1 -1
- package/dist/assets/assistant-attachment-cache-C0aRnFkj.js +1 -0
- package/dist/assets/{button-C0cth9tl.js → button-xx1UOBA5.js} +1 -1
- package/dist/assets/{clipboard-DJ4nQEuY.js → clipboard-CUasgPiF.js} +1 -1
- package/dist/assets/{collapsible-DFT9cE80.js → collapsible-CACHBRYZ.js} +1 -1
- package/dist/assets/{context-menu-CAp74NfV.js → context-menu-CYbm4wBZ.js} +1 -1
- package/dist/assets/desktopHosts-DILvrAok.js +1 -0
- package/dist/assets/desktopRelayRestore-t8asCr3m.js +1 -0
- package/dist/assets/{diagram-BryzMuta.js → diagram-u0BC9uuY.js} +1 -1
- package/dist/assets/{dialog-CgbV5ljg.js → dialog-BwQ3rY_x.js} +1 -1
- package/dist/assets/dualLimitLru-D8Sphgua.js +1 -0
- package/dist/assets/{es-BHqnSjoZ.js → es-CRjOwvYM.js} +1 -1
- package/dist/assets/fileQueries-Bgly-E8f.js +1 -0
- package/dist/assets/{fr-BfrlJY9i.js → fr-D9taEN9L.js} +1 -1
- package/dist/assets/{gitApi-tTtHQVA3.js → gitApi-Bu4W3bgD.js} +1 -1
- package/dist/assets/imageSource-BlAJJUEI.js +1 -0
- package/dist/assets/{input-DFLpqjGR.js → input-CI730njs.js} +1 -1
- package/dist/assets/insertionBoundaries-B1AqxsBD.js +4 -0
- package/dist/assets/{ja-DP-7qt-x.js → ja-CYQ5aoWp.js} +1 -1
- package/dist/assets/{ko-E2B5vGQP.js → ko-BwASV0Q0.js} +1 -1
- package/dist/assets/main-CqcB6tKc.js +169 -0
- package/dist/assets/main-DG-k0j8A.js +2 -0
- package/dist/assets/markdown-worker-CptoICmq.js +2 -0
- package/dist/assets/markdownHeightCache-DEjGKLv8.js +3 -0
- package/dist/assets/markdownTheme-DOfGg-A7.js +1 -0
- package/dist/assets/miniChat-BQtYQrsh.js +2 -0
- package/dist/assets/mobile-8iyqtXkr.js +2 -0
- package/dist/assets/{multirun-B0V3hdnD.js → multirun-CG_FCb_Z.js} +1 -1
- package/dist/assets/persistence-YbQKzHUQ.js +3 -0
- package/dist/assets/{pl-r8X5gRM5.js → pl-DUI1vPil.js} +1 -1
- package/dist/assets/platform-DzsGqlGW.js +2 -0
- package/dist/assets/projectMeta-DMpMDWE1.js +3 -0
- package/dist/assets/{pt-BR-DcpdXCf5.js → pt-BR-CZQFJllP.js} +1 -1
- package/dist/assets/relay-image-stream-DM7atqJs.js +2 -0
- package/dist/assets/renderElectronMiniChatApp-DBzRdJl8.js +2 -0
- package/dist/assets/renderMobileApp-CIseqCyn.js +28 -0
- package/dist/assets/runtime-auth-D66yq6ie.js +1 -0
- package/dist/assets/runtime-fetch-zIalUnbb.js +1 -0
- package/dist/assets/runtime-switch-LvWDU-SL.js +1 -0
- package/dist/assets/runtimeConfig-D8nJYB-e.js +2 -0
- package/dist/assets/runtimeEndpointReset-CSub_ecV.js +147 -0
- package/dist/assets/{sessionChangeSummary-hVby3sJk.js → sessionChangeSummary-DQDZ5HW-.js} +1 -1
- package/dist/assets/{sessionLookup-B6P_aCun.js → sessionLookup-BiBD-4wC.js} +1 -1
- package/dist/assets/sortable-tabs-strip-Dom6Sthu.js +1 -0
- package/dist/assets/src-AA-5nXsF.css +2 -0
- package/dist/assets/{store-BDFdnk6l.js → store-CSnH_QXM.js} +2 -2
- package/dist/assets/terminalApi-noa4Cdbh.js +1 -0
- package/dist/assets/{textarea-Cgo_nWUU.js → textarea-BfNtUdJE.js} +1 -1
- package/dist/assets/{tooltip-BifLJ8o0.js → tooltip-DPxumadF.js} +1 -1
- package/dist/assets/{uk-Di0vl9V-.js → uk-C1FYZlmA.js} +1 -1
- package/dist/assets/{url-Pk6tQ1EZ.js → url-CVhsI9kR.js} +1 -1
- package/dist/assets/{useAppFontEffects-Dmes1chr.js → useAppFontEffects-qHt5uaFN.js} +32 -32
- package/dist/assets/useAssistantUIStore-CLPCNoj_.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-BipyIXiK.js +1 -0
- package/dist/assets/{useEffectiveDirectory-CU2ChP0J.js → useEffectiveDirectory-DSNCCbCt.js} +1 -1
- package/dist/assets/useFontPreferences-CB-cGo3I.js +1 -0
- package/dist/assets/useMobileNavigationStore-C1NJGAik.js +413 -0
- package/dist/assets/useSessionAutoCleanup-Dcn6Unvd.js +2 -0
- package/dist/assets/useUIStore-CxOoCv7B.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-CzpT4M7V.js +7 -0
- package/dist/assets/{utils-BiK3tBGM.js → utils-BHTVlVov.js} +1 -1
- package/dist/assets/{zh-CN-coFCfeQE.js → zh-CN-C96qP1ip.js} +1 -1
- package/dist/assets/{zh-TW-oDRZ1-yb.js → zh-TW-BsRk1Rbb.js} +1 -1
- package/dist/index.html +12 -9
- package/dist/mini-chat.html +12 -9
- package/dist/mobile.html +12 -9
- package/package.json +1 -1
- package/server/index.js +19 -0
- package/server/lib/assistant-delivery-parts.js +19 -1
- package/server/lib/assistants/DOCUMENTATION.md +56 -11
- package/server/lib/assistants/assign.js +455 -30
- package/server/lib/assistants/assign.test.js +351 -0
- package/server/lib/assistants/cards.js +16 -2
- package/server/lib/assistants/cards.test.js +16 -1
- package/server/lib/assistants/contact-active-turn.http.test.js +134 -0
- package/server/lib/assistants/contact-attachments.http.test.js +436 -0
- package/server/lib/assistants/contact-attachments.js +814 -0
- package/server/lib/assistants/contact-attachments.p1.test.js +307 -0
- package/server/lib/assistants/contact-attachments.test.js +249 -0
- package/server/lib/assistants/contact-store.js +552 -19
- package/server/lib/assistants/contact-store.test.js +415 -1
- package/server/lib/assistants/contact-tools.js +114 -14
- package/server/lib/assistants/contact-tools.test.js +135 -6
- package/server/lib/assistants/contracts.js +42 -2
- package/server/lib/assistants/harness.js +4 -1
- package/server/lib/assistants/harness.test.js +124 -0
- package/server/lib/assistants/routes.js +46 -1
- package/server/lib/assistants/service.js +569 -84
- package/server/lib/assistants/service.test.js +936 -37
- package/server/lib/fs/prompt-attachment-store.js +663 -0
- package/server/lib/fs/prompt-attachment-store.test.js +263 -0
- package/server/lib/fs/routes.js +28 -72
- package/server/lib/fs/routes.test.js +15 -30
- package/server/lib/llm/catalog.js +91 -16
- package/server/lib/llm/catalog.test.js +152 -11
- 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/core-routes.js +2 -1
- 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-DxIghtun.js +0 -2
- package/dist/assets/DiagramView-BeUgp_Y0.js +0 -1
- package/dist/assets/FadeInOnReveal-CTlNE4Hg.js +0 -1
- package/dist/assets/MarkdownRenderer-C4MGa_kQ.js +0 -2
- package/dist/assets/MarkdownRendererImpl-DB4Q3kGa.js +0 -7
- package/dist/assets/MobileDetailNavigation-B_RwWOwv.js +0 -1
- package/dist/assets/MobileShareBridge-UiOLx0-2.js +0 -4
- package/dist/assets/MobileSurface-CNChj_dX.js +0 -2
- package/dist/assets/OnboardingScreen-DS2nugLX.js +0 -1
- package/dist/assets/SettingsView-BXfKsz0J.js +0 -25
- package/dist/assets/TerminalView-BiKCueth.js +0 -8
- package/dist/assets/ToolOutputDialog-BC2F_Fdl.js +0 -10
- package/dist/assets/desktopHosts-Dc2PS66M.js +0 -1
- package/dist/assets/desktopRelayRestore-T9y0orqa.js +0 -1
- package/dist/assets/fileQueries-BY-9OWzp.js +0 -1
- package/dist/assets/insertionBoundaries-CCQpZ5zn.js +0 -4
- package/dist/assets/main-C_PUoT3E.js +0 -169
- package/dist/assets/main-Dkg31fRY.js +0 -2
- package/dist/assets/markdown-worker-CTsrtpPh.js +0 -2
- package/dist/assets/markdownHeightCache-CYDlisxr.js +0 -3
- package/dist/assets/markdownTheme-DyJ4Gfcj.js +0 -2
- package/dist/assets/miniChat-C9QMj8T8.js +0 -2
- package/dist/assets/mobile-Z_X7CbzB.js +0 -2
- package/dist/assets/persistence-DdUac14I.js +0 -3
- package/dist/assets/platform-B4KKLnZ5.js +0 -2
- package/dist/assets/projectMeta-DRW9MQ4z.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-Dn1Su1dd.js +0 -2
- package/dist/assets/renderMobileApp-BG1JJt1Q.js +0 -28
- package/dist/assets/runtimeConfig-C6matIEB.js +0 -2
- package/dist/assets/runtimeEndpointReset-kgwmUJDb.js +0 -147
- package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +0 -1
- package/dist/assets/src-B-P5qIBc.css +0 -2
- package/dist/assets/terminalApi-947l7Ra3.js +0 -1
- package/dist/assets/useAssistantUIStore-me6s63b2.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-DuZ6URPR.js +0 -1
- package/dist/assets/useFontPreferences-BCOBlaAd.js +0 -1
- package/dist/assets/useMobileNavigationStore-C8_J-qTp.js +0 -413
- package/dist/assets/useSessionAutoCleanup-FOQeu1_T.js +0 -1
- package/dist/assets/useUIStore-CuaA8-Hp.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-GBqKnki1.js +0 -7
|
@@ -8,19 +8,43 @@ import { reduceBackfillState } from './history-state.js';
|
|
|
8
8
|
import { getWorktrees as defaultListWorktrees } from '../git/service.js';
|
|
9
9
|
import { contactCardIdentity, parseContactCard, parseContactPart } from './cards.js';
|
|
10
10
|
import {
|
|
11
|
+
CONTACT_PAGE_DEFAULT_LIMIT,
|
|
12
|
+
CONTACT_PAGE_MAX_LIMIT,
|
|
11
13
|
CONTACT_SETTLE_TEXT,
|
|
14
|
+
bumpContactGeneration,
|
|
12
15
|
clearContactMemory as clearContactMemoryStore,
|
|
13
16
|
contactHistoryForLlm,
|
|
17
|
+
createActiveContactTurn,
|
|
14
18
|
deleteContactMessages,
|
|
15
19
|
ensureContactSchema,
|
|
20
|
+
contactPartsFingerprint,
|
|
21
|
+
getContactMessage,
|
|
22
|
+
getLatestContactMessagePreview,
|
|
23
|
+
getLatestContactMessagePreviews,
|
|
16
24
|
insertContactMessage,
|
|
17
25
|
listContactMessages,
|
|
18
26
|
listInFlightWatches,
|
|
19
27
|
listWatchesBySession,
|
|
20
28
|
nextContactOrdinal,
|
|
29
|
+
projectActiveContactTurn,
|
|
21
30
|
updateSessionCardStatus,
|
|
22
31
|
upsertContactWatch,
|
|
23
32
|
} from './contact-store.js';
|
|
33
|
+
import {
|
|
34
|
+
CONTACT_ATTACHMENT_TURN_BUDGET_BYTES,
|
|
35
|
+
assertContactAttachmentOwned,
|
|
36
|
+
assertContactAttachmentReadable,
|
|
37
|
+
canonicalDescriptorFromRow,
|
|
38
|
+
ensureContactAttachmentSchema,
|
|
39
|
+
getContactAttachmentRow,
|
|
40
|
+
ingestDataUrlFilePart,
|
|
41
|
+
isSafeInlineImageMime,
|
|
42
|
+
materializeContactHistory,
|
|
43
|
+
materializeContactParts,
|
|
44
|
+
migrateContactDataUrlParts,
|
|
45
|
+
putContactAttachment,
|
|
46
|
+
readContactAttachmentBytes,
|
|
47
|
+
} from './contact-attachments.js';
|
|
24
48
|
import {
|
|
25
49
|
ASSIGNED_SESSION_FALLBACK_BUBBLE,
|
|
26
50
|
boundSessionListLimit,
|
|
@@ -31,15 +55,25 @@ import {
|
|
|
31
55
|
filterRegisteredProjects,
|
|
32
56
|
matchesProjectQuery,
|
|
33
57
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
58
|
+
normalizeConnectedModels,
|
|
34
59
|
normalizeRegisteredProjects,
|
|
35
60
|
sanitizeRegisteredProject,
|
|
36
|
-
userTextAuthorizesClearChatHistory,
|
|
37
61
|
} from './contact-tools.js';
|
|
38
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
AssignError,
|
|
64
|
+
ASSIGN_CODES,
|
|
65
|
+
assignSession,
|
|
66
|
+
attachmentScopeKey,
|
|
67
|
+
hasAssignImageParts,
|
|
68
|
+
resolveAssignDirectory,
|
|
69
|
+
resolveAssignWorkerModel,
|
|
70
|
+
sanitizeAssignFileParts,
|
|
71
|
+
} from './assign.js';
|
|
39
72
|
import { runContactTurn as defaultRunContactTurn } from './harness.js';
|
|
73
|
+
import { loadConnectedCatalog } from '../llm/catalog.js';
|
|
40
74
|
|
|
41
75
|
const require = createRequire(import.meta.url);
|
|
42
|
-
const SCHEMA_VERSION =
|
|
76
|
+
const SCHEMA_VERSION = 12;
|
|
43
77
|
const normalizeSessionDirectory = (value) => {
|
|
44
78
|
if (typeof value !== 'string') return null;
|
|
45
79
|
const trimmed = value.trim();
|
|
@@ -92,6 +126,25 @@ const isTransientMessagesFailure = (result, error) => {
|
|
|
92
126
|
};
|
|
93
127
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
94
128
|
const promptAdmitted = (result) => !result?.error && (result?.response?.status === 204 || result?.status === 204 || result?.data !== undefined || result?.response?.ok === true);
|
|
129
|
+
/** Bound catalog / project list waits so a contact lane cannot hang forever. */
|
|
130
|
+
export const CONTACT_CATALOG_DEADLINE_MS = 8_000;
|
|
131
|
+
const awaitWithDeadline = async (work, ms = CONTACT_CATALOG_DEADLINE_MS, code = 'upstream_error') => {
|
|
132
|
+
let timer = null;
|
|
133
|
+
try {
|
|
134
|
+
return await Promise.race([
|
|
135
|
+
Promise.resolve().then(() => work),
|
|
136
|
+
new Promise((_, reject) => {
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
const error = new Error(code);
|
|
139
|
+
error.code = code;
|
|
140
|
+
reject(error);
|
|
141
|
+
}, ms);
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
} finally {
|
|
145
|
+
if (timer) clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
95
148
|
|
|
96
149
|
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
150
|
if (!dbPath || !dataDir) return null;
|
|
@@ -100,6 +153,10 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
100
153
|
const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON');
|
|
101
154
|
let closed = false;
|
|
102
155
|
const shareReservations = new Map();
|
|
156
|
+
// Process-local contact-turn activity. Not durable across server restart —
|
|
157
|
+
// a new process starts empty so working cannot stick green forever. APP
|
|
158
|
+
// restart rehydrates from snapshot while this process still holds the turn.
|
|
159
|
+
const activeContactTurnsByAssistant = new Map();
|
|
103
160
|
db.exec(`CREATE TABLE IF NOT EXISTS assistant_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
104
161
|
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
162
|
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);
|
|
@@ -124,6 +181,21 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
124
181
|
CREATE INDEX IF NOT EXISTS assistant_scheduled_task_assistant
|
|
125
182
|
ON assistant_scheduled_task(assistant_id, created_at DESC);`);
|
|
126
183
|
ensureContactSchema(db);
|
|
184
|
+
ensureContactAttachmentSchema(db);
|
|
185
|
+
// Background legacy data-URL → descriptor migration (cancelable; never touches foreign DBs).
|
|
186
|
+
const contactAttachmentMigration = { controller: new AbortController(), promise: null };
|
|
187
|
+
contactAttachmentMigration.promise = Promise.resolve().then(() => migrateContactDataUrlParts({
|
|
188
|
+
db,
|
|
189
|
+
dataDir,
|
|
190
|
+
signal: contactAttachmentMigration.controller.signal,
|
|
191
|
+
yieldFn: () => new Promise((resolve) => setImmediateFn(resolve)),
|
|
192
|
+
})).catch((error) => {
|
|
193
|
+
if (error?.name !== 'AbortError' && !closed) {
|
|
194
|
+
// Diagnostics only — never throw into boot.
|
|
195
|
+
console.warn('[assistants] contact attachment migration:', error?.code || error?.message || error);
|
|
196
|
+
}
|
|
197
|
+
return { migrated: 0, failed: 0, scanned: 0, failures: [], error: error?.code || error?.message };
|
|
198
|
+
});
|
|
127
199
|
const historyColumns = new Set(db.prepare("SELECT name FROM pragma_table_info('assistant_session_history')").all().map((column) => column.name));
|
|
128
200
|
if (!historyColumns.has('directory')) db.exec('ALTER TABLE assistant_session_history ADD COLUMN directory TEXT');
|
|
129
201
|
const mirrorColumns = new Set(db.prepare("SELECT name FROM pragma_table_info('assistant_message_mirror')").all().map((column) => column.name));
|
|
@@ -168,8 +240,55 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
168
240
|
db.prepare('DELETE FROM assistant_message_backfill WHERE assistant_id=? AND session_id=?').run(assistantID, sessionID);
|
|
169
241
|
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
242
|
};
|
|
171
|
-
const
|
|
243
|
+
const activeContactTurnsFor = (assistantID) => activeContactTurnsByAssistant.get(assistantID) ?? new Map();
|
|
244
|
+
const rememberActiveContactTurn = (assistantID, input) => {
|
|
245
|
+
const next = createActiveContactTurn(input);
|
|
246
|
+
if (!next) return null;
|
|
247
|
+
const turns = activeContactTurnsFor(assistantID);
|
|
248
|
+
turns.set(next.turnID, next);
|
|
249
|
+
activeContactTurnsByAssistant.set(assistantID, turns);
|
|
250
|
+
return next;
|
|
251
|
+
};
|
|
252
|
+
const markActiveContactTurnRunning = (assistantID, turnID) => {
|
|
253
|
+
const turns = activeContactTurnsFor(assistantID);
|
|
254
|
+
const current = turns.get(turnID);
|
|
255
|
+
if (!current) return null;
|
|
256
|
+
if (current.status === 'running') return current;
|
|
257
|
+
const next = createActiveContactTurn({ ...current, status: 'running' });
|
|
258
|
+
if (!next) return null;
|
|
259
|
+
turns.set(turnID, next);
|
|
260
|
+
activeContactTurnsByAssistant.set(assistantID, turns);
|
|
261
|
+
// queued → running must tip snapshot so clients poll authoritative status.
|
|
262
|
+
bump();
|
|
263
|
+
return next;
|
|
264
|
+
};
|
|
265
|
+
const settleActiveContactTurn = (assistantID, turnID, { bumpRevision = false } = {}) => {
|
|
266
|
+
const turns = activeContactTurnsFor(assistantID);
|
|
267
|
+
if (!turns.has(turnID)) {
|
|
268
|
+
if (bumpRevision) bump();
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
turns.delete(turnID);
|
|
272
|
+
if (turns.size === 0) activeContactTurnsByAssistant.delete(assistantID);
|
|
273
|
+
else activeContactTurnsByAssistant.set(assistantID, turns);
|
|
274
|
+
// Error paths may not persist assistant rows; still tip the snapshot so
|
|
275
|
+
// list green dots clear without waiting for a later SQLite write.
|
|
276
|
+
if (bumpRevision) bump();
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
const clearActiveContactTurns = (assistantID) => {
|
|
280
|
+
if (assistantID == null) {
|
|
281
|
+
activeContactTurnsByAssistant.clear();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
activeContactTurnsByAssistant.delete(assistantID);
|
|
285
|
+
};
|
|
286
|
+
const output = (row, latestMessagePreview) => {
|
|
172
287
|
const watches = listInFlightWatches(db, row.assistant_id);
|
|
288
|
+
const activeContactTurn = projectActiveContactTurn(activeContactTurnsFor(row.assistant_id));
|
|
289
|
+
const preview = latestMessagePreview !== undefined
|
|
290
|
+
? latestMessagePreview
|
|
291
|
+
: getLatestContactMessagePreview(db, row.assistant_id);
|
|
173
292
|
return {
|
|
174
293
|
id: row.assistant_id,
|
|
175
294
|
revision: row.revision,
|
|
@@ -189,7 +308,13 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
189
308
|
historySessionIDs: historyIDs(row.assistant_id),
|
|
190
309
|
historySessionCount: historyCount(row.assistant_id),
|
|
191
310
|
assignedSessionIDs: watches.map((watch) => watch.sessionID),
|
|
192
|
-
|
|
311
|
+
// Grok-Bot list green dot: contact turn only. Assigned-session busy stays
|
|
312
|
+
// on the session card / assignedSessionIDs and must not keep the avatar lit.
|
|
313
|
+
working: activeContactTurn != null,
|
|
314
|
+
activeContactTurn,
|
|
315
|
+
// Contact-transcript list desc: authoritative last visible bubble (not
|
|
316
|
+
// defaultPrompt / updatedAt). null when the contact has no list-visible rows.
|
|
317
|
+
latestMessagePreview: preview ?? null,
|
|
193
318
|
createdAt: row.created_at,
|
|
194
319
|
updatedAt: row.updated_at,
|
|
195
320
|
tombstoneAt: row.tombstone_at,
|
|
@@ -242,7 +367,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
242
367
|
const restoreOnce = async (row, expectedSessionID, expectedGeneration) => { for (let attempt = 0; attempt < 3; attempt++) { const current = active(row.assistant_id); if (current.current_session_id !== expectedSessionID || current.session_generation !== expectedGeneration) return binding(current); const created = await createSession(current); const won = replaceBinding(current, created); if (won) return binding(won); } return binding(active(row.assistant_id)); };
|
|
243
368
|
const configuration = (row) => ({ model: { providerID: row.provider_id, modelID: row.model_id }, ...(row.agent ? { agent: row.agent } : {}), ...(row.variant ? { variant: row.variant } : {}), ...(row.default_prompt ? { system: row.default_prompt } : {}) });
|
|
244
369
|
const capturedConfiguration = (target) => ({ model: { providerID: target.providerID, modelID: target.modelID }, ...(target.agent ? { agent: target.agent } : {}), ...(target.variant ? { variant: target.variant } : {}), ...(target.system ? { system: target.system } : target.defaultPrompt ? { system: target.defaultPrompt } : {}) });
|
|
245
|
-
const validateParts = (parts) => {
|
|
370
|
+
const validateParts = (parts) => {
|
|
371
|
+
if (!validAssistantDeliveryParts(parts, { allowAttachmentRefs: true })) fail('validation_error');
|
|
372
|
+
};
|
|
246
373
|
const migrate = () => {
|
|
247
374
|
if (db.prepare("SELECT value FROM assistant_meta WHERE key='schema_version'").get()?.value === String(SCHEMA_VERSION)) return;
|
|
248
375
|
const v2Info = db.prepare("SELECT name,\"notnull\" AS required FROM pragma_table_info('assistant_v2')").all(); const v2Columns = new Set(v2Info.map((column) => column.name));
|
|
@@ -638,13 +765,60 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
638
765
|
if (text) return text;
|
|
639
766
|
return input.parts.some((part) => part?.type === 'file') ? '[attachment]' : '';
|
|
640
767
|
};
|
|
641
|
-
const userContactParts = (input, userText) => {
|
|
768
|
+
const userContactParts = async (assistantID, input, userText) => {
|
|
642
769
|
const parts = [];
|
|
770
|
+
let turnBytes = 0;
|
|
643
771
|
if (Array.isArray(input.parts)) {
|
|
644
772
|
for (const part of input.parts) {
|
|
645
773
|
const parsed = parseContactPart(part);
|
|
646
|
-
if (parsed?.type === 'text' && parsed.text.trim())
|
|
647
|
-
|
|
774
|
+
if (parsed?.type === 'text' && parsed.text.trim()) {
|
|
775
|
+
parts.push({ type: 'text', text: parsed.text });
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (parsed?.type !== 'file') continue;
|
|
779
|
+
if (parsed.attachmentID) {
|
|
780
|
+
let row;
|
|
781
|
+
try {
|
|
782
|
+
// Admission: current-generation ownership + canonical row rebuild (ignore client meta).
|
|
783
|
+
row = assertContactAttachmentReadable(db, { attachmentID: parsed.attachmentID, assistantID });
|
|
784
|
+
} catch (error) {
|
|
785
|
+
fail(error?.code === 'not_found' ? 'not_found' : 'validation_error', error?.message);
|
|
786
|
+
}
|
|
787
|
+
const descriptor = canonicalDescriptorFromRow(row);
|
|
788
|
+
turnBytes += descriptor.size || 0;
|
|
789
|
+
if (turnBytes > CONTACT_ATTACHMENT_TURN_BUDGET_BYTES) {
|
|
790
|
+
fail('validation_error', 'Contact attachment total exceeds 50MiB budget');
|
|
791
|
+
}
|
|
792
|
+
parts.push(descriptor);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (typeof parsed.url === 'string' && parsed.url.startsWith('data:')) {
|
|
796
|
+
try {
|
|
797
|
+
const descriptor = await ingestDataUrlFilePart({
|
|
798
|
+
db,
|
|
799
|
+
dataDir,
|
|
800
|
+
assistantID,
|
|
801
|
+
part: parsed,
|
|
802
|
+
clock: now,
|
|
803
|
+
mode: 'admission',
|
|
804
|
+
});
|
|
805
|
+
// Re-check generation after await.
|
|
806
|
+
const row = assertContactAttachmentReadable(db, {
|
|
807
|
+
attachmentID: descriptor.attachmentID,
|
|
808
|
+
assistantID,
|
|
809
|
+
});
|
|
810
|
+
const canonical = canonicalDescriptorFromRow(row);
|
|
811
|
+
turnBytes += canonical.size || 0;
|
|
812
|
+
if (turnBytes > CONTACT_ATTACHMENT_TURN_BUDGET_BYTES) {
|
|
813
|
+
fail('validation_error', 'Contact attachment total exceeds 50MiB budget');
|
|
814
|
+
}
|
|
815
|
+
parts.push(canonical);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
fail(error?.code || 'validation_error', error?.message);
|
|
818
|
+
}
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
fail('validation_error', 'File parts require attachmentID or data URL');
|
|
648
822
|
}
|
|
649
823
|
}
|
|
650
824
|
if (parts.length === 0 && userText) parts.push({ type: 'text', text: userText });
|
|
@@ -672,6 +846,31 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
672
846
|
throw error;
|
|
673
847
|
}
|
|
674
848
|
};
|
|
849
|
+
/** Durable failure bubble so APP restart can recover error without SSE. */
|
|
850
|
+
const persistContactTurnFailure = (assistantID, { userMessageID, turnID, error }) => {
|
|
851
|
+
const detail = typeof error === 'string' && error.trim() ? error.trim() : 'upstream_error';
|
|
852
|
+
const failureID = `${userMessageID}:error`;
|
|
853
|
+
if (getContactMessage(db, failureID)) return;
|
|
854
|
+
db.exec('BEGIN IMMEDIATE');
|
|
855
|
+
try {
|
|
856
|
+
insertContactMessage(db, {
|
|
857
|
+
messageID: failureID,
|
|
858
|
+
assistantID,
|
|
859
|
+
role: 'assistant',
|
|
860
|
+
turnID,
|
|
861
|
+
bubbleIndex: 0,
|
|
862
|
+
createdAt: now(),
|
|
863
|
+
ordinal: nextContactOrdinal(db, assistantID),
|
|
864
|
+
status: 'error',
|
|
865
|
+
parts: [{ type: 'text', text: detail }],
|
|
866
|
+
});
|
|
867
|
+
bump();
|
|
868
|
+
db.exec('COMMIT');
|
|
869
|
+
} catch (writeError) {
|
|
870
|
+
db.exec('ROLLBACK');
|
|
871
|
+
throw writeError;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
675
874
|
/** Persist assistant bubbles/cards for an already-admitted user turn (no user row). */
|
|
676
875
|
const persistContactAssistantReply = (assistantID, { userMessageID, bubbles, cards = [], turnID }) => {
|
|
677
876
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -778,17 +977,138 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
778
977
|
const resolveContactTurnSettlement = (messageID, result) => {
|
|
779
978
|
contactTurnSettlement(messageID).resolve(result);
|
|
780
979
|
};
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
980
|
+
const loadAssignCatalog = async (signal) => {
|
|
981
|
+
try {
|
|
982
|
+
// Catalog owns its 8s AbortSignal deadline; optional parent signal is forwarded.
|
|
983
|
+
return await loadConnectedCatalog(client(), {
|
|
984
|
+
timeoutMs: CONTACT_CATALOG_DEADLINE_MS,
|
|
985
|
+
...(signal ? { signal } : {}),
|
|
986
|
+
});
|
|
987
|
+
} catch (error) {
|
|
988
|
+
const wrapped = new AssignError(
|
|
989
|
+
ASSIGN_CODES.UPSTREAM,
|
|
990
|
+
typeof error?.message === 'string' && error.message.trim()
|
|
991
|
+
? error.message.trim()
|
|
992
|
+
: 'Connected model catalog is unavailable.',
|
|
993
|
+
);
|
|
994
|
+
throw wrapped;
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
const loadRegisteredProjects = async () => {
|
|
998
|
+
const listed = await awaitWithDeadline(listProjects(), CONTACT_CATALOG_DEADLINE_MS, 'upstream_error');
|
|
999
|
+
if (!Array.isArray(listed)) {
|
|
1000
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Registered project catalog failed to load.');
|
|
1001
|
+
}
|
|
1002
|
+
return listed;
|
|
1003
|
+
};
|
|
1004
|
+
/**
|
|
1005
|
+
* Confirm whether an assign prompt message was admitted on the worker session.
|
|
1006
|
+
* Prefer SDK exact lookup (v2.session.message → session.message), then bounded messages list.
|
|
1007
|
+
* Returns { found: true|false } for assignSession; throws/unavailable become null via assign.
|
|
1008
|
+
*/
|
|
1009
|
+
const lookupAssignMessage = async ({ sessionID, messageID, directory, signal } = {}) => {
|
|
1010
|
+
if (typeof sessionID !== 'string' || !sessionID || typeof messageID !== 'string' || !messageID) {
|
|
1011
|
+
return { found: false };
|
|
1012
|
+
}
|
|
1013
|
+
const api = client();
|
|
1014
|
+
const options = signal ? { signal } : undefined;
|
|
1015
|
+
const identity = (record) => (
|
|
1016
|
+
record?.info?.id || record?.id || record?.messageID || record?.message?.id || null
|
|
1017
|
+
);
|
|
1018
|
+
const viaExact = async (fn, target) => {
|
|
1019
|
+
if (typeof fn !== 'function') return null;
|
|
1020
|
+
try {
|
|
1021
|
+
const result = await fn.call(target, { sessionID, messageID, ...(directory ? { directory } : {}) }, options);
|
|
1022
|
+
const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status ?? result?.response?.status;
|
|
1023
|
+
if (result?.error) {
|
|
1024
|
+
if (status === 404) return { found: false };
|
|
1025
|
+
if (status === 405 || status === 501) return null;
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const record = result?.data ?? result;
|
|
1029
|
+
const id = identity(record);
|
|
1030
|
+
return { found: id === messageID };
|
|
1031
|
+
} catch {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const v2 = await viaExact(api?.v2?.session?.message, api?.v2?.session);
|
|
1036
|
+
if (v2) return v2;
|
|
1037
|
+
const legacy = await viaExact(api?.session?.message, api?.session);
|
|
1038
|
+
if (legacy) return legacy;
|
|
1039
|
+
try {
|
|
1040
|
+
const listed = await api.session.messages({
|
|
1041
|
+
sessionID,
|
|
1042
|
+
...(directory ? { directory } : {}),
|
|
1043
|
+
limit: 100,
|
|
1044
|
+
}, options);
|
|
1045
|
+
if (listed?.error) return { found: false };
|
|
1046
|
+
const rows = Array.isArray(listed?.data) ? listed.data : (Array.isArray(listed) ? listed : []);
|
|
1047
|
+
const hit = rows.some((row) => identity(row) === messageID);
|
|
1048
|
+
return { found: hit };
|
|
1049
|
+
} catch {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
const assignWork = async (row, params = {}) => {
|
|
1054
|
+
const fileParts = sanitizeAssignFileParts(params.fileParts || params.parts || []);
|
|
1055
|
+
const signal = params.signal;
|
|
1056
|
+
const sdkOptions = signal ? { signal } : undefined;
|
|
1057
|
+
const explicitModel = Boolean(
|
|
1058
|
+
(typeof params.providerID === 'string' && params.providerID.trim())
|
|
1059
|
+
|| (typeof params.modelID === 'string' && params.modelID.trim())
|
|
1060
|
+
|| (typeof params.model === 'string' && params.model.trim())
|
|
1061
|
+
|| (params.model && typeof params.model === 'object'),
|
|
1062
|
+
);
|
|
1063
|
+
const needsCatalog = explicitModel || hasAssignImageParts(fileParts);
|
|
1064
|
+
let catalog = null;
|
|
1065
|
+
if (needsCatalog) {
|
|
1066
|
+
catalog = await loadAssignCatalog(signal);
|
|
1067
|
+
}
|
|
1068
|
+
const workerModel = resolveAssignWorkerModel({
|
|
1069
|
+
providerID: params.providerID,
|
|
1070
|
+
modelID: params.modelID,
|
|
1071
|
+
model: params.model,
|
|
1072
|
+
fallback: { providerID: row.provider_id, modelID: row.model_id },
|
|
1073
|
+
catalog,
|
|
1074
|
+
});
|
|
1075
|
+
let acceptsImages = workerModel.acceptsImages;
|
|
1076
|
+
if (hasAssignImageParts(fileParts) && acceptsImages == null && catalog) {
|
|
1077
|
+
const entry = catalog.models.find((item) => (
|
|
1078
|
+
item?.providerID === workerModel.providerID && item?.modelID === workerModel.modelID
|
|
1079
|
+
));
|
|
1080
|
+
acceptsImages = entry?.acceptsImages === true;
|
|
1081
|
+
}
|
|
1082
|
+
// Worker model is prompt-only. Never mutate the contact assistant row.
|
|
1083
|
+
// Forward tool variant + AbortSignal; lookupMessage uses real SDK exact/list paths.
|
|
1084
|
+
return assignSession({
|
|
1085
|
+
...params,
|
|
1086
|
+
fileParts,
|
|
1087
|
+
...(params.variant !== undefined ? { variant: params.variant } : {}),
|
|
1088
|
+
...(signal ? { signal } : {}),
|
|
1089
|
+
model: {
|
|
1090
|
+
providerID: workerModel.providerID,
|
|
1091
|
+
modelID: workerModel.modelID,
|
|
1092
|
+
source: workerModel.source,
|
|
1093
|
+
acceptsImages,
|
|
1094
|
+
},
|
|
1095
|
+
acceptsImages,
|
|
1096
|
+
assistant: output(row),
|
|
1097
|
+
defaultProjectPath: row.workspace_path,
|
|
1098
|
+
allowedRoots: getAllowedRoots(),
|
|
1099
|
+
managedWorkspaceRoot: path.resolve(dataDir, 'assistant-workspaces'),
|
|
1100
|
+
listWorktrees,
|
|
1101
|
+
createSession: (created) => client().session.create(created, sdkOptions),
|
|
1102
|
+
promptExisting: (prompted) => client().session.promptAsync(prompted, sdkOptions),
|
|
1103
|
+
deleteSession: (deleted) => (
|
|
1104
|
+
typeof client().session.delete === 'function'
|
|
1105
|
+
? client().session.delete(deleted, sdkOptions)
|
|
1106
|
+
: undefined
|
|
1107
|
+
),
|
|
1108
|
+
lookupMessage: (lookup) => lookupAssignMessage({ ...lookup, signal }),
|
|
1109
|
+
messageID: params?.messageID || `msg_assign_${id()}`,
|
|
1110
|
+
});
|
|
1111
|
+
};
|
|
792
1112
|
const matchRegisteredProject = async (directory) => {
|
|
793
1113
|
const projects = await listProjects();
|
|
794
1114
|
const resolved = path.resolve(directory);
|
|
@@ -1029,16 +1349,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1029
1349
|
const messageID = string(input.messageID, 256, true);
|
|
1030
1350
|
const userText = extractUserText(input);
|
|
1031
1351
|
if (!userText) fail('validation_error');
|
|
1032
|
-
const userParts = userContactParts(input, userText);
|
|
1033
1352
|
const row = active(assistantID);
|
|
1353
|
+
const userParts = await userContactParts(row.assistant_id, input, userText);
|
|
1034
1354
|
if (typeof createChatCompletion !== 'function' && runContactTurn === defaultRunContactTurn) fail('upstream_error');
|
|
1035
1355
|
let registeredProjects = [];
|
|
1036
1356
|
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);
|
|
1357
|
+
registeredProjects = filterRegisteredProjects(await loadRegisteredProjects());
|
|
1042
1358
|
} catch (error) {
|
|
1043
1359
|
if (error instanceof AssignError || error instanceof AssistantError) throw error;
|
|
1044
1360
|
// Still allow the turn — tools can surface a later catalog failure.
|
|
@@ -1046,22 +1362,49 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1046
1362
|
}
|
|
1047
1363
|
const turnID = messageID;
|
|
1048
1364
|
const assistantSnapshot = output(row);
|
|
1365
|
+
const existingUser = getContactMessage(db, messageID);
|
|
1366
|
+
if (existingUser) {
|
|
1367
|
+
if (existingUser.assistantID !== row.assistant_id || existingUser.role !== 'user') fail('validation_error');
|
|
1368
|
+
const existingFingerprint = contactPartsFingerprint(existingUser.parts);
|
|
1369
|
+
const nextFingerprint = contactPartsFingerprint(
|
|
1370
|
+
Array.isArray(userParts) && userParts.length > 0 ? userParts : [{ type: 'text', text: userText }],
|
|
1371
|
+
);
|
|
1372
|
+
if (existingFingerprint !== nextFingerprint) fail('idempotency_conflict');
|
|
1373
|
+
// Same messageID + payload: replay admission only — never start a second lane turn.
|
|
1374
|
+
return {
|
|
1375
|
+
binding: binding(row),
|
|
1376
|
+
messageID,
|
|
1377
|
+
admitted: true,
|
|
1378
|
+
revision: revision(),
|
|
1379
|
+
replayed: true,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1049
1382
|
admitContactUser(row.assistant_id, {
|
|
1050
1383
|
userMessageID: messageID,
|
|
1051
1384
|
userText,
|
|
1052
1385
|
userParts,
|
|
1053
1386
|
turnID,
|
|
1054
1387
|
});
|
|
1388
|
+
rememberActiveContactTurn(row.assistant_id, {
|
|
1389
|
+
turnID,
|
|
1390
|
+
messageID,
|
|
1391
|
+
status: 'queued',
|
|
1392
|
+
admittedAt: now(),
|
|
1393
|
+
});
|
|
1055
1394
|
emitContactTurnEvent('openchamber:contact-turn-start', {
|
|
1056
1395
|
assistantID: row.assistant_id,
|
|
1057
1396
|
turnID,
|
|
1058
1397
|
messageID,
|
|
1059
1398
|
});
|
|
1060
1399
|
contactTurnSettlement(messageID);
|
|
1061
|
-
// Authorize wipe from this turn's real userText only — not tool args.
|
|
1062
|
-
const clearChatHistoryAuthorized = userTextAuthorizesClearChatHistory(userText);
|
|
1063
1400
|
const kickTurn = () => {
|
|
1064
1401
|
void inContactTurnLane(row.assistant_id, async () => {
|
|
1402
|
+
if (closed) {
|
|
1403
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1404
|
+
resolveContactTurnSettlement(messageID, { status: 'error', error: 'closed' });
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
markActiveContactTurnRunning(row.assistant_id, turnID);
|
|
1065
1408
|
// Load the LLM window when the lane runs — not at admit time — so a
|
|
1066
1409
|
// prior turn's clear-memory cannot leave queued work with stale history.
|
|
1067
1410
|
// Exclude the admitted user row; prompt still receives userText/parts.
|
|
@@ -1078,54 +1421,78 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1078
1421
|
const assignedCards = [];
|
|
1079
1422
|
let contactResetThisTurn = false;
|
|
1080
1423
|
let contactHistoryClearedThisTurn = false;
|
|
1081
|
-
|
|
1082
|
-
assignWork: (params) => assignWork(row, params),
|
|
1083
|
-
createAssistant: (toolInput) => createAssistant(toolInput),
|
|
1084
|
-
scheduleTask: (params) => scheduleWork(row, params),
|
|
1085
|
-
deliverPeerMessage: (toolInput) => deliverPeerMessage(row.assistant_id, toolInput),
|
|
1086
|
-
clearContactMemory: () => {
|
|
1087
|
-
// Cap the watermark at this turn's user ordinal — never global MAX,
|
|
1088
|
-
// or later-admitted queued users are permanently excluded from LLM history.
|
|
1089
|
-
const result = clearContactMemory(row.assistant_id, {
|
|
1090
|
-
upToOrdinal: beforeOrdinal,
|
|
1091
|
-
});
|
|
1092
|
-
contactResetThisTurn = true;
|
|
1093
|
-
return result;
|
|
1094
|
-
},
|
|
1095
|
-
resetContact: () => {
|
|
1096
|
-
// Server core gate: model mis-select cannot self-authorize delete.
|
|
1097
|
-
if (!clearChatHistoryAuthorized) {
|
|
1098
|
-
throw new AssignError(
|
|
1099
|
-
ASSIGN_CODES.VALIDATION,
|
|
1100
|
-
'clear_chat_history requires explicit user wipe intent in this message.',
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
|
-
const result = resetContact(row.assistant_id);
|
|
1104
|
-
contactResetThisTurn = true;
|
|
1105
|
-
contactHistoryClearedThisTurn = true;
|
|
1106
|
-
return result;
|
|
1107
|
-
},
|
|
1108
|
-
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
|
-
},
|
|
1116
|
-
listSessions: (params) => listSessionsWork(params),
|
|
1117
|
-
currentAssistant: assistantSnapshot,
|
|
1118
|
-
onCard: (card) => assignedCards.push(card),
|
|
1119
|
-
});
|
|
1424
|
+
let connectedModels = [];
|
|
1120
1425
|
try {
|
|
1426
|
+
const catalog = await loadAssignCatalog(undefined);
|
|
1427
|
+
connectedModels = normalizeConnectedModels(catalog?.models);
|
|
1428
|
+
} catch {
|
|
1429
|
+
// Catalog failure must not block default-model contact turns.
|
|
1430
|
+
// Explicit worker model / image capability checks still fail closed inside assignWork.
|
|
1431
|
+
connectedModels = [];
|
|
1432
|
+
}
|
|
1433
|
+
try {
|
|
1434
|
+
// Materialize inside turn try so failures durable-error + settle working.
|
|
1435
|
+
// DB/UI keep descriptors; harness + assign get execution-time data URLs under budget.
|
|
1436
|
+
const executionHistory = await materializeContactHistory(
|
|
1437
|
+
db,
|
|
1438
|
+
dataDir,
|
|
1439
|
+
row.assistant_id,
|
|
1440
|
+
history,
|
|
1441
|
+
{ budgetBytes: CONTACT_ATTACHMENT_TURN_BUDGET_BYTES },
|
|
1442
|
+
);
|
|
1443
|
+
const executionParts = await materializeContactParts(
|
|
1444
|
+
db,
|
|
1445
|
+
dataDir,
|
|
1446
|
+
row.assistant_id,
|
|
1447
|
+
userParts,
|
|
1448
|
+
{ budgetBytes: CONTACT_ATTACHMENT_TURN_BUDGET_BYTES },
|
|
1449
|
+
);
|
|
1450
|
+
// Current-turn attachments only — never unbounded history image forwarding.
|
|
1451
|
+
const turnFileParts = sanitizeAssignFileParts(
|
|
1452
|
+
(Array.isArray(executionParts) ? executionParts : []).filter((part) => part?.type === 'file'),
|
|
1453
|
+
);
|
|
1454
|
+
const turnAttachmentScope = attachmentScopeKey(turnFileParts);
|
|
1455
|
+
const tools = createContactTools({
|
|
1456
|
+
assignWork: (params) => assignWork(row, params),
|
|
1457
|
+
createAssistant: (toolInput) => createAssistant(toolInput),
|
|
1458
|
+
scheduleTask: (params) => scheduleWork(row, params),
|
|
1459
|
+
deliverPeerMessage: (toolInput) => deliverPeerMessage(row.assistant_id, toolInput),
|
|
1460
|
+
clearContactMemory: () => {
|
|
1461
|
+
// Cap the watermark at this turn's user ordinal — never global MAX,
|
|
1462
|
+
// or later-admitted queued users are permanently excluded from LLM history.
|
|
1463
|
+
const result = clearContactMemory(row.assistant_id, {
|
|
1464
|
+
upToOrdinal: beforeOrdinal,
|
|
1465
|
+
});
|
|
1466
|
+
contactResetThisTurn = true;
|
|
1467
|
+
return result;
|
|
1468
|
+
},
|
|
1469
|
+
resetContact: () => {
|
|
1470
|
+
// Tool wipe: keep wipe user + later-admitted users only; delete all
|
|
1471
|
+
// other roles at any ordinal (incl. late prior-lane assistant/cards).
|
|
1472
|
+
const result = resetContact(row.assistant_id, {
|
|
1473
|
+
upToOrdinal: beforeOrdinal,
|
|
1474
|
+
});
|
|
1475
|
+
contactResetThisTurn = true;
|
|
1476
|
+
contactHistoryClearedThisTurn = true;
|
|
1477
|
+
return result;
|
|
1478
|
+
},
|
|
1479
|
+
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
1480
|
+
listProjects: async () => loadRegisteredProjects(),
|
|
1481
|
+
listSessions: (params) => listSessionsWork(params),
|
|
1482
|
+
currentAssistant: assistantSnapshot,
|
|
1483
|
+
onCard: (card) => assignedCards.push(card),
|
|
1484
|
+
turnFileParts,
|
|
1485
|
+
turnAttachmentScope,
|
|
1486
|
+
});
|
|
1121
1487
|
const generated = await runContactTurn({
|
|
1122
1488
|
assistant: assistantSnapshot,
|
|
1123
|
-
history,
|
|
1489
|
+
history: executionHistory,
|
|
1124
1490
|
userText,
|
|
1125
|
-
userParts,
|
|
1491
|
+
userParts: executionParts,
|
|
1126
1492
|
createChatCompletion,
|
|
1127
1493
|
tools,
|
|
1128
1494
|
projects: registeredProjects,
|
|
1495
|
+
connectedModels,
|
|
1129
1496
|
globalEventHub,
|
|
1130
1497
|
onBubbleDelta: (bubbleIndex, delta, done) => {
|
|
1131
1498
|
emitContactTurnEvent('openchamber:contact-bubble-delta', {
|
|
@@ -1138,6 +1505,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1138
1505
|
},
|
|
1139
1506
|
});
|
|
1140
1507
|
if (closed) {
|
|
1508
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1141
1509
|
resolveContactTurnSettlement(messageID, { status: 'error', error: 'closed' });
|
|
1142
1510
|
return;
|
|
1143
1511
|
}
|
|
@@ -1159,6 +1527,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1159
1527
|
].filter((card, index, list) => list.findIndex((item) => contactCardIdentity(item) === contactCardIdentity(card)) === index);
|
|
1160
1528
|
if (bubbles.length === 0 && cards.length === 0) {
|
|
1161
1529
|
const detail = 'Assistant returned no text';
|
|
1530
|
+
try {
|
|
1531
|
+
persistContactTurnFailure(row.assistant_id, {
|
|
1532
|
+
userMessageID: messageID,
|
|
1533
|
+
turnID,
|
|
1534
|
+
error: detail,
|
|
1535
|
+
});
|
|
1536
|
+
settleActiveContactTurn(row.assistant_id, turnID);
|
|
1537
|
+
} catch {
|
|
1538
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1539
|
+
}
|
|
1162
1540
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1163
1541
|
assistantID: row.assistant_id,
|
|
1164
1542
|
turnID,
|
|
@@ -1175,9 +1553,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1175
1553
|
resolveContactTurnSettlement(messageID, { status: 'error', error: detail });
|
|
1176
1554
|
return;
|
|
1177
1555
|
}
|
|
1178
|
-
// clear_chat_history
|
|
1179
|
-
//
|
|
1180
|
-
// confirm bubble. new_conversation keeps rows
|
|
1556
|
+
// clear_chat_history keeps the wipe user row (bounded delete). Re-admit
|
|
1557
|
+
// only if the row is missing (unbounded API wipe / edge). Then persist
|
|
1558
|
+
// only the confirm bubble. new_conversation keeps all rows + watermark.
|
|
1181
1559
|
if (resetThisTurn) {
|
|
1182
1560
|
const existingUser = db.prepare('SELECT 1 AS ok FROM assistant_contact_message WHERE message_id=?').get(messageID);
|
|
1183
1561
|
if (!existingUser) {
|
|
@@ -1195,6 +1573,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1195
1573
|
cards,
|
|
1196
1574
|
turnID,
|
|
1197
1575
|
});
|
|
1576
|
+
// persistContactAssistantReply already bumps; clear activity without a second tip.
|
|
1577
|
+
settleActiveContactTurn(row.assistant_id, turnID);
|
|
1198
1578
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1199
1579
|
assistantID: row.assistant_id,
|
|
1200
1580
|
turnID,
|
|
@@ -1214,6 +1594,16 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1214
1594
|
? error.message.trim()
|
|
1215
1595
|
: (error?.code || 'upstream_error');
|
|
1216
1596
|
const statusCode = error?.code === 'no_provider' ? 'no_provider' : (error?.code || 'upstream_error');
|
|
1597
|
+
try {
|
|
1598
|
+
persistContactTurnFailure(row.assistant_id, {
|
|
1599
|
+
userMessageID: messageID,
|
|
1600
|
+
turnID,
|
|
1601
|
+
error: detail,
|
|
1602
|
+
});
|
|
1603
|
+
settleActiveContactTurn(row.assistant_id, turnID);
|
|
1604
|
+
} catch {
|
|
1605
|
+
settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
|
|
1606
|
+
}
|
|
1217
1607
|
emitContactTurnEvent('openchamber:contact-turn-end', {
|
|
1218
1608
|
assistantID: row.assistant_id,
|
|
1219
1609
|
turnID,
|
|
@@ -1234,13 +1624,35 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1234
1624
|
};
|
|
1235
1625
|
// Defer past send() resolution so callers observe admission before assistant rows.
|
|
1236
1626
|
setImmediateFn(kickTurn);
|
|
1237
|
-
return { binding: binding(row), messageID, admitted: true };
|
|
1627
|
+
return { binding: binding(row), messageID, admitted: true, revision: revision() };
|
|
1238
1628
|
};
|
|
1239
1629
|
const contactMessages = (assistantID, query = {}) => {
|
|
1240
|
-
editable(assistantID);
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1630
|
+
const row = editable(assistantID);
|
|
1631
|
+
const hasBefore = !(query.before == null || query.before === '');
|
|
1632
|
+
const hasExact = !(query.messageID == null || query.messageID === '');
|
|
1633
|
+
if (hasBefore && hasExact) fail('validation_error', 'before and messageID are mutually exclusive');
|
|
1634
|
+
let limit = query.limit == null || query.limit === '' ? CONTACT_PAGE_DEFAULT_LIMIT : Number(query.limit);
|
|
1635
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > CONTACT_PAGE_MAX_LIMIT) fail('validation_error');
|
|
1636
|
+
let page;
|
|
1637
|
+
try {
|
|
1638
|
+
page = listContactMessages(db, row.assistant_id, {
|
|
1639
|
+
before: hasBefore ? query.before : undefined,
|
|
1640
|
+
limit,
|
|
1641
|
+
messageID: hasExact ? query.messageID : undefined,
|
|
1642
|
+
});
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (error?.code === 'contact_generation_conflict' || error?.code === 'validation_error') {
|
|
1645
|
+
fail(error.code, error.message);
|
|
1646
|
+
}
|
|
1647
|
+
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
messages: page.messages,
|
|
1651
|
+
nextCursor: page.nextCursor,
|
|
1652
|
+
complete: page.complete,
|
|
1653
|
+
generation: page.generation,
|
|
1654
|
+
revision: revision(),
|
|
1655
|
+
};
|
|
1244
1656
|
};
|
|
1245
1657
|
/**
|
|
1246
1658
|
* Clear LLM memory only: advance the durable context boundary so later
|
|
@@ -1267,21 +1679,34 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1267
1679
|
};
|
|
1268
1680
|
/**
|
|
1269
1681
|
* Delete this assistant's OpenChamber contact transcript (messages, parts,
|
|
1270
|
-
* watches) and clear the context boundary.
|
|
1271
|
-
*
|
|
1682
|
+
* watches) and clear the context boundary. Also the POST /contact/reset
|
|
1683
|
+
* contract. Does not call OpenCode session/new.
|
|
1684
|
+
*
|
|
1685
|
+
* Tool path may pass `upToOrdinal` (wiping turn's user ordinal) so later-
|
|
1686
|
+
* admitted queued rows survive. Direct API without a ceiling deletes every
|
|
1687
|
+
* row (existing contract). Concurrent in-flight admissions already past 202
|
|
1688
|
+
* keep their message IDs only when bounded; unbounded API wipe drops them
|
|
1689
|
+
* and a later same-ID send replays as a fresh admission only if the row is gone.
|
|
1272
1690
|
*/
|
|
1273
|
-
const resetContact = (assistantID) => {
|
|
1691
|
+
const resetContact = (assistantID, options = {}) => {
|
|
1274
1692
|
const row = editable(assistantID);
|
|
1693
|
+
const upToOrdinal = options && typeof options === 'object' ? options.upToOrdinal : undefined;
|
|
1275
1694
|
db.exec('BEGIN IMMEDIATE');
|
|
1276
1695
|
try {
|
|
1277
|
-
|
|
1696
|
+
// Generation bumps only on transcript wipe — not on clear-memory.
|
|
1697
|
+
const generation = bumpContactGeneration(db, row.assistant_id);
|
|
1698
|
+
deleteContactMessages(db, row.assistant_id, {
|
|
1699
|
+
...(upToOrdinal !== undefined ? { upToOrdinal } : {}),
|
|
1700
|
+
});
|
|
1701
|
+
// Active contact turns stay until settle — wipe must not drop the green
|
|
1702
|
+
// dot while the clearing turn is still running.
|
|
1278
1703
|
bump();
|
|
1279
1704
|
db.exec('COMMIT');
|
|
1705
|
+
return { assistantID: row.assistant_id, reset: true, historyCleared: true, generation };
|
|
1280
1706
|
} catch (error) {
|
|
1281
1707
|
db.exec('ROLLBACK');
|
|
1282
1708
|
throw error;
|
|
1283
1709
|
}
|
|
1284
|
-
return { assistantID: row.assistant_id, reset: true, historyCleared: true };
|
|
1285
1710
|
};
|
|
1286
1711
|
const appendContactCard = (assistantID, input) => {
|
|
1287
1712
|
const row = active(assistantID);
|
|
@@ -1419,7 +1844,57 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1419
1844
|
operation = db.prepare('SELECT * FROM assistant_share_operation WHERE operation_id=?').get(operationID); if (operation?.phase !== 'reserving') { const claimed = claim(operationID, operation?.state === 'failed'); if (claimed) await submitClaim(claimed); } return shareOperation(operationID); };
|
|
1420
1845
|
const timer = setIntervalFn(() => { if (!closed) { db.prepare('DELETE FROM assistant_share_operation WHERE updated_at<?').run(now() - SHARE_RETENTION_MS); return reconcile(); } }, reconcileIntervalMs);
|
|
1421
1846
|
void reconcile();
|
|
1422
|
-
|
|
1847
|
+
const putAssistantContactAttachment = async (assistantID, uploadID, { stream, headers, signal, storeBytes } = {}) => {
|
|
1848
|
+
const row = editable(assistantID);
|
|
1849
|
+
try {
|
|
1850
|
+
return await putContactAttachment({
|
|
1851
|
+
db,
|
|
1852
|
+
dataDir,
|
|
1853
|
+
assistantID: row.assistant_id,
|
|
1854
|
+
uploadID,
|
|
1855
|
+
stream,
|
|
1856
|
+
headers,
|
|
1857
|
+
clock: now,
|
|
1858
|
+
signal,
|
|
1859
|
+
...(storeBytes ? { storeBytes } : {}),
|
|
1860
|
+
});
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
if (error?.code) fail(error.code, error.message);
|
|
1863
|
+
throw error;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
const getAssistantContactAttachment = async (assistantID, attachmentID) => {
|
|
1867
|
+
const row = editable(assistantID);
|
|
1868
|
+
try {
|
|
1869
|
+
return await readContactAttachmentBytes({
|
|
1870
|
+
db,
|
|
1871
|
+
dataDir,
|
|
1872
|
+
assistantID: row.assistant_id,
|
|
1873
|
+
attachmentID,
|
|
1874
|
+
external: true,
|
|
1875
|
+
});
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
if (error?.code) fail(error.code, error.message);
|
|
1878
|
+
throw error;
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
const migrateContactAttachments = async (assistantID = null) => migrateContactDataUrlParts({
|
|
1882
|
+
db,
|
|
1883
|
+
dataDir,
|
|
1884
|
+
assistantID,
|
|
1885
|
+
clock: now,
|
|
1886
|
+
});
|
|
1887
|
+
const snapshot = () => {
|
|
1888
|
+
const rows = db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all();
|
|
1889
|
+
// Per-assistant indexed LIMIT probes + one parts batch (catalog ≤100).
|
|
1890
|
+
const previews = getLatestContactMessagePreviews(db, rows.map((row) => row.assistant_id));
|
|
1891
|
+
return {
|
|
1892
|
+
revision: revision(),
|
|
1893
|
+
enabled: enabled(),
|
|
1894
|
+
assistants: rows.map((row) => output(row, previews.get(row.assistant_id) ?? null)),
|
|
1895
|
+
};
|
|
1896
|
+
};
|
|
1897
|
+
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) => {
|
|
1423
1898
|
db.exec('BEGIN IMMEDIATE');
|
|
1424
1899
|
try {
|
|
1425
1900
|
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);
|
|
@@ -1430,6 +1905,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1430
1905
|
db.prepare('DELETE FROM assistant_message_backfill WHERE assistant_id=?').run(assistantID);
|
|
1431
1906
|
db.prepare('DELETE FROM assistant_scheduled_task WHERE assistant_id=?').run(assistantID);
|
|
1432
1907
|
deleteContactMessages(db, assistantID);
|
|
1908
|
+
clearActiveContactTurns(assistantID);
|
|
1433
1909
|
bump();
|
|
1434
1910
|
db.exec('COMMIT');
|
|
1435
1911
|
return { assistantID, tombstoneAt: now() };
|
|
@@ -1437,5 +1913,14 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1437
1913
|
db.exec('ROLLBACK');
|
|
1438
1914
|
throw error;
|
|
1439
1915
|
}
|
|
1440
|
-
}, ensure, createNew, compact, send, whenContactTurnSettled, abort, captureQueueDeliveryTarget, sendWithCapturedConfig, share, shareOperation, historicalMessages, contactMessages, clearContactMemory, resetContact, appendContactCard, deliverPeerMessage, listAssistantScheduledTasks, processEvent, reportAssignedSessionSettle: reportAssignedSession, reconcile,
|
|
1916
|
+
}, ensure, createNew, compact, send, whenContactTurnSettled, abort, captureQueueDeliveryTarget, sendWithCapturedConfig, share, shareOperation, historicalMessages, contactMessages, clearContactMemory, resetContact, appendContactCard, deliverPeerMessage, listAssistantScheduledTasks, putAssistantContactAttachment, getAssistantContactAttachment, migrateContactAttachments, processEvent, reportAssignedSessionSettle: reportAssignedSession, reconcile, close: () => {
|
|
1917
|
+
if (!closed) {
|
|
1918
|
+
closed = true;
|
|
1919
|
+
try { contactAttachmentMigration.controller.abort(); } catch { /* ignore */ }
|
|
1920
|
+
clearActiveContactTurns();
|
|
1921
|
+
unsubscribeEvents?.();
|
|
1922
|
+
clearIntervalFn(timer);
|
|
1923
|
+
db.close();
|
|
1924
|
+
}
|
|
1925
|
+
} };
|
|
1441
1926
|
};
|