@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
|
@@ -3,22 +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,
|
|
14
23
|
createActiveContactTurn,
|
|
24
|
+
decodeContactCursor,
|
|
15
25
|
deleteContactMessages,
|
|
26
|
+
encodeContactCursor,
|
|
16
27
|
ensureContactSchema,
|
|
17
28
|
getContactContextBoundary,
|
|
29
|
+
getContactGeneration,
|
|
30
|
+
getLatestContactMessagePreview,
|
|
31
|
+
getLatestContactMessagePreviews,
|
|
18
32
|
insertContactMessage,
|
|
19
33
|
listContactMessages,
|
|
20
34
|
nextContactOrdinal,
|
|
21
35
|
projectActiveContactTurn,
|
|
36
|
+
sanitizeContactPreviewText,
|
|
22
37
|
trimContactHistoryForLlm,
|
|
23
38
|
} from './contact-store.js';
|
|
24
39
|
|
|
@@ -154,6 +169,52 @@ describe('contact LLM history trim', () => {
|
|
|
154
169
|
db.close();
|
|
155
170
|
});
|
|
156
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
|
+
|
|
157
218
|
it('clearContactMemory keeps transcript rows and only advances the LLM boundary', () => {
|
|
158
219
|
const db = openDb();
|
|
159
220
|
const assistantID = 'asst_memory';
|
|
@@ -266,3 +327,344 @@ describe('contact LLM history trim', () => {
|
|
|
266
327
|
db.close();
|
|
267
328
|
});
|
|
268
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
|
+
});
|
|
@@ -370,10 +370,10 @@ const hasAssignSessionIntent = (text) => hasIntentMatch(ASSIGN_SESSION_INTENT, t
|
|
|
370
370
|
|
|
371
371
|
/**
|
|
372
372
|
* True when userText has an explicit, non-negated transcript-wipe phrase.
|
|
373
|
-
*
|
|
374
|
-
*
|
|
373
|
+
* Tool-selection hint only (missed-fence retry / detectRequestedContactTools).
|
|
374
|
+
* Invoking clear_chat_history executes the wipe — no separate text gate.
|
|
375
375
|
*/
|
|
376
|
-
export function
|
|
376
|
+
export function userTextRequestsClearChatHistory(userText) {
|
|
377
377
|
return hasIntentMatch(CLEAR_CHAT_HISTORY_INTENT, typeof userText === 'string' ? userText : '');
|
|
378
378
|
}
|
|
379
379
|
|
|
@@ -389,8 +389,8 @@ export function detectRequestedContactTools(userText, allowedNames = []) {
|
|
|
389
389
|
const text = typeof userText === 'string' ? userText : '';
|
|
390
390
|
const requested = [];
|
|
391
391
|
// Prefer explicit wipe over the safe clear-memory default when both could match.
|
|
392
|
-
// Negation (不要清除聊天记录)
|
|
393
|
-
const wantsClearHistory = allowed.has(CLEAR_CHAT_HISTORY_TOOL_NAME) &&
|
|
392
|
+
// Negation (不要清除聊天记录) is not a wipe request hint.
|
|
393
|
+
const wantsClearHistory = allowed.has(CLEAR_CHAT_HISTORY_TOOL_NAME) && userTextRequestsClearChatHistory(text);
|
|
394
394
|
if (wantsClearHistory) {
|
|
395
395
|
requested.push(CLEAR_CHAT_HISTORY_TOOL_NAME);
|
|
396
396
|
} else if (allowed.has(NEW_CONVERSATION_TOOL_NAME) && hasNewConversationIntent(text)) {
|
|
@@ -799,6 +799,7 @@ export function createContactTools({
|
|
|
799
799
|
if (typeof resetContact !== 'function') {
|
|
800
800
|
throw new AssignError('upstream_error', 'Clearing chat history is unavailable.');
|
|
801
801
|
}
|
|
802
|
+
// Model selected the tool → execute wipe. Storage failures surface as errors.
|
|
802
803
|
await resetContact();
|
|
803
804
|
return {
|
|
804
805
|
content: [{ type: 'text', text: CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE }],
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
NEW_CONVERSATION_TOOL_NAME,
|
|
14
14
|
SCHEDULE_TASK_TOOL_NAME,
|
|
15
15
|
confirmBubbleAfterContactReset,
|
|
16
|
+
contactTurnHasSuccessfulReset,
|
|
16
17
|
createContactTools,
|
|
17
18
|
detectRequestedContactTools,
|
|
18
19
|
filterRegisteredProjects,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
resolveContactProviderModel,
|
|
26
27
|
resolvePeerAssistant,
|
|
27
28
|
stripContactToolFences,
|
|
28
|
-
|
|
29
|
+
userTextRequestsClearChatHistory,
|
|
29
30
|
} from './contact-tools.js';
|
|
30
31
|
import { attachmentScopeKey } from './assign.js';
|
|
31
32
|
|
|
@@ -120,11 +121,11 @@ describe('contact tool protocol', () => {
|
|
|
120
121
|
expect(detectRequestedContactTools('开新对话', tools)).not.toContain(ASSIGN_SESSION_TOOL_NAME);
|
|
121
122
|
expect(detectRequestedContactTools('开新对话', tools)).not.toContain(CLEAR_CHAT_HISTORY_TOOL_NAME);
|
|
122
123
|
expect(detectRequestedContactTools('不要开编码 session', tools)).toEqual([]);
|
|
123
|
-
expect(
|
|
124
|
-
expect(
|
|
125
|
-
expect(
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
124
|
+
expect(userTextRequestsClearChatHistory('清除聊天记录')).toBe(true);
|
|
125
|
+
expect(userTextRequestsClearChatHistory('清空聊天记录')).toBe(true);
|
|
126
|
+
expect(userTextRequestsClearChatHistory('清除记忆')).toBe(false);
|
|
127
|
+
expect(userTextRequestsClearChatHistory('不要清除聊天记录')).toBe(false);
|
|
128
|
+
expect(userTextRequestsClearChatHistory('开新对话')).toBe(false);
|
|
128
129
|
});
|
|
129
130
|
|
|
130
131
|
it('parses bash fences when the pi coding tools are allowed', () => {
|
|
@@ -292,6 +293,23 @@ describe('createContactTools', () => {
|
|
|
292
293
|
expect(wiped.details).toMatchObject({ reset: true, historyCleared: true });
|
|
293
294
|
expect(wiped.terminate).toBe(true);
|
|
294
295
|
expect(wiped.content[0].text).toBe(CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE);
|
|
296
|
+
expect(wiped.content[0].text).not.toMatch(/not cleared|denied|new_conversation instead/i);
|
|
297
|
+
expect(contactTurnHasSuccessfulReset([{
|
|
298
|
+
role: 'toolResult',
|
|
299
|
+
toolName: CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
300
|
+
details: wiped.details,
|
|
301
|
+
}])).toBe(true);
|
|
302
|
+
|
|
303
|
+
// Storage failure still surfaces as a tool error (no silent denial protocol).
|
|
304
|
+
resetContact.mockRejectedValueOnce(Object.assign(new Error('sqlite busy'), { code: 'upstream_error' }));
|
|
305
|
+
const failed = await tools.find((tool) => tool.name === CLEAR_CHAT_HISTORY_TOOL_NAME).execute('call_wipe_fail', {});
|
|
306
|
+
expect(failed.details.error).toBe('upstream_error');
|
|
307
|
+
expect(failed.terminate).toBe(true);
|
|
308
|
+
expect(contactTurnHasSuccessfulReset([{
|
|
309
|
+
role: 'toolResult',
|
|
310
|
+
toolName: CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
311
|
+
details: failed.details,
|
|
312
|
+
}])).toBe(false);
|
|
295
313
|
|
|
296
314
|
const created = await tools.find((tool) => tool.name === CREATE_ASSISTANT_TOOL_NAME).execute('call_1', { name: 'FlowQA', model: 'opencode-go/deepseek-v4-flash' });
|
|
297
315
|
expect(created.details.card).toMatchObject({
|
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
export const assistantContractFixtures = Object.freeze({
|
|
2
|
-
assistant: Object.freeze({
|
|
2
|
+
assistant: Object.freeze({
|
|
3
|
+
id: 'assistant_fixture',
|
|
4
|
+
revision: 4,
|
|
5
|
+
enabled: true,
|
|
6
|
+
name: 'Fixture',
|
|
7
|
+
defaultPrompt: '',
|
|
8
|
+
workspacePath: null,
|
|
9
|
+
managedWorkspacePath: '/data/assistant-workspaces/assistant_fixture',
|
|
10
|
+
effectiveWorkspacePath: '/workspace',
|
|
11
|
+
providerID: 'provider_fixture',
|
|
12
|
+
modelID: 'model_fixture',
|
|
13
|
+
agent: null,
|
|
14
|
+
variant: null,
|
|
15
|
+
mode: 'continuous',
|
|
16
|
+
sessionID: 'ses_fixture',
|
|
17
|
+
sessionGeneration: 4,
|
|
18
|
+
historySessionIDs: Object.freeze([]),
|
|
19
|
+
historySessionCount: 0,
|
|
20
|
+
assignedSessionIDs: Object.freeze([]),
|
|
21
|
+
working: false,
|
|
22
|
+
activeContactTurn: null,
|
|
23
|
+
/** Last visible contact bubble for list desc; null when transcript empty. */
|
|
24
|
+
latestMessagePreview: Object.freeze({
|
|
25
|
+
messageID: 'msg_contact_fixture',
|
|
26
|
+
ordinal: 2,
|
|
27
|
+
role: 'assistant',
|
|
28
|
+
text: 'Fixture reply',
|
|
29
|
+
fallbackKind: null,
|
|
30
|
+
}),
|
|
31
|
+
createdAt: 1234,
|
|
32
|
+
updatedAt: 1234,
|
|
33
|
+
tombstoneAt: null,
|
|
34
|
+
}),
|
|
3
35
|
sessionBinding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }),
|
|
4
36
|
compactResponse: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), summarized: true }),
|
|
5
37
|
messageAdmission: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), messageID: 'msg_fixture', admitted: true, revision: 4 }),
|
|
6
38
|
abortResponse: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), aborted: true }),
|
|
7
39
|
shareOperation: Object.freeze({ operationID: 'share_fixture', assistantID: 'assistant_fixture', sessionID: 'ses_fixture', messageID: 'msg_fixture', state: 'running', phase: 'submitted', attempt: 1, leaseExpiresAt: 1234, errorCode: null }),
|
|
8
40
|
historicalMessages: Object.freeze({ entries: Object.freeze([Object.freeze({ sessionID: 'ses_fixture', directory: null, info: Object.freeze({ id: 'msg_fixture', sessionID: 'ses_fixture', role: 'assistant', time: Object.freeze({ created: 1234 }) }), parts: Object.freeze([]) })]), nextCursor: null, complete: true }),
|
|
41
|
+
/** Frozen GET /assistants/:id/contact/messages page envelope (UI contract). */
|
|
42
|
+
contactMessagesPage: Object.freeze({
|
|
43
|
+
messages: Object.freeze([]),
|
|
44
|
+
nextCursor: null,
|
|
45
|
+
complete: true,
|
|
46
|
+
generation: 0,
|
|
47
|
+
revision: 1,
|
|
48
|
+
}),
|
|
9
49
|
assistantScheduledTasks: Object.freeze({
|
|
10
50
|
tasks: Object.freeze([
|
|
11
51
|
Object.freeze({
|