@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -1,143 +1,10 @@
1
1
  import { and, desc, eq, inArray, notInArray } from "drizzle-orm";
2
2
 
3
- import { getLogger } from "../../util/logger.js";
4
- import { getDb, rawAll } from "../db.js";
3
+ import { getDb } from "../db.js";
5
4
  import { memorySegments } from "../schema.js";
6
5
  import { computeRecencyScore } from "./ranking.js";
7
6
  import type { Candidate, CandidateType } from "./types.js";
8
7
 
9
- const log = getLogger("memory-retriever");
10
-
11
- // Threshold beyond which a raw SQL query is considered slow and logged as a warning.
12
- const SLOW_QUERY_MS = 2000;
13
-
14
- /**
15
- * Execute a synchronous raw SQL query with timing. Logs a warning if the
16
- * query exceeds SLOW_QUERY_MS. Since bun:sqlite queries are synchronous,
17
- * we can't abort them mid-execution, but we can detect and report slowness.
18
- */
19
- function timedRawQuery<T>(label: string, fn: () => T[]): T[] {
20
- const start = performance.now();
21
- const result = fn();
22
- const elapsed = performance.now() - start;
23
- if (elapsed >= SLOW_QUERY_MS) {
24
- log.warn(
25
- { label, elapsedMs: Math.round(elapsed) },
26
- "Raw SQL query exceeded slow threshold",
27
- );
28
- }
29
- return result;
30
- }
31
-
32
- interface FtsRow {
33
- segment_id: string;
34
- message_id: string;
35
- text: string;
36
- created_at: number;
37
- rank: number;
38
- }
39
-
40
- export function lexicalSearch(
41
- query: string,
42
- limit: number,
43
- excludedMessageIds: string[] = [],
44
- scopeIds?: string[],
45
- expandedQuery?: string,
46
- ): Candidate[] {
47
- const trimmed = query.trim();
48
- if (trimmed.length === 0 || limit <= 0) return [];
49
- // When an expanded query is provided (e.g. from query expansion in degraded
50
- // mode), use it instead of building one from the raw conversational query.
51
- const matchQuery = expandedQuery ?? buildFtsMatchQuery(trimmed);
52
- if (!matchQuery) return [];
53
- const excluded = new Set(excludedMessageIds);
54
- const scopeClause = scopeIds
55
- ? ` AND s.scope_id IN (${scopeIds.map(() => "?").join(",")})`
56
- : "";
57
-
58
- // Adaptive overfetch: when exclusions are present, double the SQL LIMIT until we
59
- // collect `limit` visible rows or the DB has no more matching results to offer.
60
- // This handles dense exclusion sets without wasteful fixed-ratio over-fetching.
61
- const MAX_FETCH_MULTIPLIER = 8;
62
- let queryLimit = excluded.size > 0 ? Math.max(limit * 2, limit + 24) : limit;
63
- let visibleRows: FtsRow[] = [];
64
-
65
- while (true) {
66
- let rows: FtsRow[] = [];
67
- try {
68
- rows = timedRawQuery("lexicalSearch", () =>
69
- rawAll<FtsRow>(
70
- `
71
- SELECT
72
- f.segment_id AS segment_id,
73
- s.message_id AS message_id,
74
- s.text AS text,
75
- s.created_at AS created_at,
76
- bm25(memory_segment_fts) AS rank
77
- FROM memory_segment_fts f
78
- JOIN memory_segments s ON s.id = f.segment_id
79
- WHERE memory_segment_fts MATCH ?${scopeClause}
80
- ORDER BY rank
81
- LIMIT ?
82
- `,
83
- matchQuery,
84
- ...(scopeIds ?? []),
85
- queryLimit,
86
- ),
87
- );
88
- } catch (err) {
89
- log.warn(
90
- { err, query: truncate(trimmed, 80) },
91
- "Memory lexical search query parse failed",
92
- );
93
- return [];
94
- }
95
-
96
- visibleRows =
97
- excluded.size > 0
98
- ? rows.filter((row) => !excluded.has(row.message_id))
99
- : rows;
100
-
101
- // Stop when we have enough rows, the DB returned fewer than requested
102
- // (no more results exist), or we've reached the safety cap.
103
- if (
104
- visibleRows.length >= limit ||
105
- rows.length < queryLimit ||
106
- queryLimit >= limit * MAX_FETCH_MULTIPLIER
107
- ) {
108
- break;
109
- }
110
- queryLimit = Math.min(queryLimit * 2, limit * MAX_FETCH_MULTIPLIER);
111
- }
112
-
113
- visibleRows = visibleRows.slice(0, limit);
114
-
115
- const finiteRanks = visibleRows
116
- .map((row) => row.rank)
117
- .filter((rank) => Number.isFinite(rank));
118
- const minRank = finiteRanks.length > 0 ? Math.min(...finiteRanks) : 0;
119
- const maxRank = finiteRanks.length > 0 ? Math.max(...finiteRanks) : 0;
120
-
121
- return visibleRows.map((row) => {
122
- const lexical = lexicalRankToScore(row.rank, minRank, maxRank);
123
- return {
124
- key: `segment:${row.segment_id}`,
125
- type: "segment" as CandidateType,
126
- id: row.segment_id,
127
- source: "lexical",
128
- text: row.text,
129
- kind: "segment",
130
- confidence: 0.55,
131
- importance: 0.5,
132
- createdAt: row.created_at,
133
- lexical,
134
- semantic: 0,
135
- recency: computeRecencyScore(row.created_at),
136
- finalScore: 0,
137
- };
138
- });
139
- }
140
-
141
8
  export function recencySearch(
142
9
  conversationId: string,
143
10
  limit: number,
@@ -172,128 +39,8 @@ export function recencySearch(
172
39
  confidence: 0.55,
173
40
  importance: 0.5,
174
41
  createdAt: row.createdAt,
175
- lexical: 0,
176
42
  semantic: 0,
177
43
  recency: computeRecencyScore(row.createdAt),
178
44
  finalScore: 0,
179
45
  }));
