@vellumai/assistant 0.4.23 → 0.4.26

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.
Files changed (63) hide show
  1. package/bun.lock +3 -0
  2. package/package.json +2 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -15
  4. package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
  5. package/src/__tests__/call-controller.test.ts +80 -0
  6. package/src/__tests__/config-schema.test.ts +38 -178
  7. package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
  8. package/src/__tests__/credential-security-invariants.test.ts +0 -2
  9. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
  10. package/src/__tests__/ipc-snapshot.test.ts +0 -9
  11. package/src/__tests__/onboarding-template-contract.test.ts +10 -20
  12. package/src/__tests__/relay-server.test.ts +3 -3
  13. package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
  14. package/src/__tests__/runtime-events-sse.test.ts +7 -0
  15. package/src/__tests__/session-runtime-assembly.test.ts +34 -8
  16. package/src/__tests__/system-prompt.test.ts +7 -1
  17. package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
  18. package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
  19. package/src/__tests__/twilio-routes.test.ts +2 -3
  20. package/src/__tests__/voice-quality.test.ts +21 -132
  21. package/src/calls/call-controller.ts +34 -29
  22. package/src/calls/relay-server.ts +11 -5
  23. package/src/calls/twilio-routes.ts +4 -38
  24. package/src/calls/voice-quality.ts +7 -63
  25. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
  26. package/src/config/bundled-skills/messaging/SKILL.md +3 -5
  27. package/src/config/bundled-skills/phone-calls/SKILL.md +144 -83
  28. package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
  29. package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
  30. package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
  31. package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
  32. package/src/config/calls-schema.ts +3 -53
  33. package/src/config/elevenlabs-schema.ts +33 -0
  34. package/src/config/schema.ts +183 -137
  35. package/src/config/types.ts +0 -1
  36. package/src/daemon/handlers/browser.ts +1 -6
  37. package/src/daemon/ipc-contract/browser.ts +5 -14
  38. package/src/daemon/ipc-contract-inventory.json +0 -2
  39. package/src/daemon/session-agent-loop-handlers.ts +3 -0
  40. package/src/daemon/session-runtime-assembly.ts +9 -7
  41. package/src/mcp/client.ts +2 -1
  42. package/src/memory/conversation-crud.ts +339 -166
  43. package/src/runtime/auth/middleware.ts +87 -26
  44. package/src/runtime/routes/events-routes.ts +7 -0
  45. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  46. package/src/schedule/scheduler.ts +159 -45
  47. package/src/security/secure-keys.ts +3 -3
  48. package/src/tools/browser/browser-manager.ts +72 -228
  49. package/src/tools/browser/browser-screencast.ts +0 -5
  50. package/src/tools/network/script-proxy/certs.ts +7 -237
  51. package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
  52. package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
  53. package/src/tools/network/script-proxy/logging.ts +12 -196
  54. package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
  55. package/src/tools/network/script-proxy/policy.ts +4 -152
  56. package/src/tools/network/script-proxy/router.ts +2 -60
  57. package/src/tools/network/script-proxy/server.ts +5 -137
  58. package/src/tools/network/script-proxy/types.ts +19 -125
  59. package/src/tools/system/voice-config.ts +23 -1
  60. package/src/util/logger.ts +4 -1
  61. package/src/__tests__/elevenlabs-config.test.ts +0 -95
  62. package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
  63. package/src/calls/elevenlabs-config.ts +0 -32
@@ -1,22 +1,34 @@
1
- import { and, asc,count, eq, inArray, isNull, sql } from 'drizzle-orm';
2
- import { v4 as uuid } from 'uuid';
3
- import { z } from 'zod';
4
-
5
- import type { ChannelId, InterfaceId } from '../channels/types.js';
6
- import { parseChannelId, parseInterfaceId } from '../channels/types.js';
7
- import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from '../channels/types.js';
8
- import { getConfig } from '../config/loader.js';
9
- import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
10
- import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
11
- import { getLogger } from '../util/logger.js';
12
- import { createRowMapper } from '../util/row-mapper.js';
13
- import { deleteOrphanAttachments } from './attachments-store.js';
14
- import { projectAssistantMessage } from './conversation-attention-store.js';
15
- import { getDb, rawExec, rawGet } from './db.js';
16
- import { indexMessageNow } from './indexer.js';
17
- import { channelInboundEvents, conversations, llmRequestLogs, memoryEmbeddings, memoryItemEntities, memoryItems, memoryItemSources, memorySegments, messageAttachments, messages, toolInvocations } from './schema.js';
18
-
19
- const log = getLogger('conversation-store');
1
+ import { and, asc, count, eq, inArray, isNull, sql } from "drizzle-orm";
2
+ import { v4 as uuid } from "uuid";
3
+ import { z } from "zod";
4
+
5
+ import type { ChannelId, InterfaceId } from "../channels/types.js";
6
+ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
7
+ import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from "../channels/types.js";
8
+ import { getConfig } from "../config/loader.js";
9
+ import type { GuardianRuntimeContext } from "../daemon/session-runtime-assembly.js";
10
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
11
+ import { getLogger } from "../util/logger.js";
12
+ import { createRowMapper } from "../util/row-mapper.js";
13
+ import { deleteOrphanAttachments } from "./attachments-store.js";
14
+ import { projectAssistantMessage } from "./conversation-attention-store.js";
15
+ import { getDb, rawExec, rawGet } from "./db.js";
16
+ import { indexMessageNow } from "./indexer.js";
17
+ import {
18
+ channelInboundEvents,
19
+ conversations,
20
+ llmRequestLogs,
21
+ memoryEmbeddings,
22
+ memoryItemEntities,
23
+ memoryItems,
24
+ memoryItemSources,
25
+ memorySegments,
26
+ messageAttachments,
27
+ messages,
28
+ toolInvocations,
29
+ } from "./schema.js";
30
+
31
+ const log = getLogger("conversation-store");
20
32
 
