@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.
- package/bun.lock +3 -0
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -15
- package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
- package/src/__tests__/call-controller.test.ts +80 -0
- package/src/__tests__/config-schema.test.ts +38 -178
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
- package/src/__tests__/credential-security-invariants.test.ts +0 -2
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
- package/src/__tests__/ipc-snapshot.test.ts +0 -9
- package/src/__tests__/onboarding-template-contract.test.ts +10 -20
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
- package/src/__tests__/runtime-events-sse.test.ts +7 -0
- package/src/__tests__/session-runtime-assembly.test.ts +34 -8
- package/src/__tests__/system-prompt.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
- package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/voice-quality.test.ts +21 -132
- package/src/calls/call-controller.ts +34 -29
- package/src/calls/relay-server.ts +11 -5
- package/src/calls/twilio-routes.ts +4 -38
- package/src/calls/voice-quality.ts +7 -63
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +144 -83
- package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
- package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
- package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
- package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
- package/src/config/calls-schema.ts +3 -53
- package/src/config/elevenlabs-schema.ts +33 -0
- package/src/config/schema.ts +183 -137
- package/src/config/types.ts +0 -1
- package/src/daemon/handlers/browser.ts +1 -6
- package/src/daemon/ipc-contract/browser.ts +5 -14
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/session-agent-loop-handlers.ts +3 -0
- package/src/daemon/session-runtime-assembly.ts +9 -7
- package/src/mcp/client.ts +2 -1
- package/src/memory/conversation-crud.ts +339 -166
- package/src/runtime/auth/middleware.ts +87 -26
- package/src/runtime/routes/events-routes.ts +7 -0
- package/src/runtime/routes/inbound-message-handler.ts +3 -4
- package/src/schedule/scheduler.ts +159 -45
- package/src/security/secure-keys.ts +3 -3
- package/src/tools/browser/browser-manager.ts +72 -228
- package/src/tools/browser/browser-screencast.ts +0 -5
- package/src/tools/network/script-proxy/certs.ts +7 -237
- package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
- package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
- package/src/tools/network/script-proxy/logging.ts +12 -196
- package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
- package/src/tools/network/script-proxy/policy.ts +4 -152
- package/src/tools/network/script-proxy/router.ts +2 -60
- package/src/tools/network/script-proxy/server.ts +5 -137
- package/src/tools/network/script-proxy/types.ts +19 -125
- package/src/tools/system/voice-config.ts +23 -1
- package/src/util/logger.ts +4 -1
- package/src/__tests__/elevenlabs-config.test.ts +0 -95
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
- package/src/calls/elevenlabs-config.ts +0 -32
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
import { and, asc,count, eq, inArray, isNull, sql } from
|
|
2
|
-
import { v4 as uuid } from
|
|
3
|
-
import { z } from
|
|
4
|
-
|
|
5
|
-
import type { ChannelId, InterfaceId } from
|
|
6
|
-
import { parseChannelId, parseInterfaceId } from
|
|
7
|
-
import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from
|
|
8
|
-
import { getConfig } from
|
|
9
|
-
import type { GuardianRuntimeContext } from
|
|
10
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from
|
|
11
|
-
import { getLogger } from
|
|
12
|
-
import { createRowMapper } from
|
|
13
|
-
import { deleteOrphanAttachments } from
|
|
14
|
-
import { projectAssistantMessage } from
|
|
15
|
-
import { getDb, rawExec, rawGet } from
|
|
16
|
-
import { indexMessageNow } from
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
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([
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
58
|
-
|
|
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<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
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 =
|
|
141
|
-
|
|
142
|
-
|
|
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 ===
|
|
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 (
|
|
171
|
-
|
|
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(
|
|
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 ===
|
|
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 ??
|
|
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)
|
|
214
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 (
|
|
270
|
-
|
|
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 = {
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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({
|
|
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 =
|
|
388
|
-
|
|
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(
|
|
502
|
+
rawExec("DELETE FROM memory_segment_fts");
|
|
402
503
|
} catch (err) {
|
|
403
|
-
log.warn(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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(
|
|
410
|
-
rawExec(
|
|
411
|
-
rawExec(
|
|
412
|
-
rawExec(
|
|
413
|
-
rawExec(
|
|
414
|
-
rawExec(
|
|
415
|
-
rawExec(
|
|
416
|
-
rawExec(
|
|
417
|
-
rawExec(
|
|
418
|
-
rawExec(
|
|
419
|
-
rawExec(
|
|
420
|
-
rawExec(
|
|
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(
|
|
527
|
+
rawExec("DELETE FROM messages_fts");
|
|
424
528
|
} catch (err) {
|
|
425
|
-
log.warn(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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(
|
|
432
|
-
rawExec(
|
|
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(
|
|
440
|
-
rawExec(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
rawExec(
|
|
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(
|
|
447
|
-
rawExec(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
rawExec(
|
|
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(
|
|
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
|
|
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
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
616
|
-
|
|
617
|
-
|
|
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(
|
|
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(
|
|
642
|
-
|
|
643
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
826
|
+
export function getConversationOriginChannel(
|
|
827
|
+
conversationId: string,
|
|
828
|
+
): ChannelId | null {
|
|
668
829
|
const db = getDb();
|
|
669
|
-
const row = db
|
|
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(
|
|
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(
|
|
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(
|
|
854
|
+
export function getConversationOriginInterface(
|
|
855
|
+
conversationId: string,
|
|
856
|
+
): InterfaceId | null {
|
|
685
857
|
const db = getDb();
|
|
686
|
-
const row = db
|
|
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
|
-
):
|
|
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
|