@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -19,7 +19,6 @@ import {
19
19
  conversations,
20
20
  llmRequestLogs,
21
21
  memoryEmbeddings,
22
- memoryItemEntities,
23
22
  memoryItems,
24
23
  memoryItemSources,
25
24
  memorySegments,
@@ -503,19 +502,6 @@ export function clearAll(): { conversations: number; messages: number } {
503
502
  // triggers so that the subsequent base-table DELETEs don't also fail
504
503
  // (SQLite triggers are atomic with the triggering statement, so a
505
504
  // corrupted FTS table would roll back every base-table DELETE).
506
- let segmentFtsCorrupted = false;
507
- try {
508
- rawExec("DELETE FROM memory_segment_fts");
509
- } catch (err) {
510
- log.warn(
511
- { err },
512
- "clearAll: failed to clear memory_segment_fts — dropping triggers so base-table cleanup can proceed",
513
- );
514
- rawExec("DROP TRIGGER IF EXISTS memory_segments_ai");
515
- rawExec("DROP TRIGGER IF EXISTS memory_segments_ad");
516
- rawExec("DROP TRIGGER IF EXISTS memory_segments_au");
517
- segmentFtsCorrupted = true;
518
- }
519
505
  rawExec("DELETE FROM memory_item_sources");
520
506
  rawExec("DELETE FROM memory_segments");
521
507
  rawExec("DELETE FROM memory_items");
@@ -548,21 +534,6 @@ export function clearAll(): { conversations: number; messages: number } {
548
534
  // DELETEs have completed. Dropping the virtual table clears the corruption,
549
535
  // and recreating it + triggers means subsequent writes maintain FTS
550
536
  // consistency without requiring a daemon restart.
551
- if (segmentFtsCorrupted) {
552
- rawExec("DROP TABLE IF EXISTS memory_segment_fts");
553
- rawExec(
554
- `CREATE VIRTUAL TABLE IF NOT EXISTS memory_segment_fts USING fts5(segment_id UNINDEXED, text)`,
555
- );
556
- rawExec(
557
- `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`,
558
- );
559
- rawExec(
560
- `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`,
561
- );
562
- rawExec(
563
- `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`,
564
- );
565
- }
566
537
  if (messagesFtsCorrupted) {
567
538
  rawExec("DROP TABLE IF EXISTS messages_fts");
568
539
  rawExec(
@@ -787,10 +758,6 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
787
758
  result.orphanedItemIds = orphanedIds;
788
759
 
789
760
  if (orphanedIds.length > 0) {
790
- // Delete memory_item_entities (no FK cascade on this table).
791
- tx.delete(memoryItemEntities)
792
- .where(inArray(memoryItemEntities.memoryItemId, orphanedIds))
793
- .run();
794
761
  // Delete embeddings referencing these items.
795
762
  tx.delete(memoryEmbeddings)
796
763
  .where(
@@ -1,15 +1,29 @@
1
- import { and, asc, count, desc, eq, lt, or, sql } from "drizzle-orm";
1
+ import { and, count, desc, eq, sql } from "drizzle-orm";
2
2
 
3
3
  import { getLogger } from "../util/logger.js";
4
- import type { ConversationRow, MessageRow } from "./conversation-crud.js";
5
- import { parseConversation, parseMessage } from "./conversation-crud.js";
4
+ import type { ConversationRow } from "./conversation-crud.js";
5
+ import { parseConversation } from "./conversation-crud.js";
6
6
  import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
7
7
  import { getDb, rawAll } from "./db.js";
8
8
  import { conversations, messages } from "./schema.js";
9
- import { buildFtsMatchQuery } from "./search/lexical.js";
10
9
 
11
10
  const log = getLogger("conversation-store");
12
11
 
12
+ /**
13
+ * Build an FTS5 MATCH query string from natural text by extracting tokens.
14
+ * Used for messages_fts full-text search over conversation content.
15
+ */
16
+ function buildFtsMatchQuery(text: string): string | null {
17
+ const tokens = text
18
+ .toLowerCase()
19
+ .split(/[^a-z0-9_]+/g)
20
+ .map((token) => token.trim())
21
+ .filter((token) => token.length >= 2);
22
+ if (tokens.length === 0) return null;
23
+ const unique = [...new Set(tokens)].slice(0, 24);
24
+ return unique.map((token) => `"${token.replace(/"/g, '""')}"`).join(" OR ");
25
+ }
26
+
13
27
  export function listConversations(
14
28
  limit?: number,
15
29
  includeBackground = false,
@@ -55,83 +69,6 @@ export function getLatestConversation(): ConversationRow | null {
55
69
  return row ? parseConversation(row) : null;
56
70
  }
57
71
 
58
- export interface PaginatedMessagesResult {
59
- messages: MessageRow[];
60
- /** Whether older messages exist beyond the returned page. */
61
- hasMore: boolean;
62
- }
63
-
64
- /**
65
- * Paginated variant of getMessages. Returns the most recent `limit` messages
66
- * (optionally before a cursor timestamp), in chronological order.
67
- *
68
- * When `limit` is undefined, all matching messages are returned (no pagination).
69
- * When `beforeMessageId` is provided alongside `beforeTimestamp`, it acts as a
70
- * tie-breaker to avoid skipping messages that share the same millisecond timestamp
71
- * at page boundaries.
72
- */
73
- export function getMessagesPaginated(
74
- conversationId: string,
75
- limit: number | undefined,
76
- beforeTimestamp?: number,
77
- beforeMessageId?: string,
78
- ): PaginatedMessagesResult {
79
- const db = getDb();
80
- const conditions = [eq(messages.conversationId, conversationId)];
81
- if (beforeTimestamp !== undefined) {
82
- if (beforeMessageId) {
83
- // Proper compound cursor: fetch messages that are strictly older, OR
84
- // share the same timestamp but have a smaller ID. This avoids both
85
- // duplicates and skipped messages when multiple rows share a timestamp.
86
- conditions.push(
87
- or(
88
- lt(messages.createdAt, beforeTimestamp),
89
- and(
90
- eq(messages.createdAt, beforeTimestamp),
91
- lt(messages.id, beforeMessageId),
92
- ),
93
- )!,
94
- );
95
- } else {
96
- // Legacy callers without a message ID tie-breaker: use strict lt.
97
- // This may skip same-millisecond messages at boundaries, but avoids
98
- // re-fetching the boundary message. New callers should prefer the
99
- // compound cursor (beforeTimestamp + beforeMessageId).
100
- conditions.push(lt(messages.createdAt, beforeTimestamp));
101
- }
102
- }
103
-
104
- if (limit === undefined) {
105
- // Unlimited: return all messages in chronological order, no pagination.
106
- const rows = db
107
- .select()
108
- .from(messages)
109
- .where(and(...conditions))
110
- .orderBy(asc(messages.createdAt), asc(messages.id))
111
- .all()
112
- .map(parseMessage);
113
- return { messages: rows, hasMore: false };
114
- }
115
-
116
- // Fetch limit+1 rows ordered newest-first so we can detect hasMore
117
- const rows = db
118
- .select()
119
- .from(messages)
120
- .where(and(...conditions))
121
- .orderBy(desc(messages.createdAt), desc(messages.id))
122
- .limit(limit + 1)
123
- .all()
124
- .map(parseMessage);
125
-
126
- const hasMore = rows.length > limit;
127
- const page = hasMore ? rows.slice(0, limit) : rows;
128
-
129
- // Return in chronological order (oldest first) for the client
130
- page.reverse();
131
-
132
- return { messages: page, hasMore };
133
- }
134
-
135
72
  /**
136
73
  * Check whether the last user message in a conversation is a tool_result-only
137
74
  * message (i.e., not a real user-typed message). This is used by undo() to
@@ -160,7 +97,9 @@ export function isLastUserMessageToolResult(conversationId: string): boolean {
160
97
  Array.isArray(parsed) &&
161
98
  parsed.length > 0 &&
162
99
  parsed.every(
163
- (block: Record<string, unknown>) => block.type === "tool_result",
100
+ (block: Record<string, unknown>) =>
101
+ block.type === "tool_result" ||
102
+ block.type === "web_search_tool_result",
164
103
  )
165
104
  ) {
166
105
  return true;
@@ -375,7 +314,10 @@ function buildExcerpt(rawContent: string, query: string): string {
375
314
  if (typeof block === "object" && block != null) {
376
315
  if (block.type === "text" && typeof block.text === "string") {
377
316
  parts.push(block.text);
378
- } else if (block.type === "tool_result") {
317
+ } else if (
318
+ block.type === "tool_result" ||
319
+ block.type === "web_search_tool_result"
320
+ ) {
379
321
  const inner = Array.isArray(block.content) ? block.content : [];
380
322
  for (const ib of inner) {
381
323
  if (ib?.type === "text" && typeof ib.text === "string")
@@ -37,6 +37,7 @@ import {
37
37
  migrateBackfillContactInteractionStats,
38
38
  migrateBackfillGuardianPrincipalId,
39
39
  migrateBackfillUsageCacheAccounting,
40
+ migrateCallSessionInviteMetadata,
40
41
  migrateCallSessionMode,
41
42
  migrateCanonicalGuardianDeliveriesDestinationIndex,
42
43
  migrateCanonicalGuardianRequesterChatId,
@@ -49,7 +50,10 @@ import {
49
50
  migrateConversationsThreadTypeIndex,
50
51
  migrateDropAccountsTable,
51
52
  migrateDropAssistantIdColumns,
53
+ migrateDropConflicts,
54
+ migrateDropEntityTables,
52
55
  migrateDropLegacyMemberGuardianTables,
56
+ migrateDropMemorySegmentFts,
53
57
  migrateDropRemindersTable,
54
58
  migrateDropUsageCompositeIndexes,
55
59
  migrateFkCascadeRebuilds,
@@ -63,9 +67,13 @@ import {
63
67
  migrateGuardianVerificationPurpose,
64
68
  migrateGuardianVerificationSessions,
65
69
  migrateInviteCodeHashColumn,
70
+ migrateInviteContactId,
71
+ migrateMemoryItemSupersession,
66
72
  migrateMessagesFtsBackfill,
67
73
  migrateNormalizePhoneIdentities,
68
74
  migrateNotificationDeliveryThreadDecision,
75
+ migrateOAuthAppsClientSecretPath,
76
+ migrateOAuthProvidersPingUrl,
69
77
  migrateReminderRoutingIntent,
70
78
  migrateRemindersToSchedules,
71
79
  migrateRenameGuardianVerificationValues,
@@ -344,6 +352,30 @@ export function initializeDb(): void {
344
352
  // 53. OAuth provider/app/connection tables
345
353
  createOAuthTables(database);
346
354
 
355
+ // 54. Add explicit client_secret_credential_path to oauth_apps
356
+ migrateOAuthAppsClientSecretPath(database);
357
+
358
+ // 55. Add ping_url column to oauth_providers
359
+ migrateOAuthProvidersPingUrl(database);
360
+
361
+ // 56. Add supersession tracking columns and override confidence to memory_items
362
+ migrateMemoryItemSupersession(database);
363
+
364
+ // 56b. Drop unused entity tables (entity search replaced by hybrid search on item statements)
365
+ migrateDropEntityTables(database);
366
+
367
+ // 57. Drop memory_segment_fts virtual table and triggers (replaced by Qdrant hybrid search)
368
+ migrateDropMemorySegmentFts(database);
369
+
370
+ // 58. Drop memory_item_conflicts table (conflict resolution system removed)
371
+ migrateDropConflicts(database);
372
+
373
+ // 59. Add invite metadata columns to call_sessions for outbound invite call routing
374
+ migrateCallSessionInviteMetadata(database);
375
+
376
+ // 60. Add required contact_id to assistant_ingress_invites and clean up legacy rows
377
+ migrateInviteContactId(database);
378
+
347
379
  validateMigrationState(database);
348
380
 
349
381
  if (process.env.BUN_TEST === "1") {
@@ -11,14 +11,11 @@ import {
11
11
  embeddingInputContentHash,
12
12
  type MultimodalEmbeddingInput,
13
13
  normalizeEmbeddingInput,
14
+ type SparseEmbedding,
14
15
  type TextEmbeddingInput,
15
16
  } from "./embedding-types.js";
16
17
 
17
- export type {
18
- EmbeddingInput,
19
- MultimodalEmbeddingInput,
20
- TextEmbeddingInput,
21
- };
18
+ export type { EmbeddingInput, MultimodalEmbeddingInput, TextEmbeddingInput };
22
19
  export { embeddingInputContentHash, normalizeEmbeddingInput };
23
20
 
24
21
  const log = getLogger("memory-embeddings");
@@ -412,7 +409,12 @@ export async function embedWithBackend(
412
409
 
413
410
  // ── In-memory cache check (primary provider only) ──────────────
414
411
  const cached: (number[] | null)[] = inputs.map((input) => {
415
- const v = getFromVectorCache(primaryProvider, primaryModel, input, vectorExtras);
412
+ const v = getFromVectorCache(
413
+ primaryProvider,
414
+ primaryModel,
415
+ input,
416
+ vectorExtras,
417
+ );
416
418
  if (v && v.length === expectedDim) return v;
417
419
  return null;
418
420
  });
@@ -443,7 +445,8 @@ export async function embedWithBackend(
443
445
 
444
446
  // Skip text-only backends for multimodal inputs
445
447
  const hasNonText = inputsToEmbed.some(
446
- (i) => typeof i !== "string" && normalizeEmbeddingInput(i).type !== "text",
448
+ (i) =>
449
+ typeof i !== "string" && normalizeEmbeddingInput(i).type !== "text",
447
450
  );
448
451
  if (backend.provider !== "gemini" && hasNonText) {
449
452
  continue;
@@ -502,7 +505,8 @@ export async function embedWithBackend(
502
505
  }
503
506
  if (!anyBackendAttempted) {
504
507
  const hasMultimodal = inputs.some(
505
- (i) => typeof i !== "string" && normalizeEmbeddingInput(i).type !== "text",
508
+ (i) =>
509
+ typeof i !== "string" && normalizeEmbeddingInput(i).type !== "text",
506
510
  );
507
511
  if (hasMultimodal) {
508
512
  throw new Error(
@@ -614,3 +618,75 @@ function isOllamaConfigured(config: AssistantConfig): boolean {
614
618
  Boolean(getOllamaBaseUrlEnv())
615
619
  );
616
620
  }
621
+
622
+ // ── TF-IDF sparse embedding ───────────────────────────────────────
623
+ // Simple tokenizer + TF-IDF sparse encoder. Produces a SparseEmbedding
624
+ // with term indices (hashed to a fixed vocabulary) and TF-IDF weights.
625
+ // Can be upgraded to a learned sparse encoder (e.g. SPLADE) later.
626
+
627
+ const SPARSE_VOCAB_SIZE = 30_000;
628
+
629
+ /**
630
+ * Bump this version whenever the sparse embedding algorithm changes
631
+ * (e.g. hash function fix, tokenizer change) to trigger re-indexing
632
+ * of existing sparse vectors via the sentinel mismatch mechanism.
633
+ */
634
+ export const SPARSE_EMBEDDING_VERSION = 2;
635
+
636
+ /** Tokenize text into lowercase alphanumeric tokens (Unicode-aware). */
637
+ function tokenize(text: string): string[] {
638
+ return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
639
+ }
640
+
641
+ /** Hash a token to a stable index in [0, vocabSize). */
642
+ function tokenHash(token: string, vocabSize: number): number {
643
+ // FNV-1a 32-bit hash for speed
644
+ let hash = 0x811c9dc5;
645
+ for (let i = 0; i < token.length; i++) {
646
+ hash ^= token.charCodeAt(i);
647
+ hash = Math.imul(hash, 0x01000193) >>> 0;
648
+ }
649
+ return hash % vocabSize;
650
+ }
651
+
652
+ /**
653
+ * Generate a TF-IDF-based sparse embedding for the given text.
654
+ *
655
+ * Term frequency is computed from the input. IDF is approximated using
656
+ * sub-linear TF weighting (1 + log(tf)) since we don't have a corpus-level
657
+ * document frequency table. This still produces useful sparse vectors for
658
+ * lexical matching via Qdrant's sparse vector support.
659
+ */
660
+ export function generateSparseEmbedding(text: string): SparseEmbedding {
661
+ const tokens = tokenize(text);
662
+ if (tokens.length === 0) {
663
+ return { indices: [], values: [] };
664
+ }
665
+
666
+ // Count term frequencies per hash bucket
667
+ const tf = new Map<number, number>();
668
+ for (const token of tokens) {
669
+ const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
670
+ tf.set(idx, (tf.get(idx) ?? 0) + 1);
671
+ }
672
+
673
+ // Convert to sub-linear TF weights: 1 + log(tf)
674
+ const indices: number[] = [];
675
+ const values: number[] = [];
676
+ for (const [idx, count] of tf) {
677
+ indices.push(idx);
678
+ values.push(1 + Math.log(count));
679
+ }
680
+
681
+ // L2-normalize the sparse vector so scores are comparable
682
+ let norm = 0;
683
+ for (const v of values) norm += v * v;
684
+ norm = Math.sqrt(norm);
685
+ if (norm > 0) {
686
+ for (let i = 0; i < values.length; i++) {
687
+ values[i] /= norm;
688
+ }
689
+ }
690
+
691
+ return { indices, values };
692
+ }
@@ -42,11 +42,19 @@ export type MultimodalEmbeddingInput =
42
42
  /** Accepts raw strings as shorthand for text inputs. */
43
43
  export type EmbeddingInput = string | MultimodalEmbeddingInput;
44
44
 
45
- export function normalizeEmbeddingInput(input: EmbeddingInput): MultimodalEmbeddingInput {
45
+ export function normalizeEmbeddingInput(
46
+ input: EmbeddingInput,
47
+ ): MultimodalEmbeddingInput {
46
48
  if (typeof input === "string") return { type: "text", text: input };
47
49
  return input;
48
50
  }
49
51
 
52
+ /** Sparse vector representation: parallel arrays of term indices and weights. */
53
+ export interface SparseEmbedding {
54
+ indices: number[];
55
+ values: number[];
56
+ }
57
+
50
58
  export function embeddingInputContentHash(input: EmbeddingInput): string {
51
59
  const normalized = normalizeEmbeddingInput(input);
52
60
  const hash = createHash("sha256");
@@ -5,24 +5,17 @@ import { getConfig } from "../config/loader.js";
5
5
  import type { MemoryConfig } from "../config/types.js";
6
6
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
7
7
  import { getLogger } from "../util/logger.js";
8
- import { getMemoryCheckpoint, setMemoryCheckpoint } from "./checkpoints.js";
9
8
  import { getDb } from "./db.js";
10
9
  import { selectedBackendSupportsMultimodal } from "./embedding-backend.js";
11
- import {
12
- enqueueMemoryJob,
13
- enqueueResolvePendingConflictsForMessageJob,
14
- } from "./jobs-store.js";
10
+ import { enqueueMemoryJob } from "./jobs-store.js";
15
11
  import {
16
12
  extractMediaBlocks,
17
13
  extractTextFromStoredMessageContent,
18
14
  } from "./message-content.js";
19
- import { bumpMemoryVersion } from "./recall-cache.js";
20
15
  import { memorySegments } from "./schema.js";
21
16
  import { segmentText } from "./segmenter.js";
22
17
 
23
18
  const log = getLogger("memory-indexer");
24
- const SUMMARY_JOB_CHECKPOINT_KEY = "memory:summary_jobs:last_scheduled_at";
25
- const SUMMARY_SCHEDULE_INTERVAL_MS = 6 * 60 * 60 * 1000;
26
19
 
27
20
  export interface IndexMessageInput {
28
21
  messageId: string;
@@ -34,8 +27,8 @@ export interface IndexMessageInput {
34
27
  /**
35
28
  * Trust class of the actor who produced this message, captured at
36
29
  * persist time. When `'guardian'` or `undefined` (legacy), extraction
37
- * and conflict resolution jobs run. Otherwise, the message is segmented
38
- * and embedded but no profile mutations are triggered.
30
+ * jobs run. Otherwise, the message is segmented and embedded but no
31
+ * profile mutations are triggered.
39
32
  */
40
33
  provenanceTrustClass?: TrustClass;
41
34
  }
@@ -52,7 +45,7 @@ export function indexMessageNow(
52
45
  if (!config.enabled) return { indexedSegments: 0, enqueuedJobs: 0 };
53
46
 
54
47
  // Provenance-based trust gating: only guardian and legacy (undefined) actors
55
- // are trusted for extraction and conflict resolution.
48
+ // are trusted for extraction.
56
49
  const isTrustedActor =
57
50
  input.provenanceTrustClass === "guardian" ||
58
51
  input.provenanceTrustClass === undefined;
@@ -75,9 +68,6 @@ export function indexMessageNow(
75
68
  const shouldExtract =
76
69
  input.role === "user" ||
77
70
  (input.role === "assistant" && config.extraction.extractFromAssistant);
78
- const shouldResolveConflicts =
79
- input.role === "user" && config.conflicts.enabled;
80
-
81
71
  // Check if the resolved embedding backend supports multimodal input.
82
72
  // Only enqueue embed_attachment jobs when it does (currently Gemini only).
83
73
  const supportsMultimodal = selectedBackendSupportsMultimodal(getConfig());
@@ -152,13 +142,6 @@ export function indexMessageNow(
152
142
  tx,
153
143
  );
154
144
  }
155
- if (shouldResolveConflicts && isTrustedActor) {
156
- enqueueResolvePendingConflictsForMessageJob(
157
- input.messageId,
158
- input.scopeId ?? "default",
159
- tx,
160
- );
161
- }
162
145
  enqueueMemoryJob(
163
146
  "build_conversation_summary",
164
147
  { conversationId: input.conversationId },
@@ -173,27 +156,18 @@ export function indexMessageNow(
173
156
  );
174
157
  }
175
158
 
176
- // Invalidate recall cache when synchronous segment writes changed content,
177
- // so lexical/recency retrieval doesn't serve stale results during worker lag.
178
- if (segments.length - skippedEmbedJobs > 0) {
179
- bumpMemoryVersion();
180
- }
181
-
182
- if (!isTrustedActor && (shouldExtract || shouldResolveConflicts)) {
159
+ if (!isTrustedActor && shouldExtract) {
183
160
  log.info(
184
- `Skipping extraction/conflict jobs for untrusted actor (trustClass=${input.provenanceTrustClass})`,
161
+ `Skipping extraction jobs for untrusted actor (trustClass=${input.provenanceTrustClass})`,
185
162
  );
186
163
  }
187
164
 
188
- enqueueSummaryRollupJobsIfDue();
189
-
190
165
  const extractionGated = !isTrustedActor;
191
166
  const enqueuedJobs =
192
167
  segments.length -
193
168
  skippedEmbedJobs +
194
169
  mediaBlocks.length +
195
- (shouldExtract && !extractionGated ? 2 : 1) +
196
- (shouldResolveConflicts && !extractionGated ? 1 : 0);
170
+ (shouldExtract && !extractionGated ? 2 : 1);
197
171
  return {
198
172
  indexedSegments: segments.length,
199
173
  enqueuedJobs,
@@ -222,19 +196,6 @@ export function getRecentSegmentsForConversation(
222
196
  .all();
223
197
  }
224
198
 
225
- function enqueueSummaryRollupJobsIfDue(): void {
226
- const now = Date.now();
227
- const raw = getMemoryCheckpoint(SUMMARY_JOB_CHECKPOINT_KEY);
228
- const last = raw ? Number.parseInt(raw, 10) : 0;
229
- if (Number.isFinite(last) && now - last < SUMMARY_SCHEDULE_INTERVAL_MS)
230
- return;
231
-
232
- enqueueMemoryJob("refresh_weekly_summary", {});
233
- enqueueMemoryJob("refresh_monthly_summary", {});
234
- setMemoryCheckpoint(SUMMARY_JOB_CHECKPOINT_KEY, String(now));
235
- log.debug("Scheduled periodic global summary jobs");
236
- }
237
-
238
199
  function buildSegmentId(messageId: string, segmentIndex: number): string {
239
200
  return `${messageId}:${segmentIndex}`;
240
201
  }
@@ -41,6 +41,8 @@ export interface IngressInvite {
41
41
  // Display metadata for personalized voice prompts (null for non-voice invites)
42
42
  friendName: string | null;
43
43
  guardianName: string | null;
44
+ // Contact binding — every invite is bound to a specific contact
45
+ contactId: string;
44
46
  createdAt: number;
45
47
  updatedAt: number;
46
48
  }
@@ -86,6 +88,7 @@ function rowToInvite(
86
88
  inviteCodeHash: row.inviteCodeHash,
87
89
  friendName: row.friendName,
88
90
  guardianName: row.guardianName,
91
+ contactId: row.contactId,
89
92
  createdAt: row.createdAt,
90
93
  updatedAt: row.updatedAt,
91
94
  };
@@ -97,6 +100,7 @@ function rowToInvite(
97
100
 
98
101
  export function createInvite(params: {
99
102
  sourceChannel: string;
103
+ contactId: string;
100
104
  createdBySessionId?: string;
101
105
  note?: string;
102
106
  maxUses?: number;
@@ -135,6 +139,7 @@ export function createInvite(params: {
135
139
  inviteCodeHash: params.inviteCodeHash ?? null,
136
140
  friendName: params.friendName ?? null,
137
141
  guardianName: params.guardianName ?? null,
142
+ contactId: params.contactId,
138
143
  createdAt: now,
139
144
  updatedAt: now,
140
145
  };
@@ -312,6 +317,20 @@ export function findByTokenHash(tokenHash: string): IngressInvite | null {
312
317
  return row ? rowToInvite(row) : null;
313
318
  }
314
319
 
320
+ // ---------------------------------------------------------------------------
321
+ // findById
322
+ // ---------------------------------------------------------------------------
323
+
324
+ export function findById(inviteId: string): IngressInvite | null {
325
+ const db = getDb();
326
+ const row = db
327
+ .select()
328
+ .from(assistantIngressInvites)
329
+ .where(eq(assistantIngressInvites.id, inviteId))
330
+ .get();
331
+ return row ? rowToInvite(row) : null;
332
+ }
333
+
315
334
  // ---------------------------------------------------------------------------
316
335
  // findActiveVoiceInvites
317
336
  // ---------------------------------------------------------------------------