21
33
  // ── Message metadata Zod schema ──────────────────────────────────────
22
34
  // Validates the JSON stored in messages.metadata. Known fields are typed;
@@ -28,23 +40,27 @@ const interfaceIdSchema = z.enum(INTERFACE_IDS);
28
40
  const subagentNotificationSchema = z.object({
29
41
  subagentId: z.string(),
30
42
  label: z.string(),
31
- status: z.enum(['completed', 'failed', 'aborted']),
43
+ status: z.enum(["completed", "failed", "aborted"]),
32
44
  error: z.string().optional(),
33
45
  conversationId: z.string().optional(),
34
46
  });
35
47
 
36
- export const messageMetadataSchema = z.object({
37
- userMessageChannel: channelIdSchema.optional(),
38
- assistantMessageChannel: channelIdSchema.optional(),
39
- userMessageInterface: interfaceIdSchema.optional(),
40
- assistantMessageInterface: interfaceIdSchema.optional(),
41
- subagentNotification: subagentNotificationSchema.optional(),
42
- // Provenance fields for trust-aware memory gating (M3)
43
- provenanceTrustClass: z.enum(['guardian', 'trusted_contact', 'unknown']).optional(),
44
- provenanceSourceChannel: channelIdSchema.optional(),
45
- provenanceGuardianExternalUserId: z.string().optional(),
46
- provenanceRequesterIdentifier: z.string().optional(),
47
- }).passthrough();
48
+ export const messageMetadataSchema = z
49
+ .object({
50
+ userMessageChannel: channelIdSchema.optional(),
51
+ assistantMessageChannel: channelIdSchema.optional(),
52
+ userMessageInterface: interfaceIdSchema.optional(),
53
+ assistantMessageInterface: interfaceIdSchema.optional(),
54
+ subagentNotification: subagentNotificationSchema.optional(),
55
+ // Provenance fields for trust-aware memory gating (M3)
56
+ provenanceTrustClass: z
57
+ .enum(["guardian", "trusted_contact", "unknown"])
58
+ .optional(),
59
+ provenanceSourceChannel: channelIdSchema.optional(),
60
+ provenanceGuardianExternalUserId: z.string().optional(),
61
+ provenanceRequesterIdentifier: z.string().optional(),
62
+ })
63
+ .passthrough();
48
64
 
49
65
  export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
50
66
 
@@ -54,8 +70,10 @@ export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
54
70
  * absence of trust context means we cannot verify trust —
55
71
  * callers with actual guardian trust should always supply a real context.
56
72
  */
