@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
|
@@ -3,20 +3,37 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createAssistantCardPart,
|
|
8
|
+
createScheduleCardPart,
|
|
9
|
+
createSessionCardPart,
|
|
10
|
+
} from './cards.js';
|
|
7
11
|
import {
|
|
8
12
|
CONTACT_LLM_FETCH_LIMIT,
|
|
9
13
|
CONTACT_LLM_FILE_CHAR_WEIGHT,
|
|
10
14
|
CONTACT_LLM_MAX_CHARS,
|
|
11
15
|
CONTACT_LLM_MAX_TURNS,
|
|
16
|
+
CONTACT_PAGE_DEFAULT_LIMIT,
|
|
17
|
+
CONTACT_PAGE_MAX_LIMIT,
|
|
18
|
+
CONTACT_PREVIEW_MAX_CHARS,
|
|
19
|
+
CONTACT_SETTLE_TEXT,
|
|
20
|
+
bumpContactGeneration,
|
|
12
21
|
clearContactMemory,
|
|
13
22
|
contactHistoryForLlm,
|
|
23
|
+
createActiveContactTurn,
|
|
24
|
+
decodeContactCursor,
|
|
14
25
|
deleteContactMessages,
|
|
26
|
+
encodeContactCursor,
|
|
15
27
|
ensureContactSchema,
|
|
16
28
|
getContactContextBoundary,
|
|
29
|
+
getContactGeneration,
|
|
30
|
+
getLatestContactMessagePreview,
|
|
31
|
+
getLatestContactMessagePreviews,
|
|
17
32
|
insertContactMessage,
|
|
18
33
|
listContactMessages,
|
|
19
34
|
nextContactOrdinal,
|
|
35
|
+
projectActiveContactTurn,
|
|
36
|
+
sanitizeContactPreviewText,
|
|
20
37
|
trimContactHistoryForLlm,
|
|
21
38
|
} from './contact-store.js';
|
|
22
39
|
|
|
@@ -152,6 +169,52 @@ describe('contact LLM history trim', () => {
|
|
|
152
169
|
db.close();
|
|
153
170
|
});
|
|
154
171
|
|
|
172
|
+
it('deleteContactMessages upToOrdinal keeps wipe user + later users; drops late assistant/cards', () => {
|
|
173
|
+
const db = openDb();
|
|
174
|
+
const assistantID = 'asst_wipe_bound';
|
|
175
|
+
insert(db, assistantID, { role: 'user', text: 'seed-old' });
|
|
176
|
+
insert(db, assistantID, { role: 'assistant', text: 'seed-reply' });
|
|
177
|
+
const wipeTurn = insert(db, assistantID, { role: 'user', text: '清空聊天记录' });
|
|
178
|
+
const queued = insert(db, assistantID, {
|
|
179
|
+
role: 'user',
|
|
180
|
+
text: 'queued-B',
|
|
181
|
+
parts: [{ type: 'text', text: 'queued-B' }, { type: 'file', mime: 'text/plain', url: 'data:text/plain;base64,eA==', filename: 'b.txt' }],
|
|
182
|
+
});
|
|
183
|
+
// Late write from a prior still-running lane — ordinal after wipe user.
|
|
184
|
+
insert(db, assistantID, { role: 'assistant', text: 'late-A-reply-should-die' });
|
|
185
|
+
insert(db, assistantID, {
|
|
186
|
+
role: 'assistant',
|
|
187
|
+
text: '',
|
|
188
|
+
parts: [createSessionCardPart({
|
|
189
|
+
sessionID: 'ses_late_a',
|
|
190
|
+
directory: '/repo',
|
|
191
|
+
title: 'late A card',
|
|
192
|
+
status: 'busy',
|
|
193
|
+
})],
|
|
194
|
+
});
|
|
195
|
+
insert(db, assistantID, {
|
|
196
|
+
role: 'peer',
|
|
197
|
+
text: 'peer leftover',
|
|
198
|
+
fromAssistantID: 'asst_other',
|
|
199
|
+
fromAssistantName: 'Other',
|
|
200
|
+
});
|
|
201
|
+
deleteContactMessages(db, assistantID, { upToOrdinal: wipeTurn.ordinal });
|
|
202
|
+
const page = listContactMessages(db, assistantID, { limit: 50 });
|
|
203
|
+
expect(page.messages.map((message) => ({ role: message.role, text: message.text }))).toEqual([
|
|
204
|
+
{ role: 'user', text: '清空聊天记录' },
|
|
205
|
+
{ role: 'user', text: 'queued-B' },
|
|
206
|
+
]);
|
|
207
|
+
expect(page.messages[0].messageID).toBe(wipeTurn.messageID);
|
|
208
|
+
expect(page.messages[1].messageID).toBe(queued.messageID);
|
|
209
|
+
expect(page.messages[1].parts.some((part) => part.type === 'file' && part.filename === 'b.txt')).toBe(true);
|
|
210
|
+
expect(page.messages.some((message) => message.cards?.length > 0)).toBe(false);
|
|
211
|
+
expect(page.messages.some((message) => (message.text || '').includes('late-A'))).toBe(false);
|
|
212
|
+
expect(page.messages.some((message) => message.role === 'peer')).toBe(false);
|
|
213
|
+
expect(getContactContextBoundary(db, assistantID)).toBe(0);
|
|
214
|
+
expect(contactHistoryForLlm(db, assistantID).some((item) => String(item.content).includes('late-A'))).toBe(false);
|
|
215
|
+
db.close();
|
|
216
|
+
});
|
|
217
|
+
|
|
155
218
|
it('clearContactMemory keeps transcript rows and only advances the LLM boundary', () => {
|
|
156
219
|
const db = openDb();
|
|
157
220
|
const assistantID = 'asst_memory';
|
|
@@ -231,6 +294,16 @@ describe('contact LLM history trim', () => {
|
|
|
231
294
|
reopened.close();
|
|
232
295
|
});
|
|
233
296
|
|
|
297
|
+
it('projects the earliest active contact turn for snapshot recovery', () => {
|
|
298
|
+
expect(createActiveContactTurn({ turnID: '', messageID: 'm', status: 'queued', admittedAt: 1 })).toBeNull();
|
|
299
|
+
expect(createActiveContactTurn({ turnID: 't', messageID: 'm', status: 'settled', admittedAt: 1 })).toBeNull();
|
|
300
|
+
const queued = createActiveContactTurn({ turnID: 't1', messageID: 'm1', status: 'queued', admittedAt: 20 });
|
|
301
|
+
const running = createActiveContactTurn({ turnID: 't0', messageID: 'm0', status: 'running', admittedAt: 10 });
|
|
302
|
+
expect(projectActiveContactTurn([queued, running])).toEqual(running);
|
|
303
|
+
expect(projectActiveContactTurn(new Map([['t1', queued]]))).toEqual(queued);
|
|
304
|
+
expect(projectActiveContactTurn([])).toBeNull();
|
|
305
|
+
});
|
|
306
|
+
|
|
234
307
|
it('keeps the previous boundary when clearContactMemory write fails inside a transaction', () => {
|
|
235
308
|
const db = openDb();
|
|
236
309
|
const assistantID = 'asst_fail';
|
|
@@ -254,3 +327,344 @@ describe('contact LLM history trim', () => {
|
|
|
254
327
|
db.close();
|
|
255
328
|
});
|
|
256
329
|
});
|
|
330
|
+
|
|
331
|
+
describe('contact messages page keyset', () => {
|
|
332
|
+
it('pages 45 rows as 20/20/5 with opaque cursors and ascending pages', () => {
|
|
333
|
+
const db = openDb();
|
|
334
|
+
const assistantID = 'asst_page';
|
|
335
|
+
const ids = [];
|
|
336
|
+
for (let i = 0; i < 45; i += 1) {
|
|
337
|
+
ids.push(insert(db, assistantID, { role: i % 2 === 0 ? 'user' : 'assistant', text: `m-${i}` }).messageID);
|
|
338
|
+
}
|
|
339
|
+
expect(CONTACT_PAGE_DEFAULT_LIMIT).toBe(20);
|
|
340
|
+
expect(CONTACT_PAGE_MAX_LIMIT).toBe(100);
|
|
341
|
+
|
|
342
|
+
// First page is the newest window (chat tail), ascending within the page.
|
|
343
|
+
const first = listContactMessages(db, assistantID, { limit: 20 });
|
|
344
|
+
expect(first.messages).toHaveLength(20);
|
|
345
|
+
expect(first.complete).toBe(false);
|
|
346
|
+
expect(first.generation).toBe(0);
|
|
347
|
+
expect(first.messages[0].text).toBe('m-25');
|
|
348
|
+
expect(first.messages.at(-1).text).toBe('m-44');
|
|
349
|
+
expect(first.nextCursor).toEqual(expect.any(String));
|
|
350
|
+
const cursor1 = decodeContactCursor(first.nextCursor);
|
|
351
|
+
expect(cursor1).toMatchObject({
|
|
352
|
+
v: 1,
|
|
353
|
+
assistantID,
|
|
354
|
+
generation: 0,
|
|
355
|
+
messageID: ids[25],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const second = listContactMessages(db, assistantID, { before: first.nextCursor, limit: 20 });
|
|
359
|
+
expect(second.messages).toHaveLength(20);
|
|
360
|
+
expect(second.complete).toBe(false);
|
|
361
|
+
expect(second.messages[0].text).toBe('m-5');
|
|
362
|
+
expect(second.messages.at(-1).text).toBe('m-24');
|
|
363
|
+
|
|
364
|
+
const third = listContactMessages(db, assistantID, { before: second.nextCursor, limit: 20 });
|
|
365
|
+
expect(third.messages).toHaveLength(5);
|
|
366
|
+
expect(third.complete).toBe(true);
|
|
367
|
+
expect(third.nextCursor).toBeNull();
|
|
368
|
+
expect(third.messages.map((m) => m.text)).toEqual(['m-0', 'm-1', 'm-2', 'm-3', 'm-4']);
|
|
369
|
+
db.close();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('rejects illegal cursors and cross-assistant cursor assistantID', () => {
|
|
373
|
+
const db = openDb();
|
|
374
|
+
insert(db, 'asst_a', { role: 'user', text: 'a' });
|
|
375
|
+
insert(db, 'asst_b', { role: 'user', text: 'b' });
|
|
376
|
+
expect(() => listContactMessages(db, 'asst_a', { before: 'not-base64!!!' })).toThrowError(
|
|
377
|
+
expect.objectContaining({ code: 'validation_error' }),
|
|
378
|
+
);
|
|
379
|
+
const foreign = encodeContactCursor({
|
|
380
|
+
assistantID: 'asst_b',
|
|
381
|
+
generation: 0,
|
|
382
|
+
ordinal: 1,
|
|
383
|
+
messageID: 'x',
|
|
384
|
+
});
|
|
385
|
+
expect(() => listContactMessages(db, 'asst_a', { before: foreign })).toThrowError(
|
|
386
|
+
expect.objectContaining({ code: 'validation_error' }),
|
|
387
|
+
);
|
|
388
|
+
expect(() => listContactMessages(db, 'asst_a', { before: 'x', messageID: 'y' })).toThrowError(
|
|
389
|
+
expect.objectContaining({ code: 'validation_error' }),
|
|
390
|
+
);
|
|
391
|
+
db.close();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('bumps generation on wipe path only; clear-memory keeps generation and cursors', () => {
|
|
395
|
+
const db = openDb();
|
|
396
|
+
const assistantID = 'asst_gen';
|
|
397
|
+
for (let i = 0; i < 5; i += 1) insert(db, assistantID, { role: 'user', text: `g-${i}` });
|
|
398
|
+
const page0 = listContactMessages(db, assistantID, { limit: 2 });
|
|
399
|
+
expect(page0.generation).toBe(0);
|
|
400
|
+
const cursor = page0.nextCursor;
|
|
401
|
+
|
|
402
|
+
clearContactMemory(db, assistantID, { updatedAt: 1 });
|
|
403
|
+
expect(getContactGeneration(db, assistantID)).toBe(0);
|
|
404
|
+
expect(listContactMessages(db, assistantID, { before: cursor, limit: 2 }).messages).toHaveLength(2);
|
|
405
|
+
|
|
406
|
+
expect(bumpContactGeneration(db, assistantID)).toBe(1);
|
|
407
|
+
deleteContactMessages(db, assistantID);
|
|
408
|
+
expect(getContactGeneration(db, assistantID)).toBe(1);
|
|
409
|
+
expect(() => listContactMessages(db, assistantID, { before: cursor, limit: 2 })).toThrowError(
|
|
410
|
+
expect.objectContaining({ code: 'contact_generation_conflict' }),
|
|
411
|
+
);
|
|
412
|
+
db.close();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('exact messageID is assistant-scoped, missing returns empty, works past 100 rows', () => {
|
|
416
|
+
const db = openDb();
|
|
417
|
+
const assistantID = 'asst_exact';
|
|
418
|
+
const other = 'asst_other';
|
|
419
|
+
const ids = [];
|
|
420
|
+
for (let i = 0; i < 120; i += 1) {
|
|
421
|
+
ids.push(insert(db, assistantID, { role: 'user', text: `e-${i}` }).messageID);
|
|
422
|
+
}
|
|
423
|
+
const foreign = insert(db, other, { role: 'user', text: 'foreign' });
|
|
424
|
+
|
|
425
|
+
const hit = listContactMessages(db, assistantID, { messageID: ids[110] });
|
|
426
|
+
expect(hit).toMatchObject({
|
|
427
|
+
nextCursor: null,
|
|
428
|
+
complete: true,
|
|
429
|
+
generation: 0,
|
|
430
|
+
});
|
|
431
|
+
expect(hit.messages).toHaveLength(1);
|
|
432
|
+
expect(hit.messages[0].text).toBe('e-110');
|
|
433
|
+
|
|
434
|
+
expect(listContactMessages(db, assistantID, { messageID: foreign.messageID }).messages).toEqual([]);
|
|
435
|
+
expect(listContactMessages(db, assistantID, { messageID: 'missing_id' }).messages).toEqual([]);
|
|
436
|
+
expect(listContactMessages(db, other, { messageID: foreign.messageID }).messages[0].text).toBe('foreign');
|
|
437
|
+
db.close();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('isolates pages across assistants under concurrent appends', () => {
|
|
441
|
+
const db = openDb();
|
|
442
|
+
for (let i = 0; i < 10; i += 1) {
|
|
443
|
+
insert(db, 'asst_left', { role: 'user', text: `L-${i}` });
|
|
444
|
+
insert(db, 'asst_right', { role: 'user', text: `R-${i}` });
|
|
445
|
+
}
|
|
446
|
+
const left = listContactMessages(db, 'asst_left', { limit: 5 });
|
|
447
|
+
const right = listContactMessages(db, 'asst_right', { limit: 5 });
|
|
448
|
+
expect(left.messages.every((m) => m.assistantID === 'asst_left')).toBe(true);
|
|
449
|
+
expect(right.messages.every((m) => m.assistantID === 'asst_right')).toBe(true);
|
|
450
|
+
// Newest window first.
|
|
451
|
+
expect(left.messages.map((m) => m.text)).toEqual(['L-5', 'L-6', 'L-7', 'L-8', 'L-9']);
|
|
452
|
+
insert(db, 'asst_left', { role: 'user', text: 'L-append' });
|
|
453
|
+
const left2 = listContactMessages(db, 'asst_left', { before: left.nextCursor, limit: 5 });
|
|
454
|
+
expect(left2.messages.map((m) => m.text)).toEqual(['L-0', 'L-1', 'L-2', 'L-3', 'L-4']);
|
|
455
|
+
expect(listContactMessages(db, 'asst_right', { limit: 100 }).messages).toHaveLength(10);
|
|
456
|
+
db.close();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe('contact latest message preview', () => {
|
|
461
|
+
it('returns null when the assistant has no contact rows', () => {
|
|
462
|
+
const db = openDb();
|
|
463
|
+
expect(getLatestContactMessagePreview(db, 'asst_empty')).toBeNull();
|
|
464
|
+
expect(getLatestContactMessagePreviews(db, ['asst_empty']).get('asst_empty')).toBeNull();
|
|
465
|
+
db.close();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('picks the newest user or assistant text by ordinal (not createdAt heuristics)', () => {
|
|
469
|
+
const db = openDb();
|
|
470
|
+
const assistantID = 'asst_preview_text';
|
|
471
|
+
insert(db, assistantID, { role: 'user', text: 'older user' });
|
|
472
|
+
insert(db, assistantID, { role: 'assistant', text: 'older assistant' });
|
|
473
|
+
const latest = insert(db, assistantID, { role: 'user', text: 'newest user line' });
|
|
474
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toEqual({
|
|
475
|
+
messageID: latest.messageID,
|
|
476
|
+
ordinal: latest.ordinal,
|
|
477
|
+
role: 'user',
|
|
478
|
+
text: 'newest user line',
|
|
479
|
+
fallbackKind: null,
|
|
480
|
+
});
|
|
481
|
+
db.close();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('skips settle-only markers and maps peer DM to user role', () => {
|
|
485
|
+
const db = openDb();
|
|
486
|
+
const assistantID = 'asst_preview_peer';
|
|
487
|
+
insert(db, assistantID, { role: 'user', text: 'seed' });
|
|
488
|
+
insert(db, assistantID, { role: 'assistant', text: CONTACT_SETTLE_TEXT.complete });
|
|
489
|
+
const peer = insert(db, assistantID, {
|
|
490
|
+
role: 'peer',
|
|
491
|
+
text: 'hello from peer',
|
|
492
|
+
fromAssistantID: 'asst_other',
|
|
493
|
+
fromAssistantName: 'Other',
|
|
494
|
+
});
|
|
495
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toEqual({
|
|
496
|
+
messageID: peer.messageID,
|
|
497
|
+
ordinal: peer.ordinal,
|
|
498
|
+
role: 'user',
|
|
499
|
+
text: 'hello from peer',
|
|
500
|
+
fallbackKind: null,
|
|
501
|
+
});
|
|
502
|
+
db.close();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('keeps durable error body text as the preview', () => {
|
|
506
|
+
const db = openDb();
|
|
507
|
+
const assistantID = 'asst_preview_err';
|
|
508
|
+
insert(db, assistantID, { role: 'user', text: 'hi' });
|
|
509
|
+
const err = insert(db, assistantID, {
|
|
510
|
+
role: 'assistant',
|
|
511
|
+
text: 'No connected model',
|
|
512
|
+
status: 'error',
|
|
513
|
+
});
|
|
514
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
|
|
515
|
+
messageID: err.messageID,
|
|
516
|
+
role: 'assistant',
|
|
517
|
+
text: 'No connected model',
|
|
518
|
+
fallbackKind: null,
|
|
519
|
+
});
|
|
520
|
+
db.close();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('uses attachment and card fallbackKind when there is no spoken text', () => {
|
|
524
|
+
const db = openDb();
|
|
525
|
+
const assistantID = 'asst_preview_fb';
|
|
526
|
+
insert(db, assistantID, {
|
|
527
|
+
role: 'user',
|
|
528
|
+
text: '',
|
|
529
|
+
parts: [{ type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' }],
|
|
530
|
+
});
|
|
531
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
|
|
532
|
+
role: 'user',
|
|
533
|
+
text: 'shot.png',
|
|
534
|
+
fallbackKind: 'image',
|
|
535
|
+
});
|
|
536
|
+
insert(db, assistantID, {
|
|
537
|
+
role: 'user',
|
|
538
|
+
text: '',
|
|
539
|
+
parts: [{ type: 'file', mime: 'application/pdf', url: 'data:application/pdf;base64,aa', filename: 'doc.pdf' }],
|
|
540
|
+
});
|
|
541
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
|
|
542
|
+
fallbackKind: 'file',
|
|
543
|
+
text: 'doc.pdf',
|
|
544
|
+
});
|
|
545
|
+
insert(db, assistantID, {
|
|
546
|
+
role: 'assistant',
|
|
547
|
+
text: '',
|
|
548
|
+
parts: [createSessionCardPart({
|
|
549
|
+
sessionID: 'ses_1',
|
|
550
|
+
directory: '/repo',
|
|
551
|
+
title: 'Fix login',
|
|
552
|
+
status: 'busy',
|
|
553
|
+
})],
|
|
554
|
+
});
|
|
555
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
|
|
556
|
+
role: 'assistant',
|
|
557
|
+
fallbackKind: 'session',
|
|
558
|
+
text: 'Fix login',
|
|
559
|
+
});
|
|
560
|
+
insert(db, assistantID, {
|
|
561
|
+
role: 'assistant',
|
|
562
|
+
text: '',
|
|
563
|
+
parts: [createAssistantCardPart({
|
|
564
|
+
assistantID: 'asst_x',
|
|
565
|
+
name: 'Flow',
|
|
566
|
+
providerID: 'p',
|
|
567
|
+
modelID: 'm',
|
|
568
|
+
})],
|
|
569
|
+
});
|
|
570
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
|
|
571
|
+
fallbackKind: 'assistant',
|
|
572
|
+
text: 'Flow',
|
|
573
|
+
});
|
|
574
|
+
insert(db, assistantID, {
|
|
575
|
+
role: 'assistant',
|
|
576
|
+
text: '',
|
|
577
|
+
parts: [createScheduleCardPart({
|
|
578
|
+
taskID: 'task_1',
|
|
579
|
+
projectID: 'proj_1',
|
|
580
|
+
name: 'Daily ping',
|
|
581
|
+
})],
|
|
582
|
+
});
|
|
583
|
+
expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
|
|
584
|
+
fallbackKind: 'schedule',
|
|
585
|
+
text: 'Daily ping',
|
|
586
|
+
});
|
|
587
|
+
db.close();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('bounds and sanitizes long / base64 text', () => {
|
|
591
|
+
const db = openDb();
|
|
592
|
+
const assistantID = 'asst_preview_bound';
|
|
593
|
+
const long = `hello ${'字'.repeat(CONTACT_PREVIEW_MAX_CHARS + 80)}`;
|
|
594
|
+
const dirty = `see data:image/png;base64,${'A'.repeat(80)} trailing`;
|
|
595
|
+
expect(sanitizeContactPreviewText(long).length).toBeLessThanOrEqual(CONTACT_PREVIEW_MAX_CHARS);
|
|
596
|
+
expect(sanitizeContactPreviewText(dirty)).not.toContain('base64');
|
|
597
|
+
insert(db, assistantID, { role: 'assistant', text: long });
|
|
598
|
+
const preview = getLatestContactMessagePreview(db, assistantID);
|
|
599
|
+
expect(preview.text.length).toBeLessThanOrEqual(CONTACT_PREVIEW_MAX_CHARS);
|
|
600
|
+
expect(preview.text.startsWith('hello')).toBe(true);
|
|
601
|
+
db.close();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('batches previews across assistants without mixing identities', () => {
|
|
605
|
+
const db = openDb();
|
|
606
|
+
insert(db, 'asst_a', { role: 'user', text: 'from-a' });
|
|
607
|
+
insert(db, 'asst_b', { role: 'assistant', text: 'from-b' });
|
|
608
|
+
insert(db, 'asst_c', { role: 'user', text: 'unused' });
|
|
609
|
+
deleteContactMessages(db, 'asst_c');
|
|
610
|
+
const map = getLatestContactMessagePreviews(db, ['asst_a', 'asst_b', 'asst_c', 'asst_missing']);
|
|
611
|
+
expect(map.get('asst_a').text).toBe('from-a');
|
|
612
|
+
expect(map.get('asst_b').text).toBe('from-b');
|
|
613
|
+
expect(map.get('asst_c')).toBeNull();
|
|
614
|
+
expect(map.get('asst_missing')).toBeNull();
|
|
615
|
+
db.close();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('finds latest visible text after a long settle tail and large history without full-table rank', () => {
|
|
619
|
+
const db = openDb();
|
|
620
|
+
const assistantID = 'asst_preview_deep';
|
|
621
|
+
for (let i = 0; i < 120; i += 1) {
|
|
622
|
+
insert(db, assistantID, { role: 'user', text: `hist-${i}` });
|
|
623
|
+
insert(db, assistantID, { role: 'assistant', text: `reply-${i}` });
|
|
624
|
+
}
|
|
625
|
+
const latest = insert(db, assistantID, { role: 'user', text: 'visible-after-settles' });
|
|
626
|
+
// More settle markers than CONTACT_PREVIEW_CANDIDATE_LIMIT after the real message.
|
|
627
|
+
for (let i = 0; i < 40; i += 1) {
|
|
628
|
+
insert(db, assistantID, {
|
|
629
|
+
role: 'assistant',
|
|
630
|
+
text: CONTACT_SETTLE_TEXT.complete,
|
|
631
|
+
});
|
|
632
|
+
insert(db, assistantID, {
|
|
633
|
+
role: 'assistant',
|
|
634
|
+
text: CONTACT_SETTLE_TEXT.error,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const preview = getLatestContactMessagePreview(db, assistantID);
|
|
638
|
+
expect(preview).toMatchObject({
|
|
639
|
+
messageID: latest.messageID,
|
|
640
|
+
ordinal: latest.ordinal,
|
|
641
|
+
role: 'user',
|
|
642
|
+
text: 'visible-after-settles',
|
|
643
|
+
fallbackKind: null,
|
|
644
|
+
});
|
|
645
|
+
const batch = getLatestContactMessagePreviews(db, [assistantID, 'asst_other_empty']);
|
|
646
|
+
expect(batch.get(assistantID)?.text).toBe('visible-after-settles');
|
|
647
|
+
expect(batch.get('asst_other_empty')).toBeNull();
|
|
648
|
+
|
|
649
|
+
// Probe must be an assistant_id range scan on the page index, not a full sort.
|
|
650
|
+
const plan = db.prepare(
|
|
651
|
+
`EXPLAIN QUERY PLAN
|
|
652
|
+
SELECT message_id, assistant_id, role, ordinal, status
|
|
653
|
+
FROM assistant_contact_message m
|
|
654
|
+
WHERE m.assistant_id = ?
|
|
655
|
+
AND m.role IN ('user', 'assistant', 'peer')
|
|
656
|
+
ORDER BY m.ordinal DESC, m.message_id DESC
|
|
657
|
+
LIMIT ?`,
|
|
658
|
+
).all(assistantID, 24);
|
|
659
|
+
const planText = plan.map((row) => `${row.detail || ''}`).join('\n').toLowerCase();
|
|
660
|
+
expect(planText).toMatch(/assistant_contact_message/);
|
|
661
|
+
// Must not be a whole-table SCAN without using the assistant_id path.
|
|
662
|
+
expect(
|
|
663
|
+
planText.includes('assistant_id')
|
|
664
|
+
|| planText.includes('assistant_contact_message_page')
|
|
665
|
+
|| planText.includes('using index')
|
|
666
|
+
|| planText.includes('search'),
|
|
667
|
+
).toBe(true);
|
|
668
|
+
db.close();
|
|
669
|
+
});
|
|
670
|
+
});
|