@openchambery/web 1.19.13-beta.1 → 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-CiQIIM0d.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-CeKHYyKZ.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-B-odd2Gz.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-BT5Dwgzu.js → MultiRunWindow-DDq6C8t7.js} +1 -1
- package/dist/assets/OnboardingScreen-j0iCyH7f.js +1 -0
- package/dist/assets/{OverlayScrollbar-DUghqFGg.js → OverlayScrollbar-CDC5B3RH.js} +1 -1
- package/dist/assets/SettingsView-DFWWMvAj.js +25 -0
- package/dist/assets/{SettingsWindow-DfEGfyr8.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-CIZ-shMD.js → appThemeRegistry-UIrP0-5h.js} +1 -1
- package/dist/assets/assistant-attachment-cache-C0aRnFkj.js +1 -0
- package/dist/assets/{button-DFPOdZ0g.js → button-xx1UOBA5.js} +1 -1
- package/dist/assets/{clipboard-C05anBwU.js → clipboard-CUasgPiF.js} +1 -1
- package/dist/assets/{collapsible-oGbEvbe1.js → collapsible-CACHBRYZ.js} +1 -1
- package/dist/assets/{context-menu-DGz4_2hs.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-Csk5GpDC.js → diagram-u0BC9uuY.js} +1 -1
- package/dist/assets/{dialog-rrqB-H6q.js → dialog-BwQ3rY_x.js} +1 -1
- package/dist/assets/dualLimitLru-D8Sphgua.js +1 -0
- package/dist/assets/{es-FE0sJ9EC.js → es-CRjOwvYM.js} +1 -1
- package/dist/assets/fileQueries-Bgly-E8f.js +1 -0
- package/dist/assets/{fr-DNDCSfGt.js → fr-D9taEN9L.js} +1 -1
- package/dist/assets/{gitApi-_P0FsoXe.js → gitApi-Bu4W3bgD.js} +1 -1
- package/dist/assets/imageSource-BlAJJUEI.js +1 -0
- package/dist/assets/{input-BO3r20op.js → input-CI730njs.js} +1 -1
- package/dist/assets/insertionBoundaries-B1AqxsBD.js +4 -0
- package/dist/assets/{ja-BDwrk0cM.js → ja-CYQ5aoWp.js} +1 -1
- package/dist/assets/{ko-BeeU2o4I.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-CxhTsmRq.js → multirun-CG_FCb_Z.js} +1 -1
- package/dist/assets/persistence-YbQKzHUQ.js +3 -0
- package/dist/assets/{pl-BYuzpcI5.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-Bn7cwEzv.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-BBLFgl85.js → runtimeEndpointReset-CSub_ecV.js} +6 -6
- package/dist/assets/{sessionChangeSummary-DDVT7O5J.js → sessionChangeSummary-DQDZ5HW-.js} +1 -1
- package/dist/assets/{sessionLookup-C8VZn0gJ.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-n94dSb3M.js → store-CSnH_QXM.js} +2 -2
- package/dist/assets/terminalApi-noa4Cdbh.js +1 -0
- package/dist/assets/{textarea-BjX-KhMN.js → textarea-BfNtUdJE.js} +1 -1
- package/dist/assets/{tooltip-CQGqGAG_.js → tooltip-DPxumadF.js} +1 -1
- package/dist/assets/{uk-CwpbHEa3.js → uk-C1FYZlmA.js} +1 -1
- package/dist/assets/{url-BMjqccCo.js → url-CVhsI9kR.js} +1 -1
- package/dist/assets/{useAppFontEffects-BIFw36SA.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-DJSYYkVp.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-81nUI_xp.js → utils-BHTVlVov.js} +1 -1
- package/dist/assets/{zh-CN-BpzFZ-LU.js → zh-CN-C96qP1ip.js} +1 -1
- package/dist/assets/{zh-TW-Bopgc5Eo.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/lib/assistant-delivery-parts.js +19 -1
- package/server/lib/assistants/DOCUMENTATION.md +39 -7
- package/server/lib/assistants/cards.js +16 -2
- package/server/lib/assistants/cards.test.js +16 -1
- 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 +494 -25
- package/server/lib/assistants/contact-store.test.js +403 -1
- package/server/lib/assistants/contact-tools.js +6 -5
- package/server/lib/assistants/contact-tools.test.js +24 -6
- package/server/lib/assistants/contracts.js +41 -1
- package/server/lib/assistants/harness.test.js +124 -0
- package/server/lib/assistants/routes.js +46 -1
- package/server/lib/assistants/service.js +258 -66
- package/server/lib/assistants/service.test.js +400 -30
- 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/opencode/core-routes.js +2 -1
- package/dist/assets/AssistantView-YmeyxehG.js +0 -2
- package/dist/assets/DiagramView-BpZb-G9P.js +0 -1
- package/dist/assets/FadeInOnReveal-nf6-ZCrs.js +0 -1
- package/dist/assets/MarkdownRenderer-wpeGdciL.js +0 -2
- package/dist/assets/MarkdownRendererImpl-Peu0GVSQ.js +0 -7
- package/dist/assets/MobileDetailNavigation-CtYke_1C.js +0 -1
- package/dist/assets/MobileShareBridge-DIfOl8EZ.js +0 -4
- package/dist/assets/MobileSurface-_KoX75Ff.js +0 -2
- package/dist/assets/OnboardingScreen-BPusUgFG.js +0 -1
- package/dist/assets/SettingsView-BwQpbIZl.js +0 -25
- package/dist/assets/TerminalView-8402vpcO.js +0 -8
- package/dist/assets/ToolOutputDialog-B5RdkhsG.js +0 -10
- package/dist/assets/desktopHosts-BVJXQq1S.js +0 -1
- package/dist/assets/desktopRelayRestore-RDfuj1M7.js +0 -1
- package/dist/assets/fileQueries-BcdWOPlD.js +0 -1
- package/dist/assets/insertionBoundaries-CWoioTdE.js +0 -4
- package/dist/assets/main-CLj_WS1o.js +0 -2
- package/dist/assets/main-CnSZ89pD.js +0 -169
- package/dist/assets/markdown-worker-CZWE1F1M.js +0 -2
- package/dist/assets/markdownHeightCache-Cdd3hkMl.js +0 -3
- package/dist/assets/markdownTheme-DIB8WVmz.js +0 -2
- package/dist/assets/miniChat-B9c3y2pD.js +0 -2
- package/dist/assets/mobile-DvbXUfNS.js +0 -2
- package/dist/assets/persistence-CVTWFaqe.js +0 -3
- package/dist/assets/platform-BY_Gtsqr.js +0 -2
- package/dist/assets/projectMeta-Dl5iPCB6.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-CkJ51qCh.js +0 -2
- package/dist/assets/renderMobileApp-DcjQPhWw.js +0 -28
- package/dist/assets/runtimeConfig-Bsk2kWsW.js +0 -2
- package/dist/assets/sortable-tabs-strip-s7UUxsa6.js +0 -1
- package/dist/assets/src-BpHDMgud.css +0 -2
- package/dist/assets/terminalApi-C54_rKEK.js +0 -1
- package/dist/assets/useAssistantUIStore-Cg9bsT4b.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-DoygEcN4.js +0 -1
- package/dist/assets/useFontPreferences-B5_apbBC.js +0 -1
- package/dist/assets/useMobileNavigationStore-DuTu0Mzh.js +0 -413
- package/dist/assets/useSessionAutoCleanup-C-L9teva.js +0 -1
- package/dist/assets/useUIStore-DwpQTjLO.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-DxTfgdEi.js +0 -7
|
@@ -8,7 +8,10 @@ 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,
|
|
14
17
|
createActiveContactTurn,
|
|
@@ -16,6 +19,8 @@ import {
|
|
|
16
19
|
ensureContactSchema,
|
|
17
20
|
contactPartsFingerprint,
|
|
18
21
|
getContactMessage,
|
|
22
|
+
getLatestContactMessagePreview,
|
|
23
|
+
getLatestContactMessagePreviews,
|
|
19
24
|
insertContactMessage,
|
|
20
25
|
listContactMessages,
|
|
21
26
|
listInFlightWatches,
|
|
@@ -25,6 +30,21 @@ import {
|
|
|
25
30
|
updateSessionCardStatus,
|
|
26
31
|
upsertContactWatch,
|
|
27
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';
|
|
28
48
|
import {
|
|
29
49
|
ASSIGNED_SESSION_FALLBACK_BUBBLE,
|
|
30
50
|
boundSessionListLimit,
|
|
@@ -38,7 +58,6 @@ import {
|
|
|
38
58
|
normalizeConnectedModels,
|
|
39
59
|
normalizeRegisteredProjects,
|
|
40
60
|
sanitizeRegisteredProject,
|
|
41
|
-
userTextAuthorizesClearChatHistory,
|
|
42
61
|
} from './contact-tools.js';
|
|
43
62
|
import {
|
|
44
63
|
AssignError,
|
|
@@ -54,7 +73,7 @@ import { runContactTurn as defaultRunContactTurn } from './harness.js';
|
|
|
54
73
|
import { loadConnectedCatalog } from '../llm/catalog.js';
|
|
55
74
|
|
|
56
75
|
const require = createRequire(import.meta.url);
|
|
57
|
-
const SCHEMA_VERSION =
|
|
76
|
+
const SCHEMA_VERSION = 12;
|
|
58
77
|
const normalizeSessionDirectory = (value) => {
|
|
59
78
|
if (typeof value !== 'string') return null;
|
|
60
79
|
const trimmed = value.trim();
|
|
@@ -162,6 +181,21 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
162
181
|
CREATE INDEX IF NOT EXISTS assistant_scheduled_task_assistant
|
|
163
182
|
ON assistant_scheduled_task(assistant_id, created_at DESC);`);
|
|
164
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
|
+
});
|
|
165
199
|
const historyColumns = new Set(db.prepare("SELECT name FROM pragma_table_info('assistant_session_history')").all().map((column) => column.name));
|
|
166
200
|
if (!historyColumns.has('directory')) db.exec('ALTER TABLE assistant_session_history ADD COLUMN directory TEXT');
|
|
167
201
|
const mirrorColumns = new Set(db.prepare("SELECT name FROM pragma_table_info('assistant_message_mirror')").all().map((column) => column.name));
|
|
@@ -249,9 +283,12 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
249
283
|
}
|
|
250
284
|
activeContactTurnsByAssistant.delete(assistantID);
|
|
251
285
|
};
|
|
252
|
-
const output = (row) => {
|
|
286
|
+
const output = (row, latestMessagePreview) => {
|
|
253
287
|
const watches = listInFlightWatches(db, row.assistant_id);
|
|
254
288
|
const activeContactTurn = projectActiveContactTurn(activeContactTurnsFor(row.assistant_id));
|
|
289
|
+
const preview = latestMessagePreview !== undefined
|
|
290
|
+
? latestMessagePreview
|
|
291
|
+
: getLatestContactMessagePreview(db, row.assistant_id);
|
|
255
292
|
return {
|
|
256
293
|
id: row.assistant_id,
|
|
257
294
|
revision: row.revision,
|
|
@@ -275,6 +312,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
275
312
|
// on the session card / assignedSessionIDs and must not keep the avatar lit.
|
|
276
313
|
working: activeContactTurn != null,
|
|
277
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,
|
|
278
318
|
createdAt: row.created_at,
|
|
279
319
|
updatedAt: row.updated_at,
|
|
280
320
|
tombstoneAt: row.tombstone_at,
|
|
@@ -327,7 +367,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
327
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)); };
|
|
328
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 } : {}) });
|
|
329
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 } : {}) });
|
|
330
|
-
const validateParts = (parts) => {
|
|
370
|
+
const validateParts = (parts) => {
|
|
371
|
+
if (!validAssistantDeliveryParts(parts, { allowAttachmentRefs: true })) fail('validation_error');
|
|
372
|
+
};
|
|
331
373
|
const migrate = () => {
|
|
332
374
|
if (db.prepare("SELECT value FROM assistant_meta WHERE key='schema_version'").get()?.value === String(SCHEMA_VERSION)) return;
|
|
333
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));
|
|
@@ -723,13 +765,60 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
723
765
|
if (text) return text;
|
|
724
766
|
return input.parts.some((part) => part?.type === 'file') ? '[attachment]' : '';
|
|
725
767
|
};
|
|
726
|
-
const userContactParts = (input, userText) => {
|
|
768
|
+
const userContactParts = async (assistantID, input, userText) => {
|
|
727
769
|
const parts = [];
|
|
770
|
+
let turnBytes = 0;
|
|
728
771
|
if (Array.isArray(input.parts)) {
|
|
729
772
|
for (const part of input.parts) {
|
|
730
773
|
const parsed = parseContactPart(part);
|
|
731
|
-
if (parsed?.type === 'text' && parsed.text.trim())
|
|
732
|
-
|
|
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');
|
|
733
822
|
}
|
|
734
823
|
}
|
|
735
824
|
if (parts.length === 0 && userText) parts.push({ type: 'text', text: userText });
|
|
@@ -1260,8 +1349,8 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1260
1349
|
const messageID = string(input.messageID, 256, true);
|
|
1261
1350
|
const userText = extractUserText(input);
|
|
1262
1351
|
if (!userText) fail('validation_error');
|
|
1263
|
-
const userParts = userContactParts(input, userText);
|
|
1264
1352
|
const row = active(assistantID);
|
|
1353
|
+
const userParts = await userContactParts(row.assistant_id, input, userText);
|
|
1265
1354
|
if (typeof createChatCompletion !== 'function' && runContactTurn === defaultRunContactTurn) fail('upstream_error');
|
|
1266
1355
|
let registeredProjects = [];
|
|
1267
1356
|
try {
|
|
@@ -1308,8 +1397,6 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1308
1397
|
messageID,
|
|
1309
1398
|
});
|
|
1310
1399
|
contactTurnSettlement(messageID);
|
|
1311
|
-
// Authorize wipe from this turn's real userText only — not tool args.
|
|
1312
|
-
const clearChatHistoryAuthorized = userTextAuthorizesClearChatHistory(userText);
|
|
1313
1400
|
const kickTurn = () => {
|
|
1314
1401
|
void inContactTurnLane(row.assistant_id, async () => {
|
|
1315
1402
|
if (closed) {
|
|
@@ -1334,11 +1421,6 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1334
1421
|
const assignedCards = [];
|
|
1335
1422
|
let contactResetThisTurn = false;
|
|
1336
1423
|
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
1424
|
let connectedModels = [];
|
|
1343
1425
|
try {
|
|
1344
1426
|
const catalog = await loadAssignCatalog(undefined);
|
|
@@ -1348,47 +1430,65 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1348
1430
|
// Explicit worker model / image capability checks still fail closed inside assignWork.
|
|
1349
1431
|
connectedModels = [];
|
|
1350
1432
|
}
|
|
1351
|
-
const tools = createContactTools({
|
|
1352
|
-
assignWork: (params) => assignWork(row, params),
|
|
1353
|
-
createAssistant: (toolInput) => createAssistant(toolInput),
|
|
1354
|
-
scheduleTask: (params) => scheduleWork(row, params),
|
|
1355
|
-
deliverPeerMessage: (toolInput) => deliverPeerMessage(row.assistant_id, toolInput),
|
|
1356
|
-
clearContactMemory: () => {
|
|
1357
|
-
// Cap the watermark at this turn's user ordinal — never global MAX,
|
|
1358
|
-
// or later-admitted queued users are permanently excluded from LLM history.
|
|
1359
|
-
const result = clearContactMemory(row.assistant_id, {
|
|
1360
|
-
upToOrdinal: beforeOrdinal,
|
|
1361
|
-
});
|
|
1362
|
-
contactResetThisTurn = true;
|
|
1363
|
-
return result;
|
|
1364
|
-
},
|
|
1365
|
-
resetContact: () => {
|
|
1366
|
-
// Server core gate: model mis-select cannot self-authorize delete.
|
|
1367
|
-
if (!clearChatHistoryAuthorized) {
|
|
1368
|
-
throw new AssignError(
|
|
1369
|
-
ASSIGN_CODES.VALIDATION,
|
|
1370
|
-
'clear_chat_history requires explicit user wipe intent in this message.',
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
const result = resetContact(row.assistant_id);
|
|
1374
|
-
contactResetThisTurn = true;
|
|
1375
|
-
contactHistoryClearedThisTurn = true;
|
|
1376
|
-
return result;
|
|
1377
|
-
},
|
|
1378
|
-
listAssistants: () => db.prepare('SELECT * FROM assistant_v2 WHERE tombstone_at IS NULL ORDER BY created_at').all().map(output),
|
|
1379
|
-
listProjects: async () => loadRegisteredProjects(),
|
|
1380
|
-
listSessions: (params) => listSessionsWork(params),
|
|
1381
|
-
currentAssistant: assistantSnapshot,
|
|
1382
|
-
onCard: (card) => assignedCards.push(card),
|
|
1383
|
-
turnFileParts,
|
|
1384
|
-
turnAttachmentScope,
|
|
1385
|
-
});
|
|
1386
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
|
+
});
|
|
1387
1487
|
const generated = await runContactTurn({
|
|
1388
1488
|
assistant: assistantSnapshot,
|
|
1389
|
-
history,
|
|
1489
|
+
history: executionHistory,
|
|
1390
1490
|
userText,
|
|
1391
|
-
userParts,
|
|
1491
|
+
userParts: executionParts,
|
|
1392
1492
|
createChatCompletion,
|
|
1393
1493
|
tools,
|
|
1394
1494
|
projects: registeredProjects,
|
|
@@ -1453,9 +1553,9 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1453
1553
|
resolveContactTurnSettlement(messageID, { status: 'error', error: detail });
|
|
1454
1554
|
return;
|
|
1455
1555
|
}
|
|
1456
|
-
// clear_chat_history
|
|
1457
|
-
//
|
|
1458
|
-
// 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.
|
|
1459
1559
|
if (resetThisTurn) {
|
|
1460
1560
|
const existingUser = db.prepare('SELECT 1 AS ok FROM assistant_contact_message WHERE message_id=?').get(messageID);
|
|
1461
1561
|
if (!existingUser) {
|
|
@@ -1527,10 +1627,32 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1527
1627
|
return { binding: binding(row), messageID, admitted: true, revision: revision() };
|
|
1528
1628
|
};
|
|
1529
1629
|
const contactMessages = (assistantID, query = {}) => {
|
|
1530
|
-
editable(assistantID);
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
+
};
|
|
1534
1656
|
};
|
|
1535
1657
|
/**
|
|
1536
1658
|
* Clear LLM memory only: advance the durable context boundary so later
|
|
@@ -1557,23 +1679,34 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1557
1679
|
};
|
|
1558
1680
|
/**
|
|
1559
1681
|
* Delete this assistant's OpenChamber contact transcript (messages, parts,
|
|
1560
|
-
* watches) and clear the context boundary.
|
|
1561
|
-
*
|
|
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.
|
|
1562
1690
|
*/
|
|
1563
|
-
const resetContact = (assistantID) => {
|
|
1691
|
+
const resetContact = (assistantID, options = {}) => {
|
|
1564
1692
|
const row = editable(assistantID);
|
|
1693
|
+
const upToOrdinal = options && typeof options === 'object' ? options.upToOrdinal : undefined;
|
|
1565
1694
|
db.exec('BEGIN IMMEDIATE');
|
|
1566
1695
|
try {
|
|
1567
|
-
|
|
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
|
+
});
|
|
1568
1701
|
// Active contact turns stay until settle — wipe must not drop the green
|
|
1569
1702
|
// dot while the clearing turn is still running.
|
|
1570
1703
|
bump();
|
|
1571
1704
|
db.exec('COMMIT');
|
|
1705
|
+
return { assistantID: row.assistant_id, reset: true, historyCleared: true, generation };
|
|
1572
1706
|
} catch (error) {
|
|
1573
1707
|
db.exec('ROLLBACK');
|
|
1574
1708
|
throw error;
|
|
1575
1709
|
}
|
|
1576
|
-
return { assistantID: row.assistant_id, reset: true, historyCleared: true };
|
|
1577
1710
|
};
|
|
1578
1711
|
const appendContactCard = (assistantID, input) => {
|
|
1579
1712
|
const row = active(assistantID);
|
|
@@ -1711,7 +1844,57 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1711
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); };
|
|
1712
1845
|
const timer = setIntervalFn(() => { if (!closed) { db.prepare('DELETE FROM assistant_share_operation WHERE updated_at<?').run(now() - SHARE_RETENTION_MS); return reconcile(); } }, reconcileIntervalMs);
|
|
1713
1846
|
void reconcile();
|
|
1714
|
-
|
|
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) => {
|
|
1715
1898
|
db.exec('BEGIN IMMEDIATE');
|
|
1716
1899
|
try {
|
|
1717
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);
|
|
@@ -1730,5 +1913,14 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
|
|
|
1730
1913
|
db.exec('ROLLBACK');
|
|
1731
1914
|
throw error;
|
|
1732
1915
|
}
|
|
1733
|
-
}, 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
|
+
} };
|
|
1734
1926
|
};
|