57
- export function provenanceFromGuardianContext(ctx: GuardianRuntimeContext | null | undefined): Record<string, unknown> {
58
- if (!ctx) return { provenanceTrustClass: 'unknown' };
73
+ export function provenanceFromGuardianContext(
74
+ ctx: GuardianRuntimeContext | null | undefined,
75
+ ): Record<string, unknown> {
76
+ if (!ctx) return { provenanceTrustClass: "unknown" };
59
77
  return {
60
78
  provenanceTrustClass: ctx.trustClass,
61
79
  provenanceSourceChannel: ctx.sourceChannel,
@@ -83,23 +101,26 @@ export interface ConversationRow {
83
101
  isAutoTitle: number;
84
102
  }
85
103
 
86
- export const parseConversation = createRowMapper<typeof conversations.$inferSelect, ConversationRow>({
87
- id: 'id',
88
- title: 'title',
89
- createdAt: 'createdAt',
90
- updatedAt: 'updatedAt',
91
- totalInputTokens: 'totalInputTokens',
92
- totalOutputTokens: 'totalOutputTokens',
93
- totalEstimatedCost: 'totalEstimatedCost',
94
- contextSummary: 'contextSummary',
95
- contextCompactedMessageCount: 'contextCompactedMessageCount',
96
- contextCompactedAt: 'contextCompactedAt',
97
- threadType: 'threadType',
98
- source: 'source',
99
- memoryScopeId: 'memoryScopeId',
100
- originChannel: 'originChannel',
101
- originInterface: 'originInterface',
102
- isAutoTitle: 'isAutoTitle',
104
+ export const parseConversation = createRowMapper<
105
+ typeof conversations.$inferSelect,
106
+ ConversationRow
107
+ >({
108
+ id: "id",
109
+ title: "title",
110
+ createdAt: "createdAt",
111
+ updatedAt: "updatedAt",
112
+ totalInputTokens: "totalInputTokens",
113
+ totalOutputTokens: "totalOutputTokens",
114
+ totalEstimatedCost: "totalEstimatedCost",
115
+ contextSummary: "contextSummary",
116
+ contextCompactedMessageCount: "contextCompactedMessageCount",
117
+ contextCompactedAt: "contextCompactedAt",
118
+ threadType: "threadType",
119
+ source: "source",
120
+ memoryScopeId: "memoryScopeId",
121
+ originChannel: "originChannel",
122
+ originInterface: "originInterface",
123
+ isAutoTitle: "isAutoTitle",
103
124
  });
104
125
 
105
126
  export interface MessageRow {
@@ -111,13 +132,16 @@ export interface MessageRow {
111
132
  metadata: string | null;
112
133
  }
113
134
 
114
- export const parseMessage = createRowMapper<typeof messages.$inferSelect, MessageRow>({
115
- id: 'id',
116
- conversationId: 'conversationId',
117
- role: 'role',
118
- content: 'content',
119
- createdAt: 'createdAt',
120
- metadata: 'metadata',
135
+ export const parseMessage = createRowMapper<
136
+ typeof messages.$inferSelect,
137
+ MessageRow
138
+ >({
139
+ id: "id",
140
+ conversationId: "conversationId",
141
+ role: "role",
142
+ content: "content",
143
+ createdAt: "createdAt",
144
+ metadata: "metadata",
121
145
  });
122
146
 
123
147
  /**
@@ -134,14 +158,26 @@ function monotonicNow(): number {
134
158
  return lastTimestamp;
135
159
  }
136
160
 
137
- export function createConversation(titleOrOpts?: string | { title?: string; threadType?: 'standard' | 'private' | 'background'; source?: string }) {
161
+ export function createConversation(
162
+ titleOrOpts?:
163
+ | string
164
+ | {
165
+ title?: string;
166
+ threadType?: "standard" | "private" | "background";
167
+ source?: string;
168
+ scheduleJobId?: string;
169
+ },
170
+ ) {
138
171
  const db = getDb();
139
172
  const now = Date.now();
140
- const opts = typeof titleOrOpts === 'string' ? { title: titleOrOpts } : (titleOrOpts ?? {});
141
- const threadType = opts.threadType ?? 'standard';
142
- const source = opts.source ?? 'user';
173
+ const opts =
174
+ typeof titleOrOpts === "string"
175
+ ? { title: titleOrOpts }
176
+ : (titleOrOpts ?? {});
177
+ const threadType = opts.threadType ?? "standard";
178
+ const source = opts.source ?? "user";
143
179
  const id = uuid();
144
- const memoryScopeId = threadType === 'private' ? `private:${id}` : 'default';
180
+ const memoryScopeId = threadType === "private" ? `private:${id}` : "default";
145
181
  const conversation = {
146
182
  id,
147
183
  title: opts.title ?? null,
@@ -156,6 +192,7 @@ export function createConversation(titleOrOpts?: string | { title?: string; thre
156
192
  threadType,
157
193
  source,
158
194
  memoryScopeId,
195
+ scheduleJobId: opts.scheduleJobId ?? null,
159
196
  };
160
197
 
161
198
  // Retry on SQLITE_BUSY and SQLITE_IOERR — transient disk I/O errors or WAL
@@ -166,9 +203,15 @@ export function createConversation(titleOrOpts?: string | { title?: string; thre
166
203
  db.insert(conversations).values(conversation).run();
167
204
  break;
168
205
  } catch (err) {
169
- const code = (err as { code?: string }).code ?? '';
170
- if (attempt < MAX_RETRIES && (code.startsWith('SQLITE_BUSY') || code.startsWith('SQLITE_IOERR'))) {
171
- log.warn({ attempt, conversationId: id, code }, 'createConversation: transient SQLite error, retrying');
206
+ const code = (err as { code?: string }).code ?? "";
207
+ if (
208
+ attempt < MAX_RETRIES &&
209
+ (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_IOERR"))
210
+ ) {
211
+ log.warn(
212
+ { attempt, conversationId: id, code },
213
+ "createConversation: transient SQLite error, retrying",
214
+ );
172
215
  // Synchronous sleep — createConversation is synchronous and the
173
216
  // retry window is short (50-150ms), so Bun.sleepSync is appropriate.
174
217
  Bun.sleepSync(50 * (attempt + 1));
@@ -191,15 +234,17 @@ export function getConversation(id: string): ConversationRow | null {
191
234
  return row ? parseConversation(row) : null;
192
235
  }
193
236
 
194
- export function getConversationThreadType(conversationId: string): 'standard' | 'private' {
237
+ export function getConversationThreadType(
238
+ conversationId: string,
239
+ ): "standard" | "private" {
195
240
  const conv = getConversation(conversationId);
196
241
  const raw = conv?.threadType;
197
- return raw === 'private' ? 'private' : 'standard';
242
+ return raw === "private" ? "private" : "standard";
198
243
  }
199
244
 
200
245
  export function getConversationMemoryScopeId(conversationId: string): string {
201
246
  const conv = getConversation(conversationId);
202
- return conv?.memoryScopeId ?? 'default';
247
+ return conv?.memoryScopeId ?? "default";
203
248
  }
204
249
 
205
250
  /**
@@ -210,21 +255,34 @@ export function getConversationMemoryScopeId(conversationId: string): string {
210
255
  export function deleteConversation(id: string): void {
211
256
  const db = getDb();
212
257
  db.transaction((tx) => {
213
- tx.delete(llmRequestLogs).where(eq(llmRequestLogs.conversationId, id)).run();
214
- tx.delete(toolInvocations).where(eq(toolInvocations.conversationId, id)).run();
258
+ tx.delete(llmRequestLogs)
259
+ .where(eq(llmRequestLogs.conversationId, id))
260
+ .run();
261
+ tx.delete(toolInvocations)
262
+ .where(eq(toolInvocations.conversationId, id))
263
+ .run();
215
264
  tx.delete(messages).where(eq(messages.conversationId, id)).run();
216
265
  tx.delete(conversations).where(eq(conversations.id, id)).run();
217
266
  });
218
267
  }
219
268
 
220
- export async function addMessage(conversationId: string, role: string, content: string, metadata?: Record<string, unknown>, opts?: { skipIndexing?: boolean }) {
269
+ export async function addMessage(
270
+ conversationId: string,
271
+ role: string,
272
+ content: string,
273
+ metadata?: Record<string, unknown>,
274
+ opts?: { skipIndexing?: boolean },
275
+ ) {
221
276
  const db = getDb();
222
277
  const messageId = uuid();
223
278
 
224
279
  if (metadata) {
225
280
  const result = messageMetadataSchema.safeParse(metadata);
226
281
  if (!result.success) {
227
- log.warn({ conversationId, messageId, issues: result.error.issues }, 'Invalid message metadata, storing as-is');
282
+ log.warn(
283
+ { conversationId, messageId, issues: result.error.issues },
284
+ "Invalid message metadata, storing as-is",
285
+ );
228
286
  }
229
287
  }
230
288
 
@@ -255,7 +313,12 @@ export async function addMessage(conversationId: string, role: string, content:
255
313
  if (originChannelCandidate) {
256
314
  tx.update(conversations)
257
315
  .set({ originChannel: originChannelCandidate })
258
- .where(and(eq(conversations.id, conversationId), isNull(conversations.originChannel)))
316
+ .where(
317
+ and(
318
+ eq(conversations.id, conversationId),
319
+ isNull(conversations.originChannel),
320
+ ),
321
+ )
259
322
  .run();
260
323
  }
261
324
  tx.update(conversations)
@@ -265,38 +328,62 @@ export async function addMessage(conversationId: string, role: string, content:
265
328
  });
266
329
  break;
267
330
  } catch (err) {
268
- const errCode = (err as { code?: string }).code ?? '';
269
- if (attempt < MAX_RETRIES && (errCode.startsWith('SQLITE_BUSY') || errCode.startsWith('SQLITE_IOERR'))) {
270
- log.warn({ attempt, conversationId, code: errCode }, 'addMessage: transient SQLite error, retrying');
331
+ const errCode = (err as { code?: string }).code ?? "";
332
+ if (
333
+ attempt < MAX_RETRIES &&
334
+ (errCode.startsWith("SQLITE_BUSY") ||
335
+ errCode.startsWith("SQLITE_IOERR"))
336
+ ) {
337
+ log.warn(
338
+ { attempt, conversationId, code: errCode },
339
+ "addMessage: transient SQLite error, retrying",
340
+ );
271
341
  await Bun.sleep(50 * (attempt + 1));
272
342
  continue;
273
343
  }
274
344
  throw err;
275
345
  }
276
346
  }
277
- const message = { id: messageId, conversationId, role, content, createdAt: now, ...(metadataStr ? { metadata: metadataStr } : {}) };
347
+ const message = {
348
+ id: messageId,
349
+ conversationId,
350
+ role,
351
+ content,
352
+ createdAt: now,
353
+ ...(metadataStr ? { metadata: metadataStr } : {}),
354
+ };
278
355
 
279
356
  if (!opts?.skipIndexing) {
280
357
  try {
281
358
  const config = getConfig();
282
359
  const scopeId = getConversationMemoryScopeId(conversationId);
283
- const parsed = metadata ? messageMetadataSchema.safeParse(metadata) : null;
284
- const provenanceTrustClass = parsed?.success ? parsed.data.provenanceTrustClass : undefined;
285
- indexMessageNow({
286
- messageId: message.id,
287
- conversationId: message.conversationId,
288
- role: message.role,
289
- content: message.content,
290
- createdAt: message.createdAt,
291
- scopeId,
292
- provenanceTrustClass,
293
- }, config.memory);
360
+ const parsed = metadata
361
+ ? messageMetadataSchema.safeParse(metadata)
362
+ : null;
363
+ const provenanceTrustClass = parsed?.success
364
+ ? parsed.data.provenanceTrustClass
365
+ : undefined;
366
+ indexMessageNow(
367
+ {
368
+ messageId: message.id,
369
+ conversationId: message.conversationId,
370
+ role: message.role,
371
+ content: message.content,
372
+ createdAt: message.createdAt,
373
+ scopeId,
374
+ provenanceTrustClass,
375
+ },
376
+ config.memory,
377
+ );
294
378
  } catch (err) {
295
- log.warn({ err, conversationId, messageId: message.id }, 'Failed to index message for memory');
379
+ log.warn(
380
+ { err, conversationId, messageId: message.id },
381
+ "Failed to index message for memory",
382
+ );
296
383
  }
297
384
  }
298
385
 
299
- if (role === 'assistant') {
386
+ if (role === "assistant") {
300
387
  try {
301
388
  projectAssistantMessage({
302
389
  conversationId,
@@ -305,7 +392,10 @@ export async function addMessage(conversationId: string, role: string, content:
305
392
  messageAt: message.createdAt,
306
393
  });
307
394
  } catch (err) {
308
- log.warn({ err, conversationId, messageId: message.id }, 'Failed to project assistant message for attention tracking');
395
+ log.warn(
396
+ { err, conversationId, messageId: message.id },
397
+ "Failed to project assistant message for attention tracking",
398
+ );
309
399
  }
310
400
  }
311
401
 
@@ -324,7 +414,10 @@ export function getMessages(conversationId: string): MessageRow[] {
324
414
  }
325
415
 
326
416
  /** Fetch a single message by ID, optionally scoped to a specific conversation. */
327
- export function getMessageById(messageId: string, conversationId?: string): MessageRow | null {
417
+ export function getMessageById(
418
+ messageId: string,
419
+ conversationId?: string,
420
+ ): MessageRow | null {
328
421
  const db = getDb();
329
422
  const conditions = [eq(messages.id, messageId)];
330
423
  if (conversationId) {
@@ -338,14 +431,15 @@ export function getMessageById(messageId: string, conversationId?: string): Mess
338
431
  return row ? parseMessage(row) : null;
339
432
  }
340
433
 
341
- export function updateConversationTitle(id: string, title: string, isAutoTitle?: number): void {
434
+ export function updateConversationTitle(
435
+ id: string,
436
+ title: string,
437
+ isAutoTitle?: number,
438
+ ): void {
342
439
  const db = getDb();
343
440
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
344
441
  if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
345
- db.update(conversations)
346
- .set(set)
347
- .where(eq(conversations.id, id))
348
- .run();
442
+ db.update(conversations).set(set).where(eq(conversations.id, id)).run();
349
443
  }
350
444
 
351
445
  export function updateConversationUsage(
@@ -356,7 +450,12 @@ export function updateConversationUsage(
356
450
  ): void {
357
451
  const db = getDb();
358
452
  db.update(conversations)
359
- .set({ totalInputTokens, totalOutputTokens, totalEstimatedCost, updatedAt: Date.now() })
453
+ .set({
454
+ totalInputTokens,
455
+ totalOutputTokens,
456
+ totalEstimatedCost,
457
+ updatedAt: Date.now(),
458
+ })
360
459
  .where(eq(conversations.id, id))
361
460
  .run();
362
461
  }
@@ -384,8 +483,10 @@ export function updateConversationContextWindow(
384
483
  * Returns { conversations, messages } counts.
385
484
  */
386
485
  export function clearAll(): { conversations: number; messages: number } {
387
- const msgCount = rawGet<{ c: number }>('SELECT COUNT(*) AS c FROM messages')?.c ?? 0;
388
- const convCount = rawGet<{ c: number }>('SELECT COUNT(*) AS c FROM conversations')?.c ?? 0;
486
+ const msgCount =
487
+ rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM messages")?.c ?? 0;
488
+ const convCount =
489
+ rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM conversations")?.c ?? 0;
389
490
 
390
491
  // Delete in dependency order. Cascades handle memory_segments,
391
492
  // memory_item_sources, and tool_invocations, but we explicitly
@@ -398,56 +499,78 @@ export function clearAll(): { conversations: number; messages: number } {
398
499
  // corrupted FTS table would roll back every base-table DELETE).
399
500
  let segmentFtsCorrupted = false;
400
501
  try {
401
- rawExec('DELETE FROM memory_segment_fts');
502
+ rawExec("DELETE FROM memory_segment_fts");
402
503
  } catch (err) {
403
- log.warn({ err }, 'clearAll: failed to clear memory_segment_fts — dropping triggers so base-table cleanup can proceed');
404
- rawExec('DROP TRIGGER IF EXISTS memory_segments_ai');
405
- rawExec('DROP TRIGGER IF EXISTS memory_segments_ad');
406
- rawExec('DROP TRIGGER IF EXISTS memory_segments_au');
504
+ log.warn(
505
+ { err },
506
+ "clearAll: failed to clear memory_segment_fts — dropping triggers so base-table cleanup can proceed",
507
+ );
508
+ rawExec("DROP TRIGGER IF EXISTS memory_segments_ai");
509
+ rawExec("DROP TRIGGER IF EXISTS memory_segments_ad");
510
+ rawExec("DROP TRIGGER IF EXISTS memory_segments_au");
407
511
  segmentFtsCorrupted = true;
408
512
  }
409
- rawExec('DELETE FROM memory_item_sources');
410
- rawExec('DELETE FROM memory_segments');
411
- rawExec('DELETE FROM memory_items');
412
- rawExec('DELETE FROM memory_summaries');
413
- rawExec('DELETE FROM memory_embeddings');
414
- rawExec('DELETE FROM memory_jobs');
415
- rawExec('DELETE FROM memory_checkpoints');
416
- rawExec('DELETE FROM llm_request_logs');
417
- rawExec('DELETE FROM llm_usage_events');
418
- rawExec('DELETE FROM message_attachments');
419
- rawExec('DELETE FROM attachments');
420
- rawExec('DELETE FROM tool_invocations');
513
+ rawExec("DELETE FROM memory_item_sources");
514
+ rawExec("DELETE FROM memory_segments");
515
+ rawExec("DELETE FROM memory_items");
516
+ rawExec("DELETE FROM memory_summaries");
517
+ rawExec("DELETE FROM memory_embeddings");
518
+ rawExec("DELETE FROM memory_jobs");
519
+ rawExec("DELETE FROM memory_checkpoints");
520
+ rawExec("DELETE FROM llm_request_logs");
521
+ rawExec("DELETE FROM llm_usage_events");
522
+ rawExec("DELETE FROM message_attachments");
523
+ rawExec("DELETE FROM attachments");
524
+ rawExec("DELETE FROM tool_invocations");
421
525
  let messagesFtsCorrupted = false;
422
526
  try {
423
- rawExec('DELETE FROM messages_fts');
527
+ rawExec("DELETE FROM messages_fts");
424
528
  } catch (err) {
425
- log.warn({ err }, 'clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed');
426
- rawExec('DROP TRIGGER IF EXISTS messages_fts_ai');
427
- rawExec('DROP TRIGGER IF EXISTS messages_fts_ad');
428
- rawExec('DROP TRIGGER IF EXISTS messages_fts_au');
529
+ log.warn(
530
+ { err },
531
+ "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed",
532
+ );
533
+ rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
534
+ rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
535
+ rawExec("DROP TRIGGER IF EXISTS messages_fts_au");
429
536
  messagesFtsCorrupted = true;
430
537
  }
431
- rawExec('DELETE FROM messages');
432
- rawExec('DELETE FROM conversations');
538
+ rawExec("DELETE FROM messages");
539
+ rawExec("DELETE FROM conversations");
433
540
 
434
541
  // Rebuild corrupted FTS tables and restore triggers after all base-table
435
542
  // DELETEs have completed. Dropping the virtual table clears the corruption,
436
543
  // and recreating it + triggers means subsequent writes maintain FTS
437
544
  // consistency without requiring a daemon restart.
438
545
  if (segmentFtsCorrupted) {
439
- rawExec('DROP TABLE IF EXISTS memory_segment_fts');
440
- rawExec(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_segment_fts USING fts5(segment_id UNINDEXED, text)`);
441
- rawExec(`CREATE TRIGGER IF NOT EXISTS memory_segments_ai AFTER INSERT ON memory_segments BEGIN INSERT INTO memory_segment_fts(segment_id, text) VALUES (new.id, new.text); END`);
442
- rawExec(`CREATE TRIGGER IF NOT EXISTS memory_segments_ad AFTER DELETE ON memory_segments BEGIN DELETE FROM memory_segment_fts WHERE segment_id = old.id; END`);
443
- rawExec(`CREATE TRIGGER IF NOT EXISTS memory_segments_au AFTER UPDATE ON memory_segments BEGIN DELETE FROM memory_segment_fts WHERE segment_id = old.id; INSERT INTO memory_segment_fts(segment_id, text) VALUES (new.id, new.text); END`);
546
+ rawExec("DROP TABLE IF EXISTS memory_segment_fts");
547
+ rawExec(
548
+ `CREATE VIRTUAL TABLE IF NOT EXISTS memory_segment_fts USING fts5(segment_id UNINDEXED, text)`,
549
+ );
550
+ rawExec(
551
+ `CREATE TRIGGER IF NOT EXISTS memory_segments_ai AFTER INSERT ON memory_segments BEGIN INSERT INTO memory_segment_fts(segment_id, text) VALUES (new.id, new.text); END`,
552
+ );
553
+ rawExec(
554
+ `CREATE TRIGGER IF NOT EXISTS memory_segments_ad AFTER DELETE ON memory_segments BEGIN DELETE FROM memory_segment_fts WHERE segment_id = old.id; END`,
555
+ );
556
+ rawExec(
557
+ `CREATE TRIGGER IF NOT EXISTS memory_segments_au AFTER UPDATE ON memory_segments BEGIN DELETE FROM memory_segment_fts WHERE segment_id = old.id; INSERT INTO memory_segment_fts(segment_id, text) VALUES (new.id, new.text); END`,
558
+ );
444
559
  }
445
560
  if (messagesFtsCorrupted) {
446
- rawExec('DROP TABLE IF EXISTS messages_fts');
447
- rawExec(`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`);
448
- rawExec(`CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`);
449
- rawExec(`CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`);
450
- rawExec(`CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`);
561
+ rawExec("DROP TABLE IF EXISTS messages_fts");
562
+ rawExec(
563
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`,
564
+ );
565
+ rawExec(
566
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`,
567
+ );
568
+ rawExec(
569
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`,
570
+ );
571
+ rawExec(
572
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`,
573
+ );
451
574
  }
452
575
 
453
576
  return { conversations: convCount, messages: msgCount };
@@ -460,7 +583,12 @@ export function deleteLastExchange(conversationId: string): number {
460
583
  const lastUserMsg = db
461
584
  .select({ id: messages.id })
462
585
  .from(messages)
463
- .where(and(eq(messages.conversationId, conversationId), eq(messages.role, 'user')))
586
+ .where(
587
+ and(
588
+ eq(messages.conversationId, conversationId),
589
+ eq(messages.role, "user"),
590
+ ),
591
+ )
464
592
  .orderBy(sql`rowid DESC`)
465
593
  .limit(1)
466
594
  .get();
@@ -476,20 +604,31 @@ export function deleteLastExchange(conversationId: string): number {
476
604
  sql`rowid >= ${rowidSubquery}`,
477
605
  );
478
606
 
479
- const [{ deleted }] = db.select({ deleted: count() }).from(messages).where(condition).all();
607
+ const [{ deleted }] = db
608
+ .select({ deleted: count() })
609
+ .from(messages)
610
+ .where(condition)
611
+ .all();
480
612
  if (deleted === 0) return 0;
481
613
 
482
614
  // Collect attachment IDs linked to the messages being deleted so we can
483
615
  // scope orphan cleanup to only those candidates (not freshly uploaded ones).
484
- const messageIds = db.select({ id: messages.id }).from(messages).where(condition).all().map((r) => r.id);
485
- const candidateAttachmentIds = messageIds.length > 0
486
- ? db.select({ attachmentId: messageAttachments.attachmentId })
487
- .from(messageAttachments)
488
- .where(inArray(messageAttachments.messageId, messageIds))
489
- .all()
490
- .map((r) => r.attachmentId)
491
- .filter((id): id is string => id != null)
492
- : [];
616
+ const messageIds = db
617
+ .select({ id: messages.id })
618
+ .from(messages)
619
+ .where(condition)
620
+ .all()
621
+ .map((r) => r.id);
622
+ const candidateAttachmentIds =
623
+ messageIds.length > 0
624
+ ? db
625
+ .select({ attachmentId: messageAttachments.attachmentId })
626
+ .from(messageAttachments)
627
+ .where(inArray(messageAttachments.messageId, messageIds))
628
+ .all()
629
+ .map((r) => r.attachmentId)
630
+ .filter((id): id is string => id != null)
631
+ : [];
493
632
 
494
633
  db.transaction((tx) => {
495
634
  tx.delete(messages).where(condition).run();
@@ -518,7 +657,10 @@ export interface DeletedMemoryIds {
518
657
  * Update the content of an existing message. Used when consolidating
519
658
  * multiple assistant messages into one.
520
659
  */
521
- export function updateMessageContent(messageId: string, newContent: string): void {
660
+ export function updateMessageContent(
661
+ messageId: string,
662
+ newContent: string,
663
+ ): void {
522
664
  const db = getDb();
523
665
  db.update(messages)
524
666
  .set({ content: newContent })
@@ -531,7 +673,10 @@ export function updateMessageContent(messageId: string, newContent: string): voi
531
673
  * Used during message consolidation so that attachments linked to deleted
532
674
  * messages survive the ON DELETE CASCADE on message_attachments.
533
675
  */
534
- export function relinkAttachments(fromMessageIds: string[], toMessageId: string): number {
676
+ export function relinkAttachments(
677
+ fromMessageIds: string[],
678
+ toMessageId: string,
679
+ ): number {
535
680
  if (fromMessageIds.length === 0) return 0;
536
681
  const db = getDb();
537
682
 
@@ -612,10 +757,12 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
612
757
  // Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
613
758
  if (result.segmentIds.length > 0) {
614
759
  tx.delete(memoryEmbeddings)
615
- .where(and(
616
- eq(memoryEmbeddings.targetType, 'segment'),
617
- inArray(memoryEmbeddings.targetId, result.segmentIds),
618
- ))
760
+ .where(
761
+ and(
762
+ eq(memoryEmbeddings.targetType, "segment"),
763
+ inArray(memoryEmbeddings.targetId, result.segmentIds),
764
+ ),
765
+ )
619
766
  .run();
620
767
  }
621
768
 
@@ -628,7 +775,9 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
628
775
  .where(inArray(memoryItemSources.memoryItemId, candidateItemIds))
629
776
  .all();
630
777
  const survivingIds = new Set(surviving.map((r) => r.memoryItemId));
631
- const orphanedIds = candidateItemIds.filter((id) => !survivingIds.has(id));
778
+ const orphanedIds = candidateItemIds.filter(
779
+ (id) => !survivingIds.has(id),
780
+ );
632
781
  result.orphanedItemIds = orphanedIds;
633
782
 
634
783
  if (orphanedIds.length > 0) {
@@ -638,10 +787,12 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
638
787
  .run();
639
788
  // Delete embeddings referencing these items.
640
789
  tx.delete(memoryEmbeddings)
641
- .where(and(
642
- eq(memoryEmbeddings.targetType, 'item'),
643
- inArray(memoryEmbeddings.targetId, orphanedIds),
644
- ))
790
+ .where(
791
+ and(
792
+ eq(memoryEmbeddings.targetType, "item"),
793
+ inArray(memoryEmbeddings.targetId, orphanedIds),
794
+ ),
795
+ )
645
796
  .run();
646
797
  // Delete the orphaned memory items themselves.
647
798
  tx.delete(memoryItems)
@@ -656,34 +807,56 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
656
807
  return result;
657
808
  }
658
809
 
659
- export function setConversationOriginChannelIfUnset(conversationId: string, channel: ChannelId): void {
810
+ export function setConversationOriginChannelIfUnset(
811
+ conversationId: string,
812
+ channel: ChannelId,
813
+ ): void {
660
814
  const db = getDb();
661
815
  db.update(conversations)
662
816
  .set({ originChannel: channel })
663
- .where(and(eq(conversations.id, conversationId), isNull(conversations.originChannel)))
817
+ .where(
818
+ and(
819
+ eq(conversations.id, conversationId),
820
+ isNull(conversations.originChannel),
821
+ ),
822
+ )
664
823
  .run();
665
824
  }
666
825
 
667
- export function getConversationOriginChannel(conversationId: string): ChannelId | null {
826
+ export function getConversationOriginChannel(
827
+ conversationId: string,
828
+ ): ChannelId | null {
668
829
  const db = getDb();
669
- const row = db.select({ originChannel: conversations.originChannel })
830
+ const row = db
831
+ .select({ originChannel: conversations.originChannel })
670
832
  .from(conversations)
671
833
  .where(eq(conversations.id, conversationId))
672
834
  .get();
673
835
  return parseChannelId(row?.originChannel) ?? null;
674
836
  }
675
837
 
676
- export function setConversationOriginInterfaceIfUnset(conversationId: string, interfaceId: InterfaceId): void {
838
+ export function setConversationOriginInterfaceIfUnset(
839
+ conversationId: string,
840
+ interfaceId: InterfaceId,
841
+ ): void {
677
842
  const db = getDb();
678
843
  db.update(conversations)
679
844
  .set({ originInterface: interfaceId })
680
- .where(and(eq(conversations.id, conversationId), isNull(conversations.originInterface)))
845
+ .where(
846
+ and(
847
+ eq(conversations.id, conversationId),
848
+ isNull(conversations.originInterface),
849
+ ),
850
+ )
681
851
  .run();
682
852
  }
683
853
 
684
- export function getConversationOriginInterface(conversationId: string): InterfaceId | null {
854
+ export function getConversationOriginInterface(
855
+ conversationId: string,
856
+ ): InterfaceId | null {
685
857
  const db = getDb();
686
- const row = db.select({ originInterface: conversations.originInterface })
858
+ const row = db
859
+ .select({ originInterface: conversations.originInterface })
687
860
  .from(conversations)
688
861
  .where(eq(conversations.id, conversationId))
689
862
  .get();
@@ -700,7 +873,7 @@ export function getConversationOriginInterface(conversationId: string): Interfac
700
873
  */
701
874
  export function getConversationRecentProvenanceTrustClass(
702
875
  conversationId: string,
703
- ): 'guardian' | 'trusted_contact' | 'unknown' | undefined {
876
+ ): "guardian" | "trusted_contact" | "unknown" | undefined {
704
877
  const row = rawGet<{ metadata: string | null }>(
705
878
  `SELECT metadata FROM messages
706
879
  WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL