@vellumai/assistant 0.3.15 → 0.3.18
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/ARCHITECTURE.md +211 -12
- package/Dockerfile +1 -1
- package/README.md +11 -5
- package/docs/architecture/http-token-refresh.md +274 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +328 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +19 -15
- package/src/__tests__/checker.test.ts +103 -48
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +356 -0
- package/src/__tests__/conversation-pairing.test.ts +127 -27
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-dispatch.test.ts +120 -0
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +23 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +281 -0
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +138 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +38 -22
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +323 -0
- package/src/__tests__/update-template-contract.test.ts +24 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +241 -39
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/guardian-dispatch.ts +8 -0
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +8 -6
- package/src/cli/core-commands.ts +43 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +15 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +54 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +97 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +4 -2
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +219 -8
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/identity.ts +45 -25
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +6 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +314 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +31 -9
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +546 -59
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +60 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +35 -567
- package/src/memory/channel-guardian-store.ts +63 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +44 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +33 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +136 -0
- package/src/memory/guardian-action-store.ts +418 -5
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +521 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +10 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +4 -0
- package/src/memory/schema.ts +31 -8
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +159 -18
- package/src/notifications/broadcaster.ts +69 -33
- package/src/notifications/conversation-pairing.ts +99 -21
- package/src/notifications/decision-engine.ts +176 -8
- package/src/notifications/deliveries-store.ts +39 -8
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +269 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/checker.ts +1 -16
- package/src/permissions/defaults.ts +25 -5
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +78 -16
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +1 -1
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +271 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/version.ts +29 -2
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { and, asc, count, desc, eq, gte, lt, ne, or, sql } from 'drizzle-orm';
|
|
2
|
+
|
|
3
|
+
import { getLogger } from '../util/logger.js';
|
|
4
|
+
import type { ConversationRow, MessageRow } from './conversation-crud.js';
|
|
5
|
+
import { parseConversation, parseMessage } from './conversation-crud.js';
|
|
6
|
+
import { getDb, rawAll } from './db.js';
|
|
7
|
+
import { conversations, messages } from './schema.js';
|
|
8
|
+
import { buildFtsMatchQuery } from './search/lexical.js';
|
|
9
|
+
|
|
10
|
+
const log = getLogger('conversation-store');
|
|
11
|
+
|
|
12
|
+
export function listConversations(limit?: number, includeBackground = false, offset = 0): ConversationRow[] {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const where = includeBackground ? undefined : sql`${conversations.threadType} != 'background'`;
|
|
15
|
+
const query = db
|
|
16
|
+
.select()
|
|
17
|
+
.from(conversations)
|
|
18
|
+
.where(where)
|
|
19
|
+
.orderBy(desc(conversations.updatedAt))
|
|
20
|
+
.limit(limit ?? 100)
|
|
21
|
+
.offset(offset);
|
|
22
|
+
return query.all().map(parseConversation);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function countConversations(includeBackground = false): number {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
const where = includeBackground ? undefined : sql`${conversations.threadType} != 'background'`;
|
|
28
|
+
const [{ total }] = db
|
|
29
|
+
.select({ total: count() })
|
|
30
|
+
.from(conversations)
|
|
31
|
+
.where(where)
|
|
32
|
+
.all();
|
|
33
|
+
return total;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getLatestConversation(): ConversationRow | null {
|
|
37
|
+
const db = getDb();
|
|
38
|
+
const row = db
|
|
39
|
+
.select()
|
|
40
|
+
.from(conversations)
|
|
41
|
+
.where(sql`${conversations.threadType} != 'background'`)
|
|
42
|
+
.orderBy(desc(conversations.updatedAt))
|
|
43
|
+
.limit(1)
|
|
44
|
+
.get();
|
|
45
|
+
return row ? parseConversation(row) : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the next message in a conversation after a given message.
|
|
50
|
+
* Uses gte + ne(id) instead of gt on timestamp so that messages sharing the
|
|
51
|
+
* same millisecond (common in legacy conversations where an assistant turn and
|
|
52
|
+
* the following user tool_result are saved in the same tick) are not skipped.
|
|
53
|
+
*/
|
|
54
|
+
export function getNextMessage(conversationId: string, afterTimestamp: number, excludeMessageId: string): MessageRow | null {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const row = db
|
|
57
|
+
.select()
|
|
58
|
+
.from(messages)
|
|
59
|
+
.where(and(
|
|
60
|
+
eq(messages.conversationId, conversationId),
|
|
61
|
+
gte(messages.createdAt, afterTimestamp),
|
|
62
|
+
ne(messages.id, excludeMessageId),
|
|
63
|
+
))
|
|
64
|
+
.orderBy(asc(messages.createdAt), asc(messages.id))
|
|
65
|
+
.limit(1)
|
|
66
|
+
.get();
|
|
67
|
+
return row ? parseMessage(row) : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PaginatedMessagesResult {
|
|
71
|
+
messages: MessageRow[];
|
|
72
|
+
/** Whether older messages exist beyond the returned page. */
|
|
73
|
+
hasMore: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Paginated variant of getMessages. Returns the most recent `limit` messages
|
|
78
|
+
* (optionally before a cursor timestamp), in chronological order.
|
|
79
|
+
*
|
|
80
|
+
* When `limit` is undefined, all matching messages are returned (no pagination).
|
|
81
|
+
* When `beforeMessageId` is provided alongside `beforeTimestamp`, it acts as a
|
|
82
|
+
* tie-breaker to avoid skipping messages that share the same millisecond timestamp
|
|
83
|
+
* at page boundaries.
|
|
84
|
+
*/
|
|
85
|
+
export function getMessagesPaginated(
|
|
86
|
+
conversationId: string,
|
|
87
|
+
limit: number | undefined,
|
|
88
|
+
beforeTimestamp?: number,
|
|
89
|
+
beforeMessageId?: string,
|
|
90
|
+
): PaginatedMessagesResult {
|
|
91
|
+
const db = getDb();
|
|
92
|
+
const conditions = [eq(messages.conversationId, conversationId)];
|
|
93
|
+
if (beforeTimestamp !== undefined) {
|
|
94
|
+
if (beforeMessageId) {
|
|
95
|
+
// Proper compound cursor: fetch messages that are strictly older, OR
|
|
96
|
+
// share the same timestamp but have a smaller ID. This avoids both
|
|
97
|
+
// duplicates and skipped messages when multiple rows share a timestamp.
|
|
98
|
+
conditions.push(or(
|
|
99
|
+
lt(messages.createdAt, beforeTimestamp),
|
|
100
|
+
and(eq(messages.createdAt, beforeTimestamp), lt(messages.id, beforeMessageId)),
|
|
101
|
+
)!);
|
|
102
|
+
} else {
|
|
103
|
+
// Legacy callers without a message ID tie-breaker: use strict lt.
|
|
104
|
+
// This may skip same-millisecond messages at boundaries, but avoids
|
|
105
|
+
// re-fetching the boundary message. New callers should prefer the
|
|
106
|
+
// compound cursor (beforeTimestamp + beforeMessageId).
|
|
107
|
+
conditions.push(lt(messages.createdAt, beforeTimestamp));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (limit === undefined) {
|
|
112
|
+
// Unlimited: return all messages in chronological order, no pagination.
|
|
113
|
+
const rows = db
|
|
114
|
+
.select()
|
|
115
|
+
.from(messages)
|
|
116
|
+
.where(and(...conditions))
|
|
117
|
+
.orderBy(asc(messages.createdAt), asc(messages.id))
|
|
118
|
+
.all()
|
|
119
|
+
.map(parseMessage);
|
|
120
|
+
return { messages: rows, hasMore: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fetch limit+1 rows ordered newest-first so we can detect hasMore
|
|
124
|
+
const rows = db
|
|
125
|
+
.select()
|
|
126
|
+
.from(messages)
|
|
127
|
+
.where(and(...conditions))
|
|
128
|
+
.orderBy(desc(messages.createdAt), desc(messages.id))
|
|
129
|
+
.limit(limit + 1)
|
|
130
|
+
.all()
|
|
131
|
+
.map(parseMessage);
|
|
132
|
+
|
|
133
|
+
const hasMore = rows.length > limit;
|
|
134
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
135
|
+
|
|
136
|
+
// Return in chronological order (oldest first) for the client
|
|
137
|
+
page.reverse();
|
|
138
|
+
|
|
139
|
+
return { messages: page, hasMore };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check whether the last user message in a conversation is a tool_result-only
|
|
144
|
+
* message (i.e., not a real user-typed message). This is used by undo() to
|
|
145
|
+
* determine if additional exchanges need to be deleted from the DB.
|
|
146
|
+
*/
|
|
147
|
+
export function isLastUserMessageToolResult(conversationId: string): boolean {
|
|
148
|
+
const db = getDb();
|
|
149
|
+
const lastUserMsg = db
|
|
150
|
+
.select({ content: messages.content })
|
|
151
|
+
.from(messages)
|
|
152
|
+
.where(and(eq(messages.conversationId, conversationId), eq(messages.role, 'user')))
|
|
153
|
+
.orderBy(sql`rowid DESC`)
|
|
154
|
+
.limit(1)
|
|
155
|
+
.get();
|
|
156
|
+
|
|
157
|
+
if (!lastUserMsg) return false;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(lastUserMsg.content);
|
|
161
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((block: Record<string, unknown>) => block.type === 'tool_result')) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Not JSON — it's a plain text user message
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ConversationSearchResult {
|
|
171
|
+
conversationId: string;
|
|
172
|
+
conversationTitle: string | null;
|
|
173
|
+
conversationUpdatedAt: number;
|
|
174
|
+
matchingMessages: Array<{
|
|
175
|
+
messageId: string;
|
|
176
|
+
role: string;
|
|
177
|
+
/** Plain-text excerpt around the match, truncated to ~200 chars. */
|
|
178
|
+
excerpt: string;
|
|
179
|
+
createdAt: number;
|
|
180
|
+
}>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Full-text search across message content using FTS5.
|
|
185
|
+
* Uses the messages_fts virtual table for fast tokenized matching on message
|
|
186
|
+
* content, with a LIKE fallback on conversation titles. Returns matching
|
|
187
|
+
* conversations with their relevant messages, ordered by most recently updated.
|
|
188
|
+
*/
|
|
189
|
+
export function searchConversations(
|
|
190
|
+
query: string,
|
|
191
|
+
opts?: { limit?: number; maxMessagesPerConversation?: number },
|
|
192
|
+
): ConversationSearchResult[] {
|
|
193
|
+
if (!query.trim()) return [];
|
|
194
|
+
|
|
195
|
+
const db = getDb();
|
|
196
|
+
const limit = opts?.limit ?? 20;
|
|
197
|
+
const maxMsgsPerConv = opts?.maxMessagesPerConversation ?? 3;
|
|
198
|
+
|
|
199
|
+
const ftsMatch = buildFtsMatchQuery(query.trim());
|
|
200
|
+
|
|
201
|
+
// LIKE pattern for title matching (FTS only covers message content).
|
|
202
|
+
const titlePattern = `%${query.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')}%`;
|
|
203
|
+
|
|
204
|
+
interface ConvIdRow {
|
|
205
|
+
conversation_id: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Collect conversation IDs from FTS message matches and title LIKE matches,
|
|
209
|
+
// then merge them to produce the final set of matching conversations.
|
|
210
|
+
// Both paths LIMIT on distinct conversation_id to prevent a single
|
|
211
|
+
// conversation with many matching messages from crowding out others.
|
|
212
|
+
const ftsConvIds = new Set<string>();
|
|
213
|
+
if (ftsMatch) {
|
|
214
|
+
try {
|
|
215
|
+
const ftsRows = rawAll<ConvIdRow>(`
|
|
216
|
+
SELECT DISTINCT m.conversation_id
|
|
217
|
+
FROM messages_fts f
|
|
218
|
+
JOIN messages m ON m.id = f.message_id
|
|
219
|
+
JOIN conversations c ON c.id = m.conversation_id
|
|
220
|
+
WHERE messages_fts MATCH ? AND c.thread_type != 'background'
|
|
221
|
+
LIMIT 1000
|
|
222
|
+
`, ftsMatch);
|
|
223
|
+
for (const row of ftsRows) ftsConvIds.add(row.conversation_id);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
log.warn({ err, query: query.slice(0, 80) }, 'searchConversations: FTS query failed — falling through to title matches');
|
|
226
|
+
}
|
|
227
|
+
} else if (query.trim()) {
|
|
228
|
+
// FTS tokens were all dropped (non-ASCII, single-char, etc.) — fall back to
|
|
229
|
+
// LIKE-based message content search so queries like "你", "é", or "C++" still
|
|
230
|
+
// match message text.
|
|
231
|
+
const likePattern = `%${query.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')}%`;
|
|
232
|
+
const likeRows = rawAll<ConvIdRow>(`
|
|
233
|
+
SELECT DISTINCT m.conversation_id
|
|
234
|
+
FROM messages m
|
|
235
|
+
JOIN conversations c ON c.id = m.conversation_id
|
|
236
|
+
WHERE m.content LIKE ? ESCAPE '\\' AND c.thread_type != 'background'
|
|
237
|
+
LIMIT 1000
|
|
238
|
+
`, likePattern);
|
|
239
|
+
for (const row of likeRows) ftsConvIds.add(row.conversation_id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Title-only matches (FTS doesn't index conversation titles).
|
|
243
|
+
const titleMatchConvs = db
|
|
244
|
+
.select({ id: conversations.id })
|
|
245
|
+
.from(conversations)
|
|
246
|
+
.where(
|
|
247
|
+
and(
|
|
248
|
+
sql`${conversations.threadType} != 'background'`,
|
|
249
|
+
sql`${conversations.title} LIKE ${titlePattern} ESCAPE '\\'`,
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
.all();
|
|
253
|
+
for (const row of titleMatchConvs) ftsConvIds.add(row.id);
|
|
254
|
+
|
|
255
|
+
if (ftsConvIds.size === 0) return [];
|
|
256
|
+
|
|
257
|
+
// Fetch the matching conversation rows, ordered by updatedAt, capped at limit.
|
|
258
|
+
const convIds = [...ftsConvIds];
|
|
259
|
+
const placeholders = convIds.map(() => '?').join(',');
|
|
260
|
+
interface ConvRow { id: string; title: string | null; updated_at: number }
|
|
261
|
+
const matchingConversations = rawAll<ConvRow>(
|
|
262
|
+
`SELECT id, title, updated_at FROM conversations
|
|
263
|
+
WHERE id IN (${placeholders})
|
|
264
|
+
ORDER BY updated_at DESC
|
|
265
|
+
LIMIT ?`,
|
|
266
|
+
...convIds, limit,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (matchingConversations.length === 0) return [];
|
|
270
|
+
|
|
271
|
+
const results: ConversationSearchResult[] = [];
|
|
272
|
+
|
|
273
|
+
for (const conv of matchingConversations) {
|
|
274
|
+
interface MsgRow { id: string; role: string; content: string; created_at: number }
|
|
275
|
+
let matchingMsgs: MsgRow[] = [];
|
|
276
|
+
if (ftsMatch) {
|
|
277
|
+
try {
|
|
278
|
+
matchingMsgs = rawAll<MsgRow>(`
|
|
279
|
+
SELECT m.id, m.role, m.content, m.created_at
|
|
280
|
+
FROM messages_fts f
|
|
281
|
+
JOIN messages m ON m.id = f.message_id
|
|
282
|
+
WHERE messages_fts MATCH ? AND m.conversation_id = ?
|
|
283
|
+
ORDER BY m.created_at ASC
|
|
284
|
+
LIMIT ?
|
|
285
|
+
`, ftsMatch, conv.id, maxMsgsPerConv);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
log.warn({ err, conversationId: conv.id }, 'searchConversations: FTS per-conversation query failed');
|
|
288
|
+
}
|
|
289
|
+
} else if (query.trim()) {
|
|
290
|
+
// LIKE fallback for non-ASCII / short-token queries.
|
|
291
|
+
const msgLikePattern = `%${query.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')}%`;
|
|
292
|
+
matchingMsgs = rawAll<MsgRow>(`
|
|
293
|
+
SELECT id, role, content, created_at
|
|
294
|
+
FROM messages
|
|
295
|
+
WHERE conversation_id = ? AND content LIKE ? ESCAPE '\\'
|
|
296
|
+
ORDER BY created_at ASC
|
|
297
|
+
LIMIT ?
|
|
298
|
+
`, conv.id, msgLikePattern, maxMsgsPerConv);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
results.push({
|
|
302
|
+
conversationId: conv.id,
|
|
303
|
+
conversationTitle: conv.title,
|
|
304
|
+
conversationUpdatedAt: conv.updated_at,
|
|
305
|
+
matchingMessages: matchingMsgs.map((m) => ({
|
|
306
|
+
messageId: m.id,
|
|
307
|
+
role: m.role,
|
|
308
|
+
excerpt: buildExcerpt(m.content, query),
|
|
309
|
+
createdAt: m.created_at,
|
|
310
|
+
})),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build a short excerpt from raw message content centered around the first
|
|
319
|
+
* occurrence of `query`. The content may be JSON (content blocks) or plain
|
|
320
|
+
* text; we extract a readable snippet in either case.
|
|
321
|
+
*/
|
|
322
|
+
function buildExcerpt(rawContent: string, query: string): string {
|
|
323
|
+
// Try to extract plain text from JSON content blocks first.
|
|
324
|
+
let text = rawContent;
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(rawContent);
|
|
327
|
+
if (Array.isArray(parsed)) {
|
|
328
|
+
const parts: string[] = [];
|
|
329
|
+
for (const block of parsed) {
|
|
330
|
+
if (typeof block === 'object' && block != null) {
|
|
331
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
332
|
+
parts.push(block.text);
|
|
333
|
+
} else if (block.type === 'tool_result') {
|
|
334
|
+
const inner = Array.isArray(block.content) ? block.content : [];
|
|
335
|
+
for (const ib of inner) {
|
|
336
|
+
if (ib?.type === 'text' && typeof ib.text === 'string') parts.push(ib.text);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (parts.length > 0) text = parts.join(' ');
|
|
342
|
+
} else if (typeof parsed === 'string') {
|
|
343
|
+
text = parsed;
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
// Not JSON — use as-is
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const WINDOW = 100;
|
|
350
|
+
const lowerText = text.toLowerCase();
|
|
351
|
+
const lowerQuery = query.toLowerCase();
|
|
352
|
+
const idx = lowerText.indexOf(lowerQuery);
|
|
353
|
+
if (idx === -1) {
|
|
354
|
+
// Query matched the raw JSON but not the extracted text — fall back to raw start
|
|
355
|
+
return text.slice(0, WINDOW * 2).replace(/\s+/g, ' ').trim();
|
|
356
|
+
}
|
|
357
|
+
const start = Math.max(0, idx - WINDOW);
|
|
358
|
+
const end = Math.min(text.length, idx + query.length + WINDOW);
|
|
359
|
+
const excerpt = (start > 0 ? '\u2026' : '') + text.slice(start, end).replace(/\s+/g, ' ').trim() + (end < text.length ? '\u2026' : '');
|
|
360
|
+
return excerpt;
|
|
361
|
+
}
|