@vellumai/assistant 0.3.2 → 0.3.4
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/README.md +82 -21
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
- package/src/__tests__/app-git-history.test.ts +22 -27
- package/src/__tests__/app-git-service.test.ts +44 -78
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +1267 -93
- package/src/__tests__/channel-approval.test.ts +2 -0
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +130 -1
- package/src/__tests__/channel-guardian.test.ts +371 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/daemon-server-session-init.test.ts +5 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
- package/src/__tests__/handlers-telegram-config.test.ts +82 -0
- package/src/__tests__/handlers-twilio-config.test.ts +738 -5
- package/src/__tests__/ingress-url-consistency.test.ts +64 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/run-orchestrator.test.ts +1 -1
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-process-bridge.test.ts +2 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/calls/twilio-config.ts +10 -1
- package/src/calls/twilio-rest.ts +70 -0
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +6 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +193 -17
- package/src/daemon/handlers/sessions.ts +5 -3
- package/src/daemon/handlers/skills.ts +60 -17
- package/src/daemon/ipc-contract-inventory.json +4 -0
- package/src/daemon/ipc-contract.ts +16 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +9 -14
- package/src/daemon/session-process.ts +20 -3
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session-slash.ts +50 -2
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +20 -3
- package/src/index.ts +1 -23
- package/src/memory/app-git-service.ts +24 -0
- package/src/memory/app-store.ts +0 -21
- package/src/memory/channel-delivery-store.ts +74 -3
- package/src/memory/channel-guardian-store.ts +54 -26
- package/src/memory/conversation-key-store.ts +20 -0
- package/src/memory/conversation-store.ts +14 -2
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -1995
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +6 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +28 -9
- package/src/runtime/routes/channel-routes.ts +279 -100
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/clawhub.ts +6 -2
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/subagent/manager.ts +4 -1
- package/src/subagent/types.ts +2 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +75 -127
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -15,7 +15,7 @@ import { eq, and, lte, isNotNull } from 'drizzle-orm';
|
|
|
15
15
|
import { v4 as uuid } from 'uuid';
|
|
16
16
|
import { getDb } from './db.js';
|
|
17
17
|
import { channelInboundEvents, conversations } from './schema.js';
|
|
18
|
-
import { getOrCreateConversation } from './conversation-key-store.js';
|
|
18
|
+
import { getConversationByKey, getOrCreateConversation, setConversationKeyIfAbsent } from './conversation-key-store.js';
|
|
19
19
|
import {
|
|
20
20
|
classifyError,
|
|
21
21
|
retryDelayForAttempt,
|
|
@@ -31,6 +31,7 @@ export interface InboundResult {
|
|
|
31
31
|
|
|
32
32
|
export interface RecordInboundOptions {
|
|
33
33
|
sourceMessageId?: string;
|
|
34
|
+
assistantId?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -69,8 +70,30 @@ export function recordInbound(
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
73
|
+
const assistantId = options?.assistantId;
|
|
74
|
+
const legacyKey = `${sourceChannel}:${externalChatId}`;
|
|
75
|
+
const scopedKey = assistantId ? `asst:${assistantId}:${sourceChannel}:${externalChatId}` : legacyKey;
|
|
76
|
+
|
|
77
|
+
// Resolve conversation mapping with assistant-scoped keying:
|
|
78
|
+
// 1. If scoped key exists, use it directly.
|
|
79
|
+
// 2. If assistantId is "self" and legacy key exists, reuse the legacy
|
|
80
|
+
// conversation and create a scoped alias to prevent future bleed.
|
|
81
|
+
// 3. Otherwise, create/get conversation from the scoped key.
|
|
82
|
+
let mapping: { conversationId: string; created: boolean };
|
|
83
|
+
const scopedMapping = assistantId ? getConversationByKey(scopedKey) : null;
|
|
84
|
+
if (scopedMapping) {
|
|
85
|
+
mapping = { conversationId: scopedMapping.conversationId, created: false };
|
|
86
|
+
} else if (assistantId === 'self') {
|
|
87
|
+
const legacyMapping = getConversationByKey(legacyKey);
|
|
88
|
+
if (legacyMapping) {
|
|
89
|
+
mapping = { conversationId: legacyMapping.conversationId, created: false };
|
|
90
|
+
setConversationKeyIfAbsent(scopedKey, legacyMapping.conversationId);
|
|
91
|
+
} else {
|
|
92
|
+
mapping = getOrCreateConversation(scopedKey);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
mapping = getOrCreateConversation(scopedKey);
|
|
96
|
+
}
|
|
74
97
|
const now = Date.now();
|
|
75
98
|
const eventId = uuid();
|
|
76
99
|
|
|
@@ -316,6 +339,54 @@ export function getDeadLetterEvents(): Array<{
|
|
|
316
339
|
.all();
|
|
317
340
|
}
|
|
318
341
|
|
|
342
|
+
// ── Deliver-once guard for terminal reply idempotency ────────────────
|
|
343
|
+
//
|
|
344
|
+
// When both the main poll (processChannelMessageWithApprovals) and the
|
|
345
|
+
// post-decision poll (schedulePostDecisionDelivery) race to deliver the
|
|
346
|
+
// final assistant reply for the same run, this guard ensures only one
|
|
347
|
+
// of them actually sends the message. The guard is run-scoped so old
|
|
348
|
+
// assistant messages from previous runs are not affected.
|
|
349
|
+
|
|
350
|
+
const deliveredRuns = new Set<string>();
|
|
351
|
+
|
|
352
|
+
/** TTL for delivery claims — 10 minutes, well beyond the poll max-wait. */
|
|
353
|
+
const CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Atomically claim the right to deliver the final reply for a run.
|
|
357
|
+
* Returns `true` if this caller won the claim (and should proceed with
|
|
358
|
+
* delivery). Returns `false` if another caller already claimed it.
|
|
359
|
+
*
|
|
360
|
+
* This is an in-memory guard — sufficient because both racing pollers
|
|
361
|
+
* execute within the same process. The Set is never persisted; on restart
|
|
362
|
+
* there are no in-flight pollers to race.
|
|
363
|
+
*
|
|
364
|
+
* Claims are automatically evicted after CLAIM_TTL_MS to prevent
|
|
365
|
+
* unbounded Set growth over the lifetime of the process.
|
|
366
|
+
*/
|
|
367
|
+
export function claimRunDelivery(runId: string): boolean {
|
|
368
|
+
if (deliveredRuns.has(runId)) return false;
|
|
369
|
+
deliveredRuns.add(runId);
|
|
370
|
+
setTimeout(() => deliveredRuns.delete(runId), CLAIM_TTL_MS);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Reset the deliver-once guard for a run. Used to release a claim when
|
|
376
|
+
* delivery fails (so the other racing poller can retry) and in tests
|
|
377
|
+
* for isolation between test cases.
|
|
378
|
+
*/
|
|
379
|
+
export function resetRunDeliveryClaim(runId: string): void {
|
|
380
|
+
deliveredRuns.delete(runId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Clear all delivery claims. Used in tests for full isolation.
|
|
385
|
+
*/
|
|
386
|
+
export function resetAllRunDeliveryClaims(): void {
|
|
387
|
+
deliveredRuns.clear();
|
|
388
|
+
}
|
|
389
|
+
|
|
319
390
|
/**
|
|
320
391
|
* Reset dead-lettered events back to 'failed' so the sweep can retry
|
|
321
392
|
* them. Resets attempt counter and sets an immediate retry_after.
|
|
@@ -58,6 +58,7 @@ export interface GuardianApprovalRequest {
|
|
|
58
58
|
id: string;
|
|
59
59
|
runId: string;
|
|
60
60
|
conversationId: string;
|
|
61
|
+
assistantId: string;
|
|
61
62
|
channel: string;
|
|
62
63
|
requesterExternalUserId: string;
|
|
63
64
|
requesterChatId: string;
|
|
@@ -114,6 +115,7 @@ function rowToApprovalRequest(row: typeof channelGuardianApprovalRequests.$infer
|
|
|
114
115
|
id: row.id,
|
|
115
116
|
runId: row.runId,
|
|
116
117
|
conversationId: row.conversationId,
|
|
118
|
+
assistantId: row.assistantId,
|
|
117
119
|
channel: row.channel,
|
|
118
120
|
requesterExternalUserId: row.requesterExternalUserId,
|
|
119
121
|
requesterChatId: row.requesterChatId,
|
|
@@ -305,6 +307,7 @@ export function consumeChallenge(
|
|
|
305
307
|
export function createApprovalRequest(params: {
|
|
306
308
|
runId: string;
|
|
307
309
|
conversationId: string;
|
|
310
|
+
assistantId?: string;
|
|
308
311
|
channel: string;
|
|
309
312
|
requesterExternalUserId: string;
|
|
310
313
|
requesterChatId: string;
|
|
@@ -323,6 +326,7 @@ export function createApprovalRequest(params: {
|
|
|
323
326
|
id,
|
|
324
327
|
runId: params.runId,
|
|
325
328
|
conversationId: params.conversationId,
|
|
329
|
+
assistantId: params.assistantId ?? 'self',
|
|
326
330
|
channel: params.channel,
|
|
327
331
|
requesterExternalUserId: params.requesterExternalUserId,
|
|
328
332
|
requesterChatId: params.requesterChatId,
|
|
@@ -388,25 +392,32 @@ export function getUnresolvedApprovalForRun(runId: string): GuardianApprovalRequ
|
|
|
388
392
|
/**
|
|
389
393
|
* Find a pending guardian approval request by the guardian's chat ID.
|
|
390
394
|
* Used when the guardian sends a decision from their chat.
|
|
395
|
+
*
|
|
396
|
+
* When `assistantId` is provided, the lookup is scoped to that assistant,
|
|
397
|
+
* preventing cross-assistant approval consumption in shared guardian chats.
|
|
391
398
|
*/
|
|
392
399
|
export function getPendingApprovalByGuardianChat(
|
|
393
400
|
channel: string,
|
|
394
401
|
guardianChatId: string,
|
|
402
|
+
assistantId?: string,
|
|
395
403
|
): GuardianApprovalRequest | null {
|
|
396
404
|
const db = getDb();
|
|
397
405
|
const now = Date.now();
|
|
398
406
|
|
|
407
|
+
const conditions = [
|
|
408
|
+
eq(channelGuardianApprovalRequests.channel, channel),
|
|
409
|
+
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
410
|
+
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
411
|
+
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
412
|
+
];
|
|
413
|
+
if (assistantId) {
|
|
414
|
+
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
415
|
+
}
|
|
416
|
+
|
|
399
417
|
const row = db
|
|
400
418
|
.select()
|
|
401
419
|
.from(channelGuardianApprovalRequests)
|
|
402
|
-
.where(
|
|
403
|
-
and(
|
|
404
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
405
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
406
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
407
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
408
|
-
),
|
|
409
|
-
)
|
|
420
|
+
.where(and(...conditions))
|
|
410
421
|
.orderBy(desc(channelGuardianApprovalRequests.createdAt))
|
|
411
422
|
.get();
|
|
412
423
|
|
|
@@ -418,27 +429,34 @@ export function getPendingApprovalByGuardianChat(
|
|
|
418
429
|
* guardian chat, and channel. Used when a callback button provides a run ID,
|
|
419
430
|
* so the decision is applied to exactly the right approval even when
|
|
420
431
|
* multiple approvals target the same guardian chat.
|
|
432
|
+
*
|
|
433
|
+
* When `assistantId` is provided, the lookup is further scoped to that
|
|
434
|
+
* assistant to prevent cross-assistant approval consumption.
|
|
421
435
|
*/
|
|
422
436
|
export function getPendingApprovalByRunAndGuardianChat(
|
|
423
437
|
runId: string,
|
|
424
438
|
channel: string,
|
|
425
439
|
guardianChatId: string,
|
|
440
|
+
assistantId?: string,
|
|
426
441
|
): GuardianApprovalRequest | null {
|
|
427
442
|
const db = getDb();
|
|
428
443
|
const now = Date.now();
|
|
429
444
|
|
|
445
|
+
const conditions = [
|
|
446
|
+
eq(channelGuardianApprovalRequests.runId, runId),
|
|
447
|
+
eq(channelGuardianApprovalRequests.channel, channel),
|
|
448
|
+
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
449
|
+
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
450
|
+
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
451
|
+
];
|
|
452
|
+
if (assistantId) {
|
|
453
|
+
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
454
|
+
}
|
|
455
|
+
|
|
430
456
|
const row = db
|
|
431
457
|
.select()
|
|
432
458
|
.from(channelGuardianApprovalRequests)
|
|
433
|
-
.where(
|
|
434
|
-
and(
|
|
435
|
-
eq(channelGuardianApprovalRequests.runId, runId),
|
|
436
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
437
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
438
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
439
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
440
|
-
),
|
|
441
|
-
)
|
|
459
|
+
.where(and(...conditions))
|
|
442
460
|
.get();
|
|
443
461
|
|
|
444
462
|
return row ? rowToApprovalRequest(row) : null;
|
|
@@ -448,25 +466,32 @@ export function getPendingApprovalByRunAndGuardianChat(
|
|
|
448
466
|
* Return all pending (non-expired) guardian approval requests for a given
|
|
449
467
|
* guardian chat and channel. Used to detect ambiguity when a guardian sends
|
|
450
468
|
* a plain-text decision while multiple approvals are pending.
|
|
469
|
+
*
|
|
470
|
+
* When `assistantId` is provided, the results are scoped to that assistant
|
|
471
|
+
* to prevent cross-assistant approval consumption.
|
|
451
472
|
*/
|
|
452
473
|
export function getAllPendingApprovalsByGuardianChat(
|
|
453
474
|
channel: string,
|
|
454
475
|
guardianChatId: string,
|
|
476
|
+
assistantId?: string,
|
|
455
477
|
): GuardianApprovalRequest[] {
|
|
456
478
|
const db = getDb();
|
|
457
479
|
const now = Date.now();
|
|
458
480
|
|
|
481
|
+
const conditions = [
|
|
482
|
+
eq(channelGuardianApprovalRequests.channel, channel),
|
|
483
|
+
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
484
|
+
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
485
|
+
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
486
|
+
];
|
|
487
|
+
if (assistantId) {
|
|
488
|
+
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
489
|
+
}
|
|
490
|
+
|
|
459
491
|
const rows = db
|
|
460
492
|
.select()
|
|
461
493
|
.from(channelGuardianApprovalRequests)
|
|
462
|
-
.where(
|
|
463
|
-
and(
|
|
464
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
465
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
466
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
467
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
468
|
-
),
|
|
469
|
-
)
|
|
494
|
+
.where(and(...conditions))
|
|
470
495
|
.orderBy(desc(channelGuardianApprovalRequests.createdAt))
|
|
471
496
|
.all();
|
|
472
497
|
|
|
@@ -525,7 +550,7 @@ export interface VerificationRateLimit {
|
|
|
525
550
|
actorChatId: string;
|
|
526
551
|
/** Individual attempt timestamps (epoch-ms) within the sliding window. */
|
|
527
552
|
attemptTimestamps: number[];
|
|
528
|
-
/**
|
|
553
|
+
/** Total stored attempt count (may include expired timestamps; use lockedUntil for enforcement decisions). */
|
|
529
554
|
invalidAttempts: number;
|
|
530
555
|
lockedUntil: number | null;
|
|
531
556
|
createdAt: number;
|
|
@@ -645,6 +670,9 @@ export function recordInvalidAttempt(
|
|
|
645
670
|
channel,
|
|
646
671
|
actorExternalUserId,
|
|
647
672
|
actorChatId,
|
|
673
|
+
// Legacy columns kept for backward compatibility with upgraded databases
|
|
674
|
+
invalidAttempts: 0,
|
|
675
|
+
windowStartedAt: 0,
|
|
648
676
|
attemptTimestampsJson: JSON.stringify(timestamps),
|
|
649
677
|
lockedUntil,
|
|
650
678
|
createdAt: now,
|
|
@@ -67,6 +67,26 @@ export function setConversationKey(conversationKey: string, conversationId: stri
|
|
|
67
67
|
.run();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Insert a conversation-key mapping only if the key does not already exist.
|
|
72
|
+
*
|
|
73
|
+
* Uses `onConflictDoNothing` on the unique `conversationKey` column to
|
|
74
|
+
* avoid unique-constraint races when concurrent first messages attempt
|
|
75
|
+
* to migrate a legacy key to a new scoped alias.
|
|
76
|
+
*/
|
|
77
|
+
export function setConversationKeyIfAbsent(conversationKey: string, conversationId: string): void {
|
|
78
|
+
const db = getDb();
|
|
79
|
+
db.insert(conversationKeys)
|
|
80
|
+
.values({
|
|
81
|
+
id: uuid(),
|
|
82
|
+
conversationKey,
|
|
83
|
+
conversationId,
|
|
84
|
+
createdAt: Date.now(),
|
|
85
|
+
})
|
|
86
|
+
.onConflictDoNothing()
|
|
87
|
+
.run();
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
/**
|
|
71
91
|
* Get or create a conversation for the given conversationKey.
|
|
72
92
|
*
|
|
@@ -84,7 +84,7 @@ export function deleteConversation(id: string): void {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export function listConversations(limit?: number, includeBackground = false) {
|
|
87
|
+
export function listConversations(limit?: number, includeBackground = false, offset = 0) {
|
|
88
88
|
const db = getDb();
|
|
89
89
|
const where = includeBackground ? undefined : sql`${conversations.threadType} != 'background'`;
|
|
90
90
|
const query = db
|
|
@@ -92,10 +92,22 @@ export function listConversations(limit?: number, includeBackground = false) {
|
|
|
92
92
|
.from(conversations)
|
|
93
93
|
.where(where)
|
|
94
94
|
.orderBy(desc(conversations.updatedAt))
|
|
95
|
-
.limit(limit ?? 100)
|
|
95
|
+
.limit(limit ?? 100)
|
|
96
|
+
.offset(offset);
|
|
96
97
|
return query.all();
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
export function countConversations(includeBackground = false): number {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
const where = includeBackground ? undefined : sql`${conversations.threadType} != 'background'`;
|
|
103
|
+
const [{ total }] = db
|
|
104
|
+
.select({ total: count() })
|
|
105
|
+
.from(conversations)
|
|
106
|
+
.where(where)
|
|
107
|
+
.all();
|
|
108
|
+
return total;
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
export function getLatestConversation() {
|
|
100
112
|
const db = getDb();
|
|
101
113
|
const result = db
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
3
|
+
import * as schema from './schema.js';
|
|
4
|
+
import { getDbPath, ensureDataDir, migrateToDataLayout, migrateToWorkspaceLayout } from '../util/platform.js';
|
|
5
|
+
|
|
6
|
+
let db: ReturnType<typeof drizzle<typeof schema>> | null = null;
|
|
7
|
+
|
|
8
|
+
export function getDb() {
|
|
9
|
+
if (!db) {
|
|
10
|
+
migrateToDataLayout();
|
|
11
|
+
migrateToWorkspaceLayout();
|
|
12
|
+
ensureDataDir();
|
|
13
|
+
const sqlite = new Database(getDbPath());
|
|
14
|
+
sqlite.exec('PRAGMA journal_mode=WAL');
|
|
15
|
+
sqlite.exec('PRAGMA foreign_keys = ON');
|
|
16
|
+
db = drizzle(sqlite, { schema });
|
|
17
|
+
}
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Reset the db singleton. Used by tests to ensure isolation between test files. */
|
|
22
|
+
export function resetDb(): void {
|
|
23
|
+
if (db) {
|
|
24
|
+
const raw = (db as unknown as { $client: Database }).$client;
|
|
25
|
+
raw.close();
|
|
26
|
+
db = null;
|
|
27
|
+
}
|
|
28
|
+
}
|