180
46
  }
181
-
182
- /**
183
- * Direct search over memory_items table by subject and statement text.
184
- * Supplements FTS-based lexical search with LIKE-based matching on items.
185
- */
186
- export function directItemSearch(
187
- query: string,
188
- limit: number,
189
- scopeIds?: string[],
190
- ): Candidate[] {
191
- const tokens = [
192
- ...new Set(
193
- query
194
- .toLowerCase()
195
- .split(/[^a-z0-9_.-]+/g)
196
- .filter((t) => t.length >= 2),
197
- ),
198
- ];
199
- if (tokens.length === 0) return [];
200
-
201
- const likeClauses = tokens.map(
202
- () => `(LOWER(subject) LIKE ? OR LOWER(statement) LIKE ?)`,
203
- );
204
- const likeParams = tokens.flatMap((t) => {
205
- const pattern = `%${escapeLikeWildcards(t)}%`;
206
- return [pattern, pattern];
207
- });
208
- const scopeClause = scopeIds
209
- ? ` AND scope_id IN (${scopeIds.map(() => "?").join(",")})`
210
- : "";
211
- const sqlQuery = `
212
- SELECT id, kind, subject, statement, status, confidence, importance, first_seen_at, last_seen_at
213
- FROM memory_items
214
- WHERE status = 'active' AND invalid_at IS NULL AND (${likeClauses.join(
215
- " OR ",
216
- )})${scopeClause}
217
- ORDER BY last_seen_at DESC
218
- LIMIT ?
219
- `;
220
-
221
- interface ItemRow {
222
- id: string;
223
- kind: string;
224
- subject: string;
225
- statement: string;
226
- confidence: number;
227
- importance: number | null;
228
- first_seen_at: number;
229
- last_seen_at: number;
230
- }
231
-
232
- let rows: ItemRow[] = [];
233
- try {
234
- rows = timedRawQuery("directItemSearch", () =>
235
- rawAll<ItemRow>(sqlQuery, ...likeParams, ...(scopeIds ?? []), limit),
236
- );
237
- } catch {
238
- return [];
239
- }
240
-
241
- return rows.map((row) => {
242
- // Compute lexical score based on token match coverage: fraction of query
243
- // tokens that appear in subject or statement. Direct items are keyword
244
- // matches so this score reflects query-match relevance (unlike confidence,
245
- // which reflects extraction certainty).
246
- const textLower = `${row.subject} ${row.statement}`.toLowerCase();
247
- const matchedTokens = tokens.filter((t) => textLower.includes(t)).length;
248
- const lexical = tokens.length > 0 ? matchedTokens / tokens.length : 0;
249
-
250
- return {
251
- key: `item:${row.id}`,
252
- type: "item" as CandidateType,
253
- id: row.id,
254
- source: "item_direct",
255
- text: `${row.subject}: ${row.statement}`,
256
- kind: row.kind,
257
- confidence: row.confidence,
258
- importance: row.importance ?? 0.5,
259
- createdAt: row.last_seen_at,
260
- lexical,
261
- semantic: 0,
262
- recency: computeRecencyScore(row.last_seen_at),
263
- finalScore: 0,
264
- };
265
- });
266
- }
267
-
268
- export function lexicalRankToScore(
269
- rank: number,
270
- minRank: number,
271
- maxRank: number,
272
- ): number {
273
- if (!Number.isFinite(rank)) return 0;
274
- if (!Number.isFinite(minRank) || !Number.isFinite(maxRank)) return 0;
275
- const span = maxRank - minRank;
276
- if (span <= 0) return 1;
277
- // Lower BM25 rank is better in FTS5; normalize to [0,1] where 1 is best.
278
- return (maxRank - rank) / span;
279
- }
280
-
281
- export function buildFtsMatchQuery(text: string): string | null {
282
- const tokens = text
283
- .toLowerCase()
284
- .split(/[^a-z0-9_]+/g)
285
- .map((token) => token.trim())
286
- .filter((token) => token.length >= 2);
287
- if (tokens.length === 0) return null;
288
- const unique = [...new Set(tokens)].slice(0, 24);
289
- return unique.map((token) => `"${token.replace(/"/g, '""')}"`).join(" OR ");
290
- }
291
-
292
- export function escapeLikeWildcards(s: string): string {
293
- return s.replace(/%/g, "").replace(/_/g, "");
294
- }
295
-
296
- function truncate(text: string, max: number): string {
297
- if (text.length <= max) return text;
298
- return `${text.slice(0, max - 3)}...`;
299
- }