@openchambery/web 1.19.7-beta.2 → 1.19.7-beta.20
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-DI2zs2WH.js +1 -0
- package/dist/assets/AssistantView-C0p-8f2B.js +2 -0
- package/dist/assets/DiagramView-CSXRZQ5S.js +1 -0
- package/dist/assets/FadeInOnReveal-BWJcUp5P.js +1 -0
- package/dist/assets/Icon-4My8YZwt.js +1 -0
- package/dist/assets/MarkdownRendererImpl-C6rkoZYH.js +7 -0
- package/dist/assets/{MarkstreamRendererImpl-CR0fBG5w.css → MarkstreamRendererImpl-Ca7RATbs.css} +1 -1
- package/dist/assets/MarkstreamRendererImpl-CowOCU5W.js +1 -0
- package/dist/assets/MobileDetailNavigation-ACMbLTqb.js +1 -0
- package/dist/assets/MobileShareBridge-CXR-B1xW.js +4 -0
- package/dist/assets/MobileSurface-CKBSKqUH.js +2 -0
- package/dist/assets/{MultiRunWindow-khpnWyZO.js → MultiRunWindow-el9_nl25.js} +1 -1
- package/dist/assets/OnboardingScreen-BNEtPmvw.js +1 -0
- package/dist/assets/OverlayScrollbar-BZB21Yp_.js +2 -0
- package/dist/assets/SettingsView-CVSiIRia.js +25 -0
- package/dist/assets/{SettingsWindow-Dx5rU9v5.js → SettingsWindow-BXYswtp5.js} +1 -1
- package/dist/assets/TerminalView-Bh0IQwa5.js +8 -0
- package/dist/assets/{ToolOutputDialog-BfuXazW9.js → ToolOutputDialog-XeIJ2etT.js} +3 -3
- package/dist/assets/appThemeRegistry-rSdmh4sW.js +2 -0
- package/dist/assets/{button-z0GOBtWi.js → button-Or0daT9o.js} +1 -1
- package/dist/assets/clipboard-BXxLshVq.js +1 -0
- package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-C-kzSGkq.js} +1 -1
- package/dist/assets/context-menu-Dpjla5p4.js +1 -0
- package/dist/assets/desktopHosts-BzSE0sK7.js +1 -0
- package/dist/assets/desktopRelayRestore-BaKjk1kQ.js +1 -0
- package/dist/assets/{diagram-CoEqmReY.js → diagram-D0mT3mo1.js} +1 -1
- package/dist/assets/{dialog-HCE4eLUW.js → dialog-BkhHBTd4.js} +1 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
- package/dist/assets/{es-D9KcV5TX.js → es-BHqnSjoZ.js} +1 -1
- package/dist/assets/fileQueries-DFY6Kiy1.js +1 -0
- package/dist/assets/fileReferenceActions-3pPDAJSl.js +1 -0
- package/dist/assets/{fr-DPA9qUl3.js → fr-BfrlJY9i.js} +1 -1
- package/dist/assets/{gitApi-DNTMAa3F.js → gitApi-Uvb9w70Y.js} +3 -3
- package/dist/assets/{input-BG3VWTIF.js → input-BUsGnzwe.js} +1 -1
- package/dist/assets/insertionBoundaries-Bgec5nsP.js +4 -0
- package/dist/assets/{ja-Dsif3NoD.js → ja-DP-7qt-x.js} +1 -1
- package/dist/assets/{ko-Dixx4Ctg.js → ko-E2B5vGQP.js} +1 -1
- package/dist/assets/main-COfQt8Sj.js +169 -0
- package/dist/assets/main-PPlpRj1-.js +2 -0
- package/dist/assets/markdown-worker-wEFz5Hm_.js +3 -0
- package/dist/assets/markdownHeightCache-Bj707yG-.js +2 -0
- package/dist/assets/markdownTheme-CVS01wHf.js +2 -0
- package/dist/assets/miniChat-EZGRqQZr.js +2 -0
- package/dist/assets/mobile-DQNqMo5J.js +2 -0
- package/dist/assets/multirun-EViX73vm.js +4 -0
- package/dist/assets/outsideFileGrants-BB4GPvCq.js +1 -0
- package/dist/assets/path-utils-jGozTg9d.js +1 -0
- package/dist/assets/persistence-CSZp1CNB.js +3 -0
- package/dist/assets/{pl-Bj78ZGgf.js → pl-r8X5gRM5.js} +1 -1
- package/dist/assets/platform-CenlWqxu.js +2 -0
- package/dist/assets/projectMeta-Bv1L_nEb.js +3 -0
- package/dist/assets/{pt-BR-CFL3xSAf.js → pt-BR-DcpdXCf5.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-Nsq4Wc4s.js +2 -0
- package/dist/assets/renderMobileApp-5WJDYaMR.js +28 -0
- package/dist/assets/runtimeConfig-DKUQS4iY.js +2 -0
- package/dist/assets/runtimeEndpointReset-BMeu9Hqj.js +147 -0
- package/dist/assets/sessionChangeSummary-CX2MJIkc.js +1 -0
- package/dist/assets/sessionLookup-Sb2HN3G2.js +1 -0
- package/dist/assets/sortable-tabs-strip-C5gGnikj.js +1 -0
- package/dist/assets/src-Af9cMFKF.css +2 -0
- package/dist/assets/{store-DzpbXWX1.js → store-BDFdnk6l.js} +2 -2
- package/dist/assets/terminalApi-CIIs9vFx.js +1 -0
- package/dist/assets/textarea-CIO34TZz.js +1 -0
- package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BSrJYYYc.js} +1 -1
- package/dist/assets/{uk-BY60Ub3Z.js → uk-Di0vl9V-.js} +1 -1
- package/dist/assets/url-CE_gAfNa.js +1 -0
- package/dist/assets/{ChatContainer-CnOrdAgs.js → useAppFontEffects-B_6vc0u3.js} +46 -41
- package/dist/assets/useAssistantUIStore-BHacrxe_.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-B2L5PWHW.js +1 -0
- package/dist/assets/useEffectiveDirectory-CBd5vt8N.js +1 -0
- package/dist/assets/useFontPreferences-BxOHKuVw.js +1 -0
- package/dist/assets/useMobileNavigationStore-CUGaydRW.js +412 -0
- package/dist/assets/useSessionAutoCleanup-Clhzc1wn.js +1 -0
- package/dist/assets/useUIStore-CqqO11ZI.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-C4iQlbPQ.js +7 -0
- package/dist/assets/utils-B0mQXN9L.js +1 -0
- package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
- package/dist/assets/{zh-CN-Cox1LaOW.js → zh-CN-coFCfeQE.js} +1 -1
- package/dist/assets/{zh-TW-B6RU0mxa.js → zh-TW-oDRZ1-yb.js} +1 -1
- package/dist/index.html +9 -11
- package/dist/mini-chat.html +9 -11
- package/dist/mobile.html +15 -17
- package/package.json +2 -1
- package/server/index.js +2 -0
- package/server/lib/assistant-delivery-parts.js +3 -1
- package/server/lib/assistant-delivery-parts.test.js +11 -0
- package/server/lib/assistants/DOCUMENTATION.md +76 -3
- package/server/lib/assistants/assign.js +225 -0
- package/server/lib/assistants/assign.test.js +118 -0
- package/server/lib/assistants/bubbles.js +45 -0
- package/server/lib/assistants/bubbles.test.js +36 -0
- package/server/lib/assistants/cards.js +148 -0
- package/server/lib/assistants/cards.test.js +116 -0
- package/server/lib/assistants/contact-store.js +284 -0
- package/server/lib/assistants/contact-store.test.js +131 -0
- package/server/lib/assistants/contact-tools.js +761 -0
- package/server/lib/assistants/contact-tools.test.js +328 -0
- package/server/lib/assistants/contracts.js +20 -1
- package/server/lib/assistants/harness.js +551 -0
- package/server/lib/assistants/harness.test.js +607 -0
- package/server/lib/assistants/routes.js +44 -4
- package/server/lib/assistants/service.js +930 -17
- package/server/lib/assistants/service.test.js +1163 -45
- package/server/lib/llm/DOCUMENTATION.md +84 -0
- package/server/lib/llm/catalog.js +100 -0
- package/server/lib/llm/catalog.test.js +78 -0
- package/server/lib/llm/completions.js +151 -0
- package/server/lib/llm/completions.test.js +127 -0
- package/server/lib/llm/generate.js +570 -0
- package/server/lib/llm/generate.test.js +459 -0
- package/server/lib/llm/routes.js +47 -0
- package/server/lib/llm/temp-directory.js +37 -0
- package/server/lib/notifications/APNS.md +4 -4
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/apns-runtime.js +139 -20
- package/server/lib/notifications/apns-runtime.test.js +66 -7
- package/server/lib/notifications/apns-titles.js +62 -62
- package/server/lib/notifications/apns-titles.test.js +3 -1
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +81 -49
- package/server/lib/notifications/runtime.test.js +61 -0
- package/server/lib/opencode/feature-routes-runtime.js +30 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
- package/server/lib/opencode/settings-runtime.js +18 -2
- package/server/lib/session-goal/DOCUMENTATION.md +8 -1
- package/server/lib/session-goal/runtime.js +20 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -1
- package/server/lib/session-index/service.js +10 -4
- package/server/lib/session-index/service.test.js +37 -0
- package/server/lib/session-title/runtime.js +1 -0
- package/server/lib/session-title/runtime.test.js +11 -0
- package/dist/assets/AgentAvatar-ziUs4sw3.js +0 -1
- package/dist/assets/AssistantView-DF4yXFgS.js +0 -1
- package/dist/assets/ContextPanel-Cg_uCHzJ.js +0 -285
- package/dist/assets/DiagramView-DYh3Zn2y.js +0 -1
- package/dist/assets/FadeInOnReveal-BAlsRFJH.js +0 -1
- package/dist/assets/Icon-CLpn1mIz.js +0 -1
- package/dist/assets/MarkdownRendererImpl-QzUEpBEm.js +0 -7
- package/dist/assets/MarkstreamRendererImpl-CXrfeUia.js +0 -1
- package/dist/assets/MobileDetailNavigation-DJXEYfq4.js +0 -1
- package/dist/assets/MobileShareBridge-CliVo1YH.js +0 -4
- package/dist/assets/MobileSurface-9Xh6WQkY.js +0 -2
- package/dist/assets/OnboardingScreen-CJYyR54B.js +0 -1
- package/dist/assets/OverlayScrollbar-C-jZ8ITG.js +0 -2
- package/dist/assets/SettingsView-DyCZehFG.js +0 -25
- package/dist/assets/TerminalView-XSALfUaH.js +0 -8
- package/dist/assets/appThemeRegistry-DDWrniMM.js +0 -2
- package/dist/assets/clipboard-Bz0Zm9VJ.js +0 -1
- package/dist/assets/desktop-CfLlmafK.js +0 -1
- package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
- package/dist/assets/desktopRelayRestore-LBt0CBOj.js +0 -1
- package/dist/assets/fileQueries-Dnn15WsA.js +0 -1
- package/dist/assets/insertionBoundaries-DMxuUnL_.js +0 -4
- package/dist/assets/main-B9HVtgXE.js +0 -2
- package/dist/assets/main-ZNyIs3Sn.js +0 -29
- package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
- package/dist/assets/markdownHeightCache-CvUyZT4q.js +0 -1
- package/dist/assets/markdownTheme-TdhQOZSN.js +0 -2
- package/dist/assets/miniChat-q0T0WGkZ.js +0 -2
- package/dist/assets/mobile-DEduvLJj.js +0 -2
- package/dist/assets/multirun-DU-wrxlG.js +0 -4
- package/dist/assets/persistence-CmjKImz2.js +0 -3
- package/dist/assets/platform-skqcMARt.js +0 -2
- package/dist/assets/projectMeta-DNWx6Uva.js +0 -3
- package/dist/assets/radio-BEonIX1k.js +0 -1
- package/dist/assets/renderElectronMiniChatApp--JYWY41L.js +0 -2
- package/dist/assets/renderMobileApp-B0Ix_uNo.js +0 -25
- package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
- package/dist/assets/runtimeConfig-BTDIG7mX.js +0 -2
- package/dist/assets/runtimeEndpointReset-CZQCYtVI.js +0 -4
- package/dist/assets/shikiHighlight-C9Cx56Qg.js +0 -1
- package/dist/assets/sortable-tabs-strip-Bj8N0VWf.js +0 -1
- package/dist/assets/src-2eFAiBLq.css +0 -2
- package/dist/assets/terminalApi-CNqqwjfG.js +0 -1
- package/dist/assets/url-KeihbE0d.js +0 -1
- package/dist/assets/useAppFontEffects-CKLSamae.js +0 -7
- package/dist/assets/useAssistantUIStore-DaplPyb0.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-DJSkFfhT.js +0 -1
- package/dist/assets/useEffectiveDirectory-B3rbY1Kv.js +0 -1
- package/dist/assets/useFontPreferences-D8emhcNv.js +0 -1
- package/dist/assets/useMobileNavigationStore-CAvVUItx.js +0 -412
- package/dist/assets/useSessionAutoCleanup-DxNgqJ8s.js +0 -1
- package/dist/assets/useUIStore-B7KJS5G7.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-DiDF68LR.js +0 -7
- package/dist/assets/utils-CTL16t2f.js +0 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { parseContactCard, parseContactPart, serializeContactPart } from './cards.js';
|
|
2
|
+
|
|
3
|
+
const json = (value) => JSON.stringify(value);
|
|
4
|
+
const parse = (value) => JSON.parse(value);
|
|
5
|
+
|
|
6
|
+
export const CONTACT_SCHEMA_SQL = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS assistant_contact_message (
|
|
8
|
+
message_id TEXT PRIMARY KEY,
|
|
9
|
+
assistant_id TEXT NOT NULL,
|
|
10
|
+
role TEXT NOT NULL,
|
|
11
|
+
turn_id TEXT NOT NULL,
|
|
12
|
+
bubble_index INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
created_at INTEGER NOT NULL,
|
|
14
|
+
ordinal INTEGER NOT NULL,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'complete',
|
|
16
|
+
from_assistant_id TEXT,
|
|
17
|
+
from_assistant_name TEXT
|
|
18
|
+
);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS assistant_contact_message_page
|
|
20
|
+
ON assistant_contact_message(assistant_id, ordinal, message_id);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS assistant_contact_part (
|
|
22
|
+
message_id TEXT NOT NULL,
|
|
23
|
+
part_id TEXT NOT NULL,
|
|
24
|
+
ordinal INTEGER NOT NULL,
|
|
25
|
+
part_json TEXT NOT NULL,
|
|
26
|
+
PRIMARY KEY (message_id, part_id)
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS assistant_contact_part_message
|
|
29
|
+
ON assistant_contact_part(message_id, ordinal, part_id);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS assistant_contact_watch (
|
|
31
|
+
assistant_id TEXT NOT NULL,
|
|
32
|
+
session_id TEXT NOT NULL,
|
|
33
|
+
directory TEXT,
|
|
34
|
+
status TEXT NOT NULL,
|
|
35
|
+
updated_at INTEGER NOT NULL,
|
|
36
|
+
PRIMARY KEY (assistant_id, session_id)
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS assistant_contact_watch_session
|
|
39
|
+
ON assistant_contact_watch(session_id, status);
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const CONTACT_SETTLE_TEXT = Object.freeze({
|
|
43
|
+
complete: 'oc.settle.complete',
|
|
44
|
+
error: 'oc.settle.error',
|
|
45
|
+
question: 'oc.settle.question',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const IN_FLIGHT_WATCH = new Set(['busy', 'question']);
|
|
49
|
+
|
|
50
|
+
export function ensureContactSchema(db) {
|
|
51
|
+
db.exec(CONTACT_SCHEMA_SQL);
|
|
52
|
+
const columns = new Set(db.prepare("SELECT name FROM pragma_table_info('assistant_contact_message')").all().map((column) => column.name));
|
|
53
|
+
if (!columns.has('from_assistant_id')) db.exec('ALTER TABLE assistant_contact_message ADD COLUMN from_assistant_id TEXT');
|
|
54
|
+
if (!columns.has('from_assistant_name')) db.exec('ALTER TABLE assistant_contact_message ADD COLUMN from_assistant_name TEXT');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const decodeCursor = (value) => {
|
|
58
|
+
if (value == null || value === '') return null;
|
|
59
|
+
const parsed = JSON.parse(Buffer.from(String(value), 'base64url').toString('utf8'));
|
|
60
|
+
if (!Number.isSafeInteger(parsed?.ordinal) || typeof parsed?.messageID !== 'string') return null;
|
|
61
|
+
return parsed;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const encodeCursor = (row) => Buffer.from(json({ ordinal: row.ordinal, messageID: row.message_id })).toString('base64url');
|
|
65
|
+
|
|
66
|
+
const hydrateMessage = (db, row) => {
|
|
67
|
+
const parts = db.prepare(
|
|
68
|
+
'SELECT part_json FROM assistant_contact_part WHERE message_id=? ORDER BY ordinal ASC, part_id ASC',
|
|
69
|
+
).all(row.message_id).map((part) => parse(part.part_json)).map(parseContactPart).filter(Boolean);
|
|
70
|
+
const fromAssistantID = typeof row.from_assistant_id === 'string' && row.from_assistant_id.trim() ? row.from_assistant_id : null;
|
|
71
|
+
const storedName = typeof row.from_assistant_name === 'string' && row.from_assistant_name.trim() ? row.from_assistant_name : null;
|
|
72
|
+
let liveName = null;
|
|
73
|
+
if (fromAssistantID) {
|
|
74
|
+
try {
|
|
75
|
+
liveName = db.prepare('SELECT name FROM assistant_v2 WHERE assistant_id=?').get(fromAssistantID)?.name;
|
|
76
|
+
} catch {
|
|
77
|
+
liveName = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
messageID: row.message_id,
|
|
82
|
+
assistantID: row.assistant_id,
|
|
83
|
+
role: row.role,
|
|
84
|
+
turnID: row.turn_id,
|
|
85
|
+
bubbleIndex: row.bubble_index,
|
|
86
|
+
createdAt: row.created_at,
|
|
87
|
+
ordinal: row.ordinal,
|
|
88
|
+
status: row.status,
|
|
89
|
+
fromAssistantID,
|
|
90
|
+
fromAssistantName: storedName || (typeof liveName === 'string' && liveName.trim() ? liveName : null),
|
|
91
|
+
parts,
|
|
92
|
+
text: parts.filter((part) => part.type === 'text').map((part) => part.text).join(''),
|
|
93
|
+
cards: parts.filter((part) => part.type === 'card'),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function nextContactOrdinal(db, assistantID) {
|
|
98
|
+
return Number(db.prepare(
|
|
99
|
+
'SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_contact_message WHERE assistant_id=?',
|
|
100
|
+
).get(assistantID).next);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function insertContactMessage(db, {
|
|
104
|
+
messageID,
|
|
105
|
+
assistantID,
|
|
106
|
+
role,
|
|
107
|
+
turnID,
|
|
108
|
+
bubbleIndex = 0,
|
|
109
|
+
createdAt,
|
|
110
|
+
ordinal,
|
|
111
|
+
status = 'complete',
|
|
112
|
+
parts,
|
|
113
|
+
fromAssistantID = null,
|
|
114
|
+
fromAssistantName = null,
|
|
115
|
+
}) {
|
|
116
|
+
db.prepare(
|
|
117
|
+
'INSERT INTO assistant_contact_message(message_id,assistant_id,role,turn_id,bubble_index,created_at,ordinal,status,from_assistant_id,from_assistant_name) VALUES (?,?,?,?,?,?,?,?,?,?)',
|
|
118
|
+
).run(messageID, assistantID, role, turnID, bubbleIndex, createdAt, ordinal, status, fromAssistantID, fromAssistantName);
|
|
119
|
+
parts.forEach((part, index) => {
|
|
120
|
+
const serialized = serializeContactPart(part, index);
|
|
121
|
+
db.prepare(
|
|
122
|
+
'INSERT INTO assistant_contact_part(message_id,part_id,ordinal,part_json) VALUES (?,?,?,?)',
|
|
123
|
+
).run(messageID, serialized.id, index + 1, json(serialized));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function listContactMessages(db, assistantID, { before, limit = 50 } = {}) {
|
|
128
|
+
const cursor = before == null || before === '' ? null : decodeCursor(before);
|
|
129
|
+
if (before && !cursor) {
|
|
130
|
+
const error = new Error('validation_error');
|
|
131
|
+
error.code = 'validation_error';
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
const rows = cursor
|
|
135
|
+
? db.prepare(
|
|
136
|
+
'SELECT * FROM assistant_contact_message WHERE assistant_id=? AND (ordinal<? OR (ordinal=? AND message_id<?)) ORDER BY ordinal DESC, message_id DESC LIMIT ?',
|
|
137
|
+
).all(assistantID, cursor.ordinal, cursor.ordinal, cursor.messageID, limit + 1)
|
|
138
|
+
: db.prepare(
|
|
139
|
+
'SELECT * FROM assistant_contact_message WHERE assistant_id=? ORDER BY ordinal DESC, message_id DESC LIMIT ?',
|
|
140
|
+
).all(assistantID, limit + 1);
|
|
141
|
+
const page = rows.slice(0, limit);
|
|
142
|
+
const nextCursor = rows.length > limit && page[page.length - 1] ? encodeCursor(page[page.length - 1]) : null;
|
|
143
|
+
return {
|
|
144
|
+
messages: [...page].reverse().map((row) => hydrateMessage(db, row)),
|
|
145
|
+
nextCursor,
|
|
146
|
+
complete: nextCursor === null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function deleteContactMessages(db, assistantID) {
|
|
151
|
+
const ids = db.prepare('SELECT message_id FROM assistant_contact_message WHERE assistant_id=?').all(assistantID).map((row) => row.message_id);
|
|
152
|
+
for (const messageID of ids) {
|
|
153
|
+
db.prepare('DELETE FROM assistant_contact_part WHERE message_id=?').run(messageID);
|
|
154
|
+
}
|
|
155
|
+
db.prepare('DELETE FROM assistant_contact_message WHERE assistant_id=?').run(assistantID);
|
|
156
|
+
db.prepare('DELETE FROM assistant_contact_watch WHERE assistant_id=?').run(assistantID);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function upsertContactWatch(db, { assistantID, sessionID, directory, status, updatedAt }) {
|
|
160
|
+
db.prepare(
|
|
161
|
+
'INSERT INTO assistant_contact_watch(assistant_id,session_id,directory,status,updated_at) VALUES (?,?,?,?,?) ON CONFLICT(assistant_id,session_id) DO UPDATE SET directory=excluded.directory,status=excluded.status,updated_at=excluded.updated_at',
|
|
162
|
+
).run(assistantID, sessionID, directory || null, status, updatedAt);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function listWatchesBySession(db, sessionID) {
|
|
166
|
+
return db.prepare('SELECT assistant_id, session_id, directory, status FROM assistant_contact_watch WHERE session_id=?').all(sessionID)
|
|
167
|
+
.map((row) => ({
|
|
168
|
+
assistantID: row.assistant_id,
|
|
169
|
+
sessionID: row.session_id,
|
|
170
|
+
directory: row.directory,
|
|
171
|
+
status: row.status,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function listInFlightWatches(db, assistantID) {
|
|
176
|
+
const rows = typeof assistantID === 'string' && assistantID
|
|
177
|
+
? db.prepare('SELECT assistant_id, session_id, directory, status FROM assistant_contact_watch WHERE assistant_id=?').all(assistantID)
|
|
178
|
+
: db.prepare('SELECT assistant_id, session_id, directory, status FROM assistant_contact_watch').all();
|
|
179
|
+
return rows
|
|
180
|
+
.filter((row) => IN_FLIGHT_WATCH.has(row.status))
|
|
181
|
+
.map((row) => ({
|
|
182
|
+
assistantID: row.assistant_id,
|
|
183
|
+
sessionID: row.session_id,
|
|
184
|
+
directory: row.directory,
|
|
185
|
+
status: row.status,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function updateSessionCardStatus(db, { assistantID, sessionID, status }) {
|
|
190
|
+
const rows = db.prepare(
|
|
191
|
+
'SELECT p.message_id, p.part_id, p.ordinal, p.part_json FROM assistant_contact_part p JOIN assistant_contact_message m ON m.message_id=p.message_id WHERE m.assistant_id=?',
|
|
192
|
+
).all(assistantID);
|
|
193
|
+
let updated = 0;
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const part = parseContactPart(parse(row.part_json));
|
|
196
|
+
if (!part || part.type !== 'card' || part.cardType !== 'session' || part.sessionID !== sessionID) continue;
|
|
197
|
+
const next = { ...part, status };
|
|
198
|
+
db.prepare('UPDATE assistant_contact_part SET part_json=? WHERE message_id=? AND part_id=?').run(json(serializeContactPart(next, row.ordinal - 1)), row.message_id, row.part_id);
|
|
199
|
+
updated += 1;
|
|
200
|
+
}
|
|
201
|
+
return updated;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* LLM-only contact window. SQLite and the transcript UI may keep older bubbles;
|
|
206
|
+
* this budget is the only model context. There is no summarizer and no
|
|
207
|
+
* user-facing compress / continuous / stateless control on this path.
|
|
208
|
+
*
|
|
209
|
+
* A turn is a user message plus the assistant replies that follow it until the
|
|
210
|
+
* next user. Newest turn is always kept, even when it exceeds the char budget.
|
|
211
|
+
* Char estimate is text length plus CONTACT_LLM_FILE_CHAR_WEIGHT per file part
|
|
212
|
+
* (CJK counts as one char; no tokenizer).
|
|
213
|
+
*/
|
|
214
|
+
export const CONTACT_LLM_MAX_TURNS = 8;
|
|
215
|
+
export const CONTACT_LLM_MAX_CHARS = 6_000;
|
|
216
|
+
export const CONTACT_LLM_FETCH_LIMIT = 40;
|
|
217
|
+
export const CONTACT_LLM_FILE_CHAR_WEIGHT = 80;
|
|
218
|
+
|
|
219
|
+
const estimateLlmChars = (item) => {
|
|
220
|
+
const text = typeof item.content === 'string' ? item.content.length : 0;
|
|
221
|
+
const files = Array.isArray(item.parts) ? item.parts.length * CONTACT_LLM_FILE_CHAR_WEIGHT : 0;
|
|
222
|
+
return text + files;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const toLlmHistoryItem = (message) => {
|
|
226
|
+
const files = message.parts.filter((part) => part.type === 'file');
|
|
227
|
+
const content = message.text.trim() || (files.length > 0 ? '[attachment]' : '');
|
|
228
|
+
return files.length > 0 ? { role: message.role, content, parts: files } : { role: message.role, content };
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const isLlmEligibleContactMessage = (message) => {
|
|
232
|
+
if (message.role !== 'user' && message.role !== 'assistant') return false;
|
|
233
|
+
if (message.status === 'error') return false;
|
|
234
|
+
// Peer DMs are read-only inbox rows. They must not become harness turns.
|
|
235
|
+
if (message.role === 'peer' || message.fromAssistantID) return false;
|
|
236
|
+
const hasText = Boolean(message.text?.trim());
|
|
237
|
+
const hasFiles = Array.isArray(message.parts) && message.parts.some((part) => part.type === 'file');
|
|
238
|
+
// Drop pure-card, tool-trace, and thinking-only rows. Process text is not
|
|
239
|
+
// persisted as contact bubbles; card-only rows have neither text nor files.
|
|
240
|
+
return hasText || hasFiles;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** Turn-aware keep of recent user+assistant text pairs for the model only. */
|
|
244
|
+
export function trimContactHistoryForLlm(messages, {
|
|
245
|
+
maxTurns = CONTACT_LLM_MAX_TURNS,
|
|
246
|
+
maxChars = CONTACT_LLM_MAX_CHARS,
|
|
247
|
+
} = {}) {
|
|
248
|
+
const eligible = (Array.isArray(messages) ? messages : [])
|
|
249
|
+
.filter(isLlmEligibleContactMessage)
|
|
250
|
+
.map(toLlmHistoryItem)
|
|
251
|
+
.filter((item) => item.content.trim().length > 0);
|
|
252
|
+
|
|
253
|
+
const turns = [];
|
|
254
|
+
let current = [];
|
|
255
|
+
for (let index = eligible.length - 1; index >= 0; index -= 1) {
|
|
256
|
+
const item = eligible[index];
|
|
257
|
+
if (item.role === 'user') {
|
|
258
|
+
current.unshift(item);
|
|
259
|
+
turns.push(current);
|
|
260
|
+
current = [];
|
|
261
|
+
} else {
|
|
262
|
+
current.unshift(item);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (current.length > 0) turns.push(current);
|
|
266
|
+
|
|
267
|
+
const kept = [];
|
|
268
|
+
let chars = 0;
|
|
269
|
+
for (const turn of turns) {
|
|
270
|
+
const turnChars = turn.reduce((sum, item) => sum + estimateLlmChars(item), 0);
|
|
271
|
+
if (kept.length >= maxTurns) break;
|
|
272
|
+
if (kept.length > 0 && chars + turnChars > maxChars) break;
|
|
273
|
+
kept.push(turn);
|
|
274
|
+
chars += turnChars;
|
|
275
|
+
}
|
|
276
|
+
return kept.reverse().flat();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function contactHistoryForLlm(db, assistantID) {
|
|
280
|
+
const page = listContactMessages(db, assistantID, { limit: CONTACT_LLM_FETCH_LIMIT });
|
|
281
|
+
return trimContactHistoryForLlm(page.messages);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export { parseContactCard };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { createSessionCardPart } from './cards.js';
|
|
7
|
+
import {
|
|
8
|
+
CONTACT_LLM_FETCH_LIMIT,
|
|
9
|
+
CONTACT_LLM_FILE_CHAR_WEIGHT,
|
|
10
|
+
CONTACT_LLM_MAX_CHARS,
|
|
11
|
+
CONTACT_LLM_MAX_TURNS,
|
|
12
|
+
contactHistoryForLlm,
|
|
13
|
+
deleteContactMessages,
|
|
14
|
+
ensureContactSchema,
|
|
15
|
+
insertContactMessage,
|
|
16
|
+
listContactMessages,
|
|
17
|
+
nextContactOrdinal,
|
|
18
|
+
trimContactHistoryForLlm,
|
|
19
|
+
} from './contact-store.js';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
const openDb = () => {
|
|
24
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'contact-store-'));
|
|
25
|
+
const Database = require('better-sqlite3');
|
|
26
|
+
const db = new Database(path.join(directory, 'contact.sqlite'));
|
|
27
|
+
ensureContactSchema(db);
|
|
28
|
+
return db;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const insert = (db, assistantID, { role, text = '', parts, status = 'complete', fromAssistantID = null, fromAssistantName = null }) => {
|
|
32
|
+
const messageID = crypto.randomUUID();
|
|
33
|
+
insertContactMessage(db, {
|
|
34
|
+
messageID,
|
|
35
|
+
assistantID,
|
|
36
|
+
role,
|
|
37
|
+
turnID: messageID,
|
|
38
|
+
bubbleIndex: 0,
|
|
39
|
+
createdAt: Date.now(),
|
|
40
|
+
ordinal: nextContactOrdinal(db, assistantID),
|
|
41
|
+
status,
|
|
42
|
+
parts: parts || (text ? [{ type: 'text', text }] : [{ type: 'text', text: '' }]),
|
|
43
|
+
fromAssistantID,
|
|
44
|
+
fromAssistantName,
|
|
45
|
+
});
|
|
46
|
+
return messageID;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe('contact LLM history trim', () => {
|
|
50
|
+
it('documents the budget constants in one place', () => {
|
|
51
|
+
expect(CONTACT_LLM_MAX_TURNS).toBe(8);
|
|
52
|
+
expect(CONTACT_LLM_MAX_CHARS).toBe(6_000);
|
|
53
|
+
expect(CONTACT_LLM_FETCH_LIMIT).toBe(40);
|
|
54
|
+
expect(CONTACT_LLM_FILE_CHAR_WEIGHT).toBe(80);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps the newest user+assistant turns and leaves older SQLite rows for the UI', () => {
|
|
58
|
+
const db = openDb();
|
|
59
|
+
const assistantID = 'asst_trim';
|
|
60
|
+
for (let index = 0; index < 12; index += 1) {
|
|
61
|
+
insert(db, assistantID, { role: 'user', text: `user-${index}` });
|
|
62
|
+
insert(db, assistantID, { role: 'assistant', text: `assistant-${index}` });
|
|
63
|
+
}
|
|
64
|
+
const history = contactHistoryForLlm(db, assistantID);
|
|
65
|
+
expect(history).toHaveLength(CONTACT_LLM_MAX_TURNS * 2);
|
|
66
|
+
expect(history[0]).toEqual({ role: 'user', content: 'user-4' });
|
|
67
|
+
expect(history.at(-1)).toEqual({ role: 'assistant', content: 'assistant-11' });
|
|
68
|
+
const page = listContactMessages(db, assistantID, { limit: 100 });
|
|
69
|
+
expect(page.messages).toHaveLength(24);
|
|
70
|
+
expect(page.messages[0].text).toBe('user-0');
|
|
71
|
+
db.close();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('drops peer, error, and pure-card rows from the LLM window', () => {
|
|
75
|
+
const db = openDb();
|
|
76
|
+
const assistantID = 'asst_filter';
|
|
77
|
+
insert(db, assistantID, { role: 'user', text: 'keep-user' });
|
|
78
|
+
insert(db, assistantID, { role: 'assistant', text: 'keep-assistant' });
|
|
79
|
+
insert(db, assistantID, { role: 'peer', text: 'secret-from-peer', fromAssistantID: 'other', fromAssistantName: 'PeerQA' });
|
|
80
|
+
insert(db, assistantID, { role: 'assistant', text: 'failed', status: 'error' });
|
|
81
|
+
insert(db, assistantID, {
|
|
82
|
+
role: 'assistant',
|
|
83
|
+
parts: [createSessionCardPart({ sessionID: 'ses_1', directory: '/repo', title: 'Work', status: 'busy' })],
|
|
84
|
+
});
|
|
85
|
+
expect(contactHistoryForLlm(db, assistantID)).toEqual([
|
|
86
|
+
{ role: 'user', content: 'keep-user' },
|
|
87
|
+
{ role: 'assistant', content: 'keep-assistant' },
|
|
88
|
+
]);
|
|
89
|
+
expect(listContactMessages(db, assistantID, { limit: 50 }).messages).toHaveLength(5);
|
|
90
|
+
db.close();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('stops before an older turn that would exceed the char budget', () => {
|
|
94
|
+
const huge = 'x'.repeat(CONTACT_LLM_MAX_CHARS - 10);
|
|
95
|
+
const messages = [
|
|
96
|
+
{ role: 'user', status: 'complete', text: huge, parts: [{ type: 'text', text: huge }] },
|
|
97
|
+
{ role: 'assistant', status: 'complete', text: 'old-reply', parts: [{ type: 'text', text: 'old-reply' }] },
|
|
98
|
+
{ role: 'user', status: 'complete', text: 'recent', parts: [{ type: 'text', text: 'recent' }] },
|
|
99
|
+
{ role: 'assistant', status: 'complete', text: 'fresh', parts: [{ type: 'text', text: 'fresh' }] },
|
|
100
|
+
];
|
|
101
|
+
expect(trimContactHistoryForLlm(messages)).toEqual([
|
|
102
|
+
{ role: 'user', content: 'recent' },
|
|
103
|
+
{ role: 'assistant', content: 'fresh' },
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('always keeps the newest turn even when it exceeds the char budget', () => {
|
|
108
|
+
const huge = 'y'.repeat(CONTACT_LLM_MAX_CHARS + 200);
|
|
109
|
+
const messages = [
|
|
110
|
+
{ role: 'user', status: 'complete', text: 'older', parts: [{ type: 'text', text: 'older' }] },
|
|
111
|
+
{ role: 'assistant', status: 'complete', text: 'older-reply', parts: [{ type: 'text', text: 'older-reply' }] },
|
|
112
|
+
{ role: 'user', status: 'complete', text: huge, parts: [{ type: 'text', text: huge }] },
|
|
113
|
+
{ role: 'assistant', status: 'complete', text: 'now', parts: [{ type: 'text', text: 'now' }] },
|
|
114
|
+
];
|
|
115
|
+
expect(trimContactHistoryForLlm(messages)).toEqual([
|
|
116
|
+
{ role: 'user', content: huge },
|
|
117
|
+
{ role: 'assistant', content: 'now' },
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('clears LLM history after deleteContactMessages', () => {
|
|
122
|
+
const db = openDb();
|
|
123
|
+
const assistantID = 'asst_reset';
|
|
124
|
+
insert(db, assistantID, { role: 'user', text: 'before' });
|
|
125
|
+
insert(db, assistantID, { role: 'assistant', text: 'reply' });
|
|
126
|
+
deleteContactMessages(db, assistantID);
|
|
127
|
+
expect(contactHistoryForLlm(db, assistantID)).toEqual([]);
|
|
128
|
+
expect(listContactMessages(db, assistantID, { limit: 50 }).messages).toEqual([]);
|
|
129
|
+
db.close();
|
|
130
|
+
});
|
|
131
|
+
});
|