@vellumai/assistant 0.5.13 → 0.5.15

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 (425) hide show
  1. package/.env.example +1 -6
  2. package/AGENTS.md +4 -0
  3. package/ARCHITECTURE.md +0 -1
  4. package/bunfig.toml +1 -0
  5. package/docs/architecture/memory.md +3 -3
  6. package/openapi.yaml +127 -22
  7. package/package.json +1 -1
  8. package/src/__tests__/access-request-decision.test.ts +2 -32
  9. package/src/__tests__/actor-token-service.test.ts +1 -31
  10. package/src/__tests__/anthropic-provider.test.ts +53 -40
  11. package/src/__tests__/app-git-history.test.ts +9 -17
  12. package/src/__tests__/app-git-service.test.ts +14 -20
  13. package/src/__tests__/app-store-dir-names.test.ts +10 -20
  14. package/src/__tests__/approval-cascade.test.ts +2 -19
  15. package/src/__tests__/approval-primitive.test.ts +2 -27
  16. package/src/__tests__/approval-routes-http.test.ts +2 -30
  17. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -28
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -45
  19. package/src/__tests__/attachments-store.test.ts +5 -32
  20. package/src/__tests__/audit-log-rotation.test.ts +5 -36
  21. package/src/__tests__/avatar-e2e.test.ts +1 -9
  22. package/src/__tests__/avatar-generator.test.ts +1 -7
  23. package/src/__tests__/browser-fill-credential.test.ts +0 -4
  24. package/src/__tests__/browser-manager.test.ts +0 -6
  25. package/src/__tests__/call-controller.test.ts +1 -22
  26. package/src/__tests__/call-conversation-messages.test.ts +0 -21
  27. package/src/__tests__/call-domain.test.ts +0 -25
  28. package/src/__tests__/call-pointer-messages.test.ts +0 -21
  29. package/src/__tests__/call-recovery.test.ts +0 -22
  30. package/src/__tests__/call-routes-http.test.ts +0 -24
  31. package/src/__tests__/call-store.test.ts +0 -21
  32. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -24
  33. package/src/__tests__/canonical-guardian-store.test.ts +48 -21
  34. package/src/__tests__/channel-approval-routes.test.ts +6 -26
  35. package/src/__tests__/channel-approvals.test.ts +1 -38
  36. package/src/__tests__/channel-delivery-store.test.ts +0 -21
  37. package/src/__tests__/channel-guardian.test.ts +0 -26
  38. package/src/__tests__/channel-reply-delivery.test.ts +5 -0
  39. package/src/__tests__/channel-retry-sweep.test.ts +0 -21
  40. package/src/__tests__/checker.test.ts +26 -61
  41. package/src/__tests__/clawhub.test.ts +9 -25
  42. package/src/__tests__/cli-command-risk-guard.test.ts +0 -18
  43. package/src/__tests__/config-loader-backfill.test.ts +9 -28
  44. package/src/__tests__/config-schema-cmd.test.ts +5 -25
  45. package/src/__tests__/config-schema.test.ts +21 -40
  46. package/src/__tests__/config-watcher.test.ts +4 -91
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -21
  48. package/src/__tests__/contacts-tools.test.ts +0 -21
  49. package/src/__tests__/context-memory-e2e.test.ts +0 -21
  50. package/src/__tests__/context-window-manager.test.ts +130 -3
  51. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -4
  52. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -4
  53. package/src/__tests__/conversation-agent-loop.test.ts +0 -4
  54. package/src/__tests__/conversation-attachments.test.ts +1 -24
  55. package/src/__tests__/conversation-attention-store.test.ts +0 -21
  56. package/src/__tests__/conversation-attention-telegram.test.ts +0 -22
  57. package/src/__tests__/conversation-clear-safety.test.ts +0 -22
  58. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -21
  59. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -24
  60. package/src/__tests__/conversation-disk-view-integration.test.ts +1 -23
  61. package/src/__tests__/conversation-disk-view.test.ts +5 -27
  62. package/src/__tests__/conversation-error.test.ts +1 -1
  63. package/src/__tests__/conversation-fork-crud.test.ts +1 -33
  64. package/src/__tests__/conversation-fork-route.test.ts +0 -27
  65. package/src/__tests__/conversation-history-web-search.test.ts +23 -16
  66. package/src/__tests__/conversation-init.benchmark.test.ts +22 -43
  67. package/src/__tests__/conversation-key-store-disk-view.test.ts +8 -34
  68. package/src/__tests__/conversation-load-history-repair.test.ts +0 -4
  69. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -4
  70. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -4
  71. package/src/__tests__/conversation-queue.test.ts +8 -8
  72. package/src/__tests__/conversation-routes-disk-view.test.ts +13 -51
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +64 -38
  74. package/src/__tests__/conversation-slash-commands.test.ts +5 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +0 -4
  76. package/src/__tests__/conversation-slash-unknown.test.ts +0 -4
  77. package/src/__tests__/conversation-speed-override.test.ts +326 -0
  78. package/src/__tests__/conversation-starter-routes.test.ts +0 -23
  79. package/src/__tests__/conversation-store.test.ts +0 -21
  80. package/src/__tests__/conversation-unread-route.test.ts +0 -24
  81. package/src/__tests__/conversation-usage.test.ts +56 -21
  82. package/src/__tests__/conversation-wipe.test.ts +0 -21
  83. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -4
  84. package/src/__tests__/conversation-workspace-injection.test.ts +0 -4
  85. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -4
  86. package/src/__tests__/credential-execution-shell-lockdown.test.ts +8 -5
  87. package/src/__tests__/credential-vault-unit.test.ts +9 -428
  88. package/src/__tests__/credentials-cli.test.ts +10 -10
  89. package/src/__tests__/daemon-assistant-events.test.ts +0 -19
  90. package/src/__tests__/date-context.test.ts +77 -97
  91. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +7 -24
  92. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +29 -42
  93. package/src/__tests__/delete-managed-skill-tool.test.ts +2 -10
  94. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -26
  95. package/src/__tests__/docker-signing-key-bootstrap.test.ts +61 -15
  96. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -36
  97. package/src/__tests__/email-cli.test.ts +6 -6
  98. package/src/__tests__/ephemeral-permissions.test.ts +5 -17
  99. package/src/__tests__/first-greeting.test.ts +4 -32
  100. package/src/__tests__/followup-tools.test.ts +0 -21
  101. package/src/__tests__/gateway-only-enforcement.test.ts +0 -20
  102. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -23
  103. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -23
  104. package/src/__tests__/guardian-action-followup-store.test.ts +0 -21
  105. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -21
  106. package/src/__tests__/guardian-action-late-reply.test.ts +0 -21
  107. package/src/__tests__/guardian-action-store.test.ts +0 -21
  108. package/src/__tests__/guardian-action-sweep.test.ts +0 -21
  109. package/src/__tests__/guardian-binding-drift-heal.test.ts +0 -23
  110. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +172 -22
  111. package/src/__tests__/guardian-dispatch.test.ts +0 -21
  112. package/src/__tests__/guardian-grant-minting.test.ts +0 -22
  113. package/src/__tests__/guardian-outbound-http.test.ts +0 -22
  114. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -23
  115. package/src/__tests__/guardian-routing-invariants.test.ts +0 -22
  116. package/src/__tests__/guardian-routing-state.test.ts +0 -22
  117. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -24
  118. package/src/__tests__/headless-browser-interactions.test.ts +0 -4
  119. package/src/__tests__/headless-browser-navigate.test.ts +0 -4
  120. package/src/__tests__/headless-browser-read-tools.test.ts +0 -4
  121. package/src/__tests__/headless-browser-snapshot.test.ts +0 -4
  122. package/src/__tests__/heartbeat-service.test.ts +99 -26
  123. package/src/__tests__/hooks-blocking.test.ts +3 -3
  124. package/src/__tests__/hooks-config.test.ts +7 -7
  125. package/src/__tests__/hooks-discovery.test.ts +3 -3
  126. package/src/__tests__/hooks-integration.test.ts +5 -5
  127. package/src/__tests__/hooks-manager.test.ts +3 -3
  128. package/src/__tests__/hooks-runner.test.ts +5 -23
  129. package/src/__tests__/hooks-settings.test.ts +3 -3
  130. package/src/__tests__/hooks-templates.test.ts +3 -3
  131. package/src/__tests__/http-conversation-lineage.test.ts +0 -27
  132. package/src/__tests__/identity-intro-cache.test.ts +0 -4
  133. package/src/__tests__/inbound-invite-redemption.test.ts +0 -22
  134. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -16
  135. package/src/__tests__/intent-routing.test.ts +2 -55
  136. package/src/__tests__/invite-redemption-service.test.ts +0 -21
  137. package/src/__tests__/invite-routes-http.test.ts +0 -21
  138. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +0 -17
  139. package/src/__tests__/journal-context.test.ts +8 -75
  140. package/src/__tests__/list-messages-attachments.test.ts +0 -22
  141. package/src/__tests__/llm-context-route-provider.test.ts +0 -21
  142. package/src/__tests__/llm-request-log-turn-query.test.ts +46 -28
  143. package/src/__tests__/llm-usage-store.test.ts +0 -21
  144. package/src/__tests__/log-export-workspace.test.ts +1 -1
  145. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
  146. package/src/__tests__/managed-store.test.ts +1 -1
  147. package/src/__tests__/mcp-cli.test.ts +7 -10
  148. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -21
  149. package/src/__tests__/memory-jobs-worker-backoff.test.ts +0 -11
  150. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -21
  151. package/src/__tests__/memory-recall-log-store.test.ts +0 -27
  152. package/src/__tests__/memory-recall-quality.test.ts +0 -21
  153. package/src/__tests__/memory-regressions.experimental.test.ts +31 -30
  154. package/src/__tests__/memory-regressions.test.ts +282 -70
  155. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -21
  156. package/src/__tests__/memory-upsert-concurrency.test.ts +0 -21
  157. package/src/__tests__/messaging-send-tool.test.ts +201 -0
  158. package/src/__tests__/migration-cross-version-compatibility.test.ts +18 -13
  159. package/src/__tests__/migration-export-http.test.ts +7 -1
  160. package/src/__tests__/migration-import-commit-http.test.ts +16 -14
  161. package/src/__tests__/migration-import-preflight-http.test.ts +27 -44
  162. package/src/__tests__/migration-validate-http.test.ts +1 -28
  163. package/src/__tests__/native-web-search.test.ts +25 -22
  164. package/src/__tests__/non-member-access-request.test.ts +0 -22
  165. package/src/__tests__/notification-guardian-path.test.ts +0 -21
  166. package/src/__tests__/notification-schedule-dedup.test.ts +1 -25
  167. package/src/__tests__/oauth-apps-routes.test.ts +103 -2
  168. package/src/__tests__/oauth-cli.test.ts +52 -0
  169. package/src/__tests__/oauth-provider-profiles.test.ts +0 -16
  170. package/src/__tests__/oauth-provider-serializer.test.ts +232 -0
  171. package/src/__tests__/oauth-providers-routes.test.ts +257 -0
  172. package/src/__tests__/oauth-store.test.ts +0 -21
  173. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  174. package/src/__tests__/openai-provider.test.ts +261 -0
  175. package/src/__tests__/pairing-concurrent.test.ts +6 -6
  176. package/src/__tests__/pairing-routes.test.ts +7 -1
  177. package/src/__tests__/path-policy.test.ts +1 -1
  178. package/src/__tests__/platform.test.ts +64 -88
  179. package/src/__tests__/playbook-execution.test.ts +0 -21
  180. package/src/__tests__/playbook-tools.test.ts +0 -21
  181. package/src/__tests__/pricing.test.ts +100 -0
  182. package/src/__tests__/relay-server.test.ts +1 -25
  183. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -24
  184. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -24
  185. package/src/__tests__/runtime-events-sse.test.ts +0 -24
  186. package/src/__tests__/sandbox-diagnostics.test.ts +2 -1
  187. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
  188. package/src/__tests__/schedule-store.test.ts +0 -21
  189. package/src/__tests__/schedule-tools.test.ts +0 -21
  190. package/src/__tests__/scheduler-recurrence.test.ts +0 -21
  191. package/src/__tests__/scoped-approval-grants.test.ts +0 -21
  192. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -21
  193. package/src/__tests__/secret-allowlist.test.ts +1 -1
  194. package/src/__tests__/secret-ingress-channel.test.ts +0 -5
  195. package/src/__tests__/secret-ingress-cli.test.ts +0 -6
  196. package/src/__tests__/secret-ingress-http.test.ts +0 -5
  197. package/src/__tests__/secret-ingress.test.ts +0 -5
  198. package/src/__tests__/send-endpoint-busy.test.ts +0 -24
  199. package/src/__tests__/sequence-store.test.ts +0 -21
  200. package/src/__tests__/server-history-render.test.ts +0 -24
  201. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -4
  202. package/src/__tests__/skill-load-inline-command.test.ts +9 -0
  203. package/src/__tests__/skill-load-inline-includes.test.ts +9 -0
  204. package/src/__tests__/skill-load-tool.test.ts +11 -0
  205. package/src/__tests__/skills-uninstall.test.ts +10 -8
  206. package/src/__tests__/skills.test.ts +1 -1
  207. package/src/__tests__/slack-channel-config.test.ts +1 -1
  208. package/src/__tests__/slack-inbound-verification.test.ts +0 -22
  209. package/src/__tests__/starter-bundle.test.ts +4 -1
  210. package/src/__tests__/suggestion-routes.test.ts +2 -0
  211. package/src/__tests__/system-prompt.test.ts +1 -1
  212. package/src/__tests__/terminal-tools.test.ts +1 -1
  213. package/src/__tests__/test-preload.ts +31 -0
  214. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
  215. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  216. package/src/__tests__/tool-executor.test.ts +0 -20
  217. package/src/__tests__/tool-input-summary.test.ts +124 -0
  218. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -1
  219. package/src/__tests__/trust-store.test.ts +7 -1
  220. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -1
  221. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -1
  222. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -1
  223. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  224. package/src/__tests__/turn-boundary-resolution.test.ts +1 -1
  225. package/src/__tests__/twilio-routes.test.ts +1 -1
  226. package/src/__tests__/update-bulletin.test.ts +1 -1
  227. package/src/__tests__/vbundle-pax-and-symlink.test.ts +1 -1
  228. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -0
  229. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -1
  230. package/src/__tests__/voice-session-bridge.test.ts +1 -1
  231. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +4 -4
  232. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -1
  233. package/src/__tests__/workspace-migration-down-functions.test.ts +15 -3
  234. package/src/__tests__/workspace-migration-seed-device-id.test.ts +40 -4
  235. package/src/agent/loop.ts +6 -9
  236. package/src/approvals/guardian-decision-primitive.ts +46 -18
  237. package/src/approvals/guardian-request-resolvers.ts +19 -2
  238. package/src/calls/active-call-lease.ts +2 -2
  239. package/src/cli/AGENTS.md +1 -1
  240. package/src/cli/commands/doctor.ts +9 -9
  241. package/src/cli/commands/memory.ts +142 -0
  242. package/src/cli/commands/oauth/__tests__/connect.test.ts +13 -11
  243. package/src/cli/commands/oauth/__tests__/ping.test.ts +1 -1
  244. package/src/cli/commands/oauth/connect.ts +13 -12
  245. package/src/cli/commands/oauth/index.ts +1 -1
  246. package/src/cli/commands/oauth/providers.ts +47 -62
  247. package/src/cli/commands/platform/__tests__/connect.test.ts +72 -46
  248. package/src/cli/commands/platform/__tests__/disconnect.test.ts +54 -1
  249. package/src/cli/commands/platform/__tests__/status.test.ts +36 -0
  250. package/src/cli/commands/platform/connect.ts +17 -7
  251. package/src/cli/commands/platform/disconnect.ts +28 -3
  252. package/src/cli/commands/platform/index.ts +3 -3
  253. package/src/cli.ts +1 -299
  254. package/src/config/assistant-feature-flags.ts +23 -15
  255. package/src/config/bundled-skills/app-builder/TOOLS.json +16 -0
  256. package/src/config/bundled-skills/app-builder/tools/app-create.ts +4 -0
  257. package/src/config/bundled-skills/app-builder/tools/app-delete.ts +5 -1
  258. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +9 -1
  259. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +5 -1
  260. package/src/config/bundled-skills/contacts/TOOLS.json +8 -0
  261. package/src/config/bundled-skills/contacts/tools/contact-search.ts +10 -1
  262. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +16 -2
  263. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +1 -0
  264. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  265. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +37 -0
  266. package/src/config/bundled-skills/slack/SKILL.md +18 -0
  267. package/src/config/env-registry.ts +15 -11
  268. package/src/config/env.ts +1 -11
  269. package/src/config/feature-flag-registry.json +16 -0
  270. package/src/config/schema.ts +4 -0
  271. package/src/config/schemas/heartbeat.ts +6 -1
  272. package/src/config/schemas/inference.ts +14 -3
  273. package/src/config/schemas/memory-processing.ts +16 -8
  274. package/src/config/schemas/memory-retrieval.ts +3 -3
  275. package/src/config/skills.ts +1 -1
  276. package/src/context/window-manager.ts +174 -51
  277. package/src/credential-execution/executable-discovery.ts +2 -2
  278. package/src/daemon/approved-devices-store.ts +2 -2
  279. package/src/daemon/assistant-attachments.ts +2 -0
  280. package/src/daemon/config-watcher.ts +4 -50
  281. package/src/daemon/conversation-agent-loop-handlers.ts +9 -1
  282. package/src/daemon/conversation-agent-loop.ts +12 -0
  283. package/src/daemon/conversation-error.ts +3 -5
  284. package/src/daemon/conversation-history.ts +7 -3
  285. package/src/daemon/conversation-lifecycle.ts +16 -0
  286. package/src/daemon/conversation-messaging.ts +1 -0
  287. package/src/daemon/conversation-notifiers.ts +67 -30
  288. package/src/daemon/conversation-process.ts +161 -2
  289. package/src/daemon/conversation-queue-manager.ts +2 -0
  290. package/src/daemon/conversation-runtime-assembly.ts +33 -11
  291. package/src/daemon/conversation-slash.ts +14 -3
  292. package/src/daemon/conversation-tool-setup.ts +2 -0
  293. package/src/daemon/conversation-usage.ts +32 -4
  294. package/src/daemon/conversation.ts +33 -1
  295. package/src/daemon/daemon-control.ts +32 -16
  296. package/src/daemon/date-context.ts +47 -45
  297. package/src/daemon/dictation-profile-store.ts +2 -2
  298. package/src/daemon/handlers/conversations.ts +19 -0
  299. package/src/daemon/handlers/shared.ts +14 -21
  300. package/src/daemon/lifecycle.ts +5 -7
  301. package/src/daemon/message-types/conversations.ts +2 -0
  302. package/src/daemon/message-types/guardian-actions.ts +3 -17
  303. package/src/daemon/message-types/integrations.ts +11 -1
  304. package/src/daemon/message-types/messages.ts +1 -0
  305. package/src/daemon/pairing-store.ts +2 -79
  306. package/src/daemon/server.ts +154 -8
  307. package/src/daemon/watch-handler.ts +65 -21
  308. package/src/email/guardrails.ts +3 -3
  309. package/src/heartbeat/heartbeat-service.ts +14 -7
  310. package/src/hooks/cli.ts +2 -2
  311. package/src/hooks/config.ts +2 -2
  312. package/src/hooks/discovery.ts +2 -2
  313. package/src/hooks/manager.ts +2 -2
  314. package/src/hooks/runner.ts +5 -2
  315. package/src/hooks/templates.ts +2 -2
  316. package/src/memory/admin.ts +181 -2
  317. package/src/memory/app-git-service.ts +61 -4
  318. package/src/memory/attachments-store.ts +2 -0
  319. package/src/memory/canonical-guardian-store.ts +16 -0
  320. package/src/memory/db-init.ts +8 -0
  321. package/src/memory/embedding-local.ts +5 -2
  322. package/src/memory/indexer.ts +44 -26
  323. package/src/memory/items-extractor.ts +34 -82
  324. package/src/memory/job-handlers/batch-extraction.ts +741 -0
  325. package/src/memory/job-handlers/journal-carry-forward.test.ts +383 -0
  326. package/src/memory/job-handlers/journal-carry-forward.ts +255 -0
  327. package/src/memory/jobs-store.ts +28 -0
  328. package/src/memory/jobs-worker.ts +56 -9
  329. package/src/memory/lifecycle-events-store.ts +4 -2
  330. package/src/memory/llm-request-log-store.ts +40 -2
  331. package/src/memory/llm-usage-store.ts +4 -3
  332. package/src/memory/migrations/199-guardian-request-enrichment-columns.ts +71 -0
  333. package/src/memory/migrations/200-usage-llm-call-count.ts +20 -0
  334. package/src/memory/migrations/index.ts +2 -0
  335. package/src/memory/query-expansion.ts +83 -0
  336. package/src/memory/retriever.test.ts +119 -0
  337. package/src/memory/retriever.ts +513 -105
  338. package/src/memory/schema/guardian.ts +4 -0
  339. package/src/memory/schema/infrastructure.ts +1 -0
  340. package/src/memory/search/formatting.test.ts +140 -0
  341. package/src/memory/search/formatting.ts +143 -198
  342. package/src/memory/search/mmr.ts +136 -0
  343. package/src/memory/search/staleness.ts +0 -15
  344. package/src/memory/search/tier-classifier.ts +10 -21
  345. package/src/memory/search/types.ts +17 -0
  346. package/src/messaging/providers/slack/adapter.ts +51 -5
  347. package/src/notifications/broadcaster.ts +13 -0
  348. package/src/notifications/copy-composer.ts +8 -0
  349. package/src/oauth/connect-orchestrator.ts +1 -1
  350. package/src/oauth/connection-resolver.ts +2 -2
  351. package/src/oauth/provider-serializer.ts +116 -0
  352. package/src/permissions/trust-store.ts +24 -7
  353. package/src/prompts/__tests__/build-cli-reference-section.test.ts +5 -0
  354. package/src/prompts/journal-context.ts +50 -35
  355. package/src/prompts/persona-resolver.ts +1 -1
  356. package/src/prompts/system-prompt.ts +27 -28
  357. package/src/prompts/templates/BOOTSTRAP.md +14 -1
  358. package/src/prompts/templates/HEARTBEAT.md +10 -0
  359. package/src/prompts/templates/NOW.md +19 -25
  360. package/src/prompts/templates/SOUL.md +13 -1
  361. package/src/prompts/templates/UPDATES.md +12 -0
  362. package/src/prompts/update-bulletin.ts +1 -1
  363. package/src/providers/anthropic/client.ts +89 -18
  364. package/src/providers/model-catalog.ts +22 -2
  365. package/src/providers/model-intents.ts +2 -2
  366. package/src/providers/openai/client.ts +40 -1
  367. package/src/providers/retry.ts +23 -4
  368. package/src/providers/types.ts +2 -0
  369. package/src/runtime/assistant-scope.ts +1 -1
  370. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -0
  371. package/src/runtime/auth/route-policy.ts +1 -0
  372. package/src/runtime/auth/token-service.ts +51 -29
  373. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -1
  374. package/src/runtime/guardian-decision-types.ts +16 -10
  375. package/src/runtime/http-server.ts +3 -14
  376. package/src/runtime/http-types.ts +1 -0
  377. package/src/runtime/migrations/vbundle-builder.ts +7 -4
  378. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -4
  379. package/src/runtime/migrations/vbundle-importer.ts +1 -1
  380. package/src/runtime/routes/conversation-query-routes.ts +40 -8
  381. package/src/runtime/routes/conversation-routes.ts +125 -3
  382. package/src/runtime/routes/guardian-action-routes.ts +9 -3
  383. package/src/runtime/routes/identity-routes.ts +25 -4
  384. package/src/runtime/routes/llm-context-normalization.ts +1 -0
  385. package/src/runtime/routes/log-export-routes.ts +34 -12
  386. package/src/runtime/routes/migration-routes.ts +6 -10
  387. package/src/runtime/routes/oauth-apps.ts +2 -9
  388. package/src/runtime/routes/oauth-providers.ts +60 -0
  389. package/src/runtime/routes/pairing-routes.ts +0 -8
  390. package/src/runtime/routes/settings-routes.ts +0 -1
  391. package/src/runtime/routes/telemetry-routes.ts +16 -4
  392. package/src/security/encrypted-store.ts +2 -2
  393. package/src/security/secret-allowlist.ts +3 -3
  394. package/src/signals/emit-event.ts +42 -0
  395. package/src/signals/user-message.ts +37 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +83 -19
  397. package/src/telemetry/usage-telemetry-reporter.ts +23 -17
  398. package/src/tools/browser/runtime-check.ts +2 -2
  399. package/src/tools/credentials/vault.ts +2 -249
  400. package/src/tools/memory/definitions.ts +1 -1
  401. package/src/tools/memory/handlers.test.ts +50 -8
  402. package/src/tools/memory/handlers.ts +3 -1
  403. package/src/tools/side-effects.ts +1 -6
  404. package/src/tools/terminal/safe-env.ts +3 -2
  405. package/src/tools/terminal/shell.ts +11 -14
  406. package/src/tools/tool-approval-handler.ts +20 -1
  407. package/src/tools/tool-input-summary.ts +66 -0
  408. package/src/tools/types.ts +4 -0
  409. package/src/usage/types.ts +4 -0
  410. package/src/util/device-id.ts +10 -10
  411. package/src/util/platform.ts +71 -33
  412. package/src/util/pricing.ts +19 -6
  413. package/src/util/strip-comment-lines.ts +28 -0
  414. package/src/workspace/git-service.ts +8 -18
  415. package/src/workspace/migrations/003-seed-device-id.ts +6 -4
  416. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +7 -1
  417. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -4
  418. package/src/workspace/migrations/021-move-signals-to-workspace.ts +84 -0
  419. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +94 -0
  420. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +86 -0
  421. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +126 -0
  422. package/src/workspace/migrations/migrate-to-workspace-volume.ts +3 -6
  423. package/src/workspace/migrations/registry.ts +8 -0
  424. package/src/signals/confirm.ts +0 -82
  425. package/src/signals/trust-rule.ts +0 -174
@@ -1,4 +1,4 @@
1
- import { asc, eq, inArray, sql } from "drizzle-orm";
1
+ import { and, asc, eq, inArray, notInArray, sql } from "drizzle-orm";
2
2
 
3
3
  import type { AssistantConfig } from "../config/types.js";
4
4
  import { estimateTextTokens } from "../context/token-estimator.js";
@@ -9,6 +9,7 @@ import {
9
9
  computeRetryDelay,
10
10
  isRetryableNetworkError,
11
11
  } from "../util/retry.js";
12
+ import { getConversationDirName } from "./conversation-directories.js";
12
13
  import { getDb } from "./db.js";
13
14
  import {
14
15
  embedWithBackend,
@@ -17,22 +18,19 @@ import {
17
18
  logMemoryEmbeddingWarning,
18
19
  } from "./embedding-backend.js";
19
20
  import { isQdrantBreakerOpen } from "./qdrant-circuit-breaker.js";
21
+ import { expandQueryWithHyDE } from "./query-expansion.js";
20
22
  import {
21
23
  conversations,
22
24
  memoryItems,
23
25
  memoryItemSources,
24
26
  messages,
25
27
  } from "./schema.js";
26
- import {
27
- buildTwoLayerInjection,
28
- CAPABILITY_KINDS,
29
- IDENTITY_KINDS,
30
- PREFERENCE_KINDS,
31
- } from "./search/formatting.js";
28
+ import { buildMemoryInjection } from "./search/formatting.js";
29
+ import { applyMMR } from "./search/mmr.js";
32
30
  import { isQdrantConnectionError, semanticSearch } from "./search/semantic.js";
33
- import { applyStaleDemotion, computeStaleness } from "./search/staleness.js";
31
+ import { computeStaleness } from "./search/staleness.js";
34
32
  import {
35
- classifyTiers,
33
+ filterByMinScore,
36
34
  type TieredCandidate,
37
35
  } from "./search/tier-classifier.js";
38
36
  import type {
@@ -50,6 +48,7 @@ export {
50
48
  escapeXmlTags,
51
49
  formatAbsoluteTime,
52
50
  formatRelativeTime,
51
+ lookupSupersessionChain,
53
52
  } from "./search/formatting.js";
54
53
  export type {
55
54
  DegradationReason,
@@ -64,6 +63,10 @@ const log = getLogger("memory-retriever");
64
63
  const EMBED_MAX_RETRIES = 3;
65
64
  const EMBED_BASE_DELAY_MS = 500;
66
65
 
66
+ /** MMR diversity penalty applied to near-duplicate items after score filtering.
67
+ * 0 = no penalty, 1 = maximum penalty. */
68
+ const MMR_PENALTY = 0.6;
69
+
67
70
  /**
68
71
  * Wrap embedWithBackend with retry + exponential backoff for transient failures
69
72
  * (network errors, 429s, 5xx). Aborts immediately if the caller's signal fires.
@@ -234,20 +237,141 @@ async function generateQueryEmbedding(
234
237
  return { queryVector, provider, model, degraded, degradation, reason };
235
238
  }
236
239
 
240
+ /** Result from HyDE-expanded search. */
241
+ interface HyDESearchResult {
242
+ candidates: Candidate[];
243
+ hydeExpanded: boolean;
244
+ hydeDocCount: number;
245
+ }
246
+
237
247
  /**
238
- * Memory recall pipeline: hybrid search tier classification →
239
- * staleness annotation two-layer XML injection.
248
+ * Run HyDE-expanded search: generate hypothetical documents, embed them
249
+ * alongside the raw query in parallel, run parallel semantic searches,
250
+ * and merge all candidate arrays.
251
+ *
252
+ * Falls back to raw-query-only search on any HyDE failure (expansion
253
+ * error, embedding error for hypothetical docs). The raw query search
254
+ * always runs regardless of HyDE success.
255
+ */
256
+ async function runHyDESearch(
257
+ query: string,
258
+ rawQueryVector: number[],
259
+ config: AssistantConfig,
260
+ signal: AbortSignal | undefined,
261
+ provider: string,
262
+ model: string,
263
+ limit: number,
264
+ excludeMessageIds: string[],
265
+ scopeIds: string[] | undefined,
266
+ sparseVector: { indices: number[]; values: number[] } | undefined,
267
+ ): Promise<HyDESearchResult> {
268
+ // Always search with the raw query — this is our baseline
269
+ const rawSearchPromise = semanticSearch(
270
+ rawQueryVector,
271
+ provider,
272
+ model,
273
+ limit,
274
+ excludeMessageIds,
275
+ scopeIds,
276
+ sparseVector,
277
+ );
278
+ // Suppress unhandled rejection if Qdrant rejects before we await
279
+ rawSearchPromise.catch(() => {});
280
+
281
+ // Attempt HyDE expansion — returns [] on any failure
282
+ let hypotheticalDocs: string[];
283
+ try {
284
+ hypotheticalDocs = await expandQueryWithHyDE(query, config, signal);
285
+ } catch {
286
+ // expandQueryWithHyDE already catches internally, but be defensive
287
+ hypotheticalDocs = [];
288
+ }
289
+
290
+ if (hypotheticalDocs.length === 0) {
291
+ // No hypothetical docs — fall back to raw query only
292
+ const rawResults = await rawSearchPromise;
293
+ return {
294
+ candidates: rawResults,
295
+ hydeExpanded: false,
296
+ hydeDocCount: 0,
297
+ };
298
+ }
299
+
300
+ log.debug(
301
+ { hydeDocCount: hypotheticalDocs.length },
302
+ "HyDE expansion produced hypothetical documents",
303
+ );
304
+
305
+ // Embed all hypothetical docs in parallel with the raw search
306
+ let hydeVectors: number[][] = [];
307
+ try {
308
+ const hydeEmbedResult = await embedWithRetry(config, hypotheticalDocs, {
309
+ signal,
310
+ });
311
+ hydeVectors = hydeEmbedResult.vectors;
312
+ } catch (err) {
313
+ log.warn(
314
+ { err: err instanceof Error ? err.message : String(err) },
315
+ "Failed to embed HyDE hypothetical docs; falling back to raw query",
316
+ );
317
+ const rawResults = await rawSearchPromise;
318
+ return {
319
+ candidates: rawResults,
320
+ hydeExpanded: false,
321
+ hydeDocCount: 0,
322
+ };
323
+ }
324
+
325
+ // Run parallel semantic searches for each hypothetical doc embedding,
326
+ // generating per-doc sparse embeddings so sparse and dense components match.
327
+ const hydeSearchPromises = hydeVectors.map((vector, i) => {
328
+ const docSparseVector = generateSparseEmbedding(hypotheticalDocs[i]!);
329
+ return semanticSearch(
330
+ vector,
331
+ provider,
332
+ model,
333
+ limit,
334
+ excludeMessageIds,
335
+ scopeIds,
336
+ docSparseVector,
337
+ ).catch((err) => {
338
+ log.warn(
339
+ { err: err instanceof Error ? err.message : String(err) },
340
+ "HyDE hypothetical doc search failed; skipping",
341
+ );
342
+ return [] as Candidate[];
343
+ });
344
+ });
345
+
346
+ // Await all searches in parallel (raw + hypothetical)
347
+ const [rawResults, ...hydeResults] = await Promise.all([
348
+ rawSearchPromise,
349
+ ...hydeSearchPromises,
350
+ ]);
351
+
352
+ // Merge all candidate arrays into a single flat array
353
+ const allCandidates = [rawResults, ...hydeResults].flat();
354
+
355
+ return {
356
+ candidates: allCandidates,
357
+ hydeExpanded: true,
358
+ hydeDocCount: hypotheticalDocs.length,
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Memory recall pipeline: hybrid search → score filtering →
364
+ * staleness annotation → unified XML injection.
240
365
  *
241
366
  * Pipeline steps:
242
367
  * 1. Build query text (caller provides via buildMemoryQuery)
243
368
  * 2. Generate dense + sparse embeddings
244
369
  * 3. Hybrid search on Qdrant (dense + sparse RRF fusion)
245
370
  * 4. Deduplicate results
246
- * 5. Classify tiers (score > 0.6 → tier 1, > 0.4 → tier 2)
247
- * 6. Enrich item candidates with metadata for staleness
248
- * 7. Compute staleness per item
249
- * 8. Demote very_stale tier 1 tier 2
250
- * 9. Build two-layer XML injection with budget allocation
371
+ * 5. Filter by minimum score threshold
372
+ * 6. Enrich candidates with source labels and item metadata
373
+ * 7. Compute staleness per item (for debugging/logging)
374
+ * 8. Build unified XML injection with budget allocation
251
375
  */
252
376
  export async function buildMemoryRecall(
253
377
  query: string,
@@ -300,26 +424,49 @@ export async function buildMemoryRecall(
300
424
  options?.scopePolicyOverride,
301
425
  );
302
426
 
303
- const HYBRID_LIMIT = 20;
427
+ const HYBRID_LIMIT = 40;
304
428
 
305
429
  let hybridCandidates: Candidate[] = [];
306
430
  let semanticSearchFailed = false;
307
431
  let sparseVectorUsed = false;
432
+ let hydeExpanded = false;
433
+ let hydeDocCount = 0;
308
434
  const hybridSearchStart = Date.now();
309
435
 
310
436
  const qdrantBreakerOpen = isQdrantBreakerOpen();
311
437
  if (queryVector && !qdrantBreakerOpen) {
312
438
  try {
313
- hybridCandidates = await semanticSearch(
314
- queryVector,
315
- provider ?? "unknown",
316
- model ?? "unknown",
317
- HYBRID_LIMIT,
318
- excludeMessageIds,
319
- scopeIds,
320
- sparseVectorAvailable ? sparseVector : undefined,
321
- );
322
- sparseVectorUsed = sparseVectorAvailable;
439
+ if (options?.hydeEnabled) {
440
+ // ── HyDE path: expand query into hypothetical docs and search in parallel ──
441
+ const hydeCandidates = await runHyDESearch(
442
+ query,
443
+ queryVector,
444
+ config,
445
+ signal,
446
+ provider ?? "unknown",
447
+ model ?? "unknown",
448
+ HYBRID_LIMIT,
449
+ excludeMessageIds,
450
+ scopeIds,
451
+ sparseVectorAvailable ? sparseVector : undefined,
452
+ );
453
+ hybridCandidates = hydeCandidates.candidates;
454
+ hydeExpanded = hydeCandidates.hydeExpanded;
455
+ hydeDocCount = hydeCandidates.hydeDocCount;
456
+ sparseVectorUsed = sparseVectorAvailable || hydeExpanded;
457
+ } else {
458
+ // ── Standard path: single raw query search ──
459
+ hybridCandidates = await semanticSearch(
460
+ queryVector,
461
+ provider ?? "unknown",
462
+ model ?? "unknown",
463
+ HYBRID_LIMIT,
464
+ excludeMessageIds,
465
+ scopeIds,
466
+ sparseVectorAvailable ? sparseVector : undefined,
467
+ );
468
+ sparseVectorUsed = sparseVectorAvailable;
469
+ }
323
470
  } catch (err) {
324
471
  semanticSearchFailed = true;
325
472
  if (isQdrantConnectionError(err)) {
@@ -363,8 +510,9 @@ export async function buildMemoryRecall(
363
510
  // from messages that were removed by context compaction should be kept —
364
511
  // those messages are no longer in the conversation history and memory is
365
512
  // the only way they can influence the response.
513
+ let inContextMessageIds: Set<string> | null = null;
366
514
  if (conversationId) {
367
- const inContextMessageIds = getEffectiveInContextMessageIds(conversationId);
515
+ inContextMessageIds = getEffectiveInContextMessageIds(conversationId);
368
516
  if (inContextMessageIds) {
369
517
  for (const [key, c] of candidateMap) {
370
518
  if (c.type === "segment") {
@@ -413,11 +561,12 @@ export async function buildMemoryRecall(
413
561
  }
414
562
 
415
563
  // Filter items whose ALL sources are in-context
564
+ const contextIds = inContextMessageIds;
416
565
  for (const [key, c] of candidateMap) {
417
566
  if (c.type !== "item") continue;
418
567
  const sourceMessageIds = itemSourceMap.get(c.id);
419
568
  if (!sourceMessageIds || sourceMessageIds.length === 0) continue;
420
- if (sourceMessageIds.every((mid) => inContextMessageIds.has(mid))) {
569
+ if (sourceMessageIds.every((mid) => contextIds.has(mid))) {
421
570
  candidateMap.delete(key);
422
571
  }
423
572
  }
@@ -436,26 +585,78 @@ export async function buildMemoryRecall(
436
585
  for (const c of allCandidates) {
437
586
  // Multiplicative scoring: importance, confidence, and recency amplify semantic
438
587
  // relevance but can't substitute for it. An irrelevant item (semantic ≈ 0)
439
- // stays low regardless of metadata. Multiplier range: 0.4 (all zero) to 1.0.
588
+ // stays low regardless of metadata. Multiplier range: 0.35 (all zero) to 1.0.
440
589
  const metadataMultiplier =
441
- 0.4 + c.importance * 0.25 + c.confidence * 0.15 + c.recency * 0.2;
590
+ 0.35 + c.importance * 0.3 + c.confidence * 0.1 + c.recency * 0.25;
442
591
  c.finalScore = c.semantic * metadataMultiplier;
443
592
  }
444
593
  allCandidates.sort((a, b) => b.finalScore - a.finalScore);
445
594
 
446
- // ── Step 5: Tier classification ─────────────────────────────────
447
- const tiered = classifyTiers(allCandidates);
595
+ // ── Step 5: Filter by minimum score threshold ───────────────────
596
+ const filtered = filterByMinScore(allCandidates);
597
+
598
+ // ── Step 5b: MMR diversity ranking ─────────────────────────────
599
+ const mmrRanked = applyMMR(filtered, MMR_PENALTY);
600
+
601
+ // MMR rewrites finalScore, so re-enforce the min-score threshold to
602
+ // drop candidates whose adjusted score fell below the cutoff.
603
+ const diversified = filterByMinScore(mmrRanked);
604
+
605
+ // ── Step 5c: Enrich candidates with source labels ──────────────
606
+ enrichSourceLabels(diversified);
607
+
608
+ // ── Serendipity: sample random memories for unexpected connections ──
609
+ const SERENDIPITY_COUNT = 3;
610
+ const serendipityCandidates = sampleSerendipityItems(
611
+ diversified,
612
+ SERENDIPITY_COUNT,
613
+ scopeIds,
614
+ );
615
+
616
+ // Filter serendipity items whose ALL sources are in-context (same logic
617
+ // as Step 4b) to prevent current-turn content leaking via random sampling.
618
+ if (inContextMessageIds && serendipityCandidates.length > 0) {
619
+ filterInContextItems(serendipityCandidates, inContextMessageIds);
620
+ }
448
621
 
449
- // ── Step 5b: Enrich candidates with source labels ──────────────
450
- enrichSourceLabels(tiered);
622
+ enrichSourceLabels(serendipityCandidates);
451
623
 
452
624
  // ── Step 6: Enrich with item metadata for staleness ─────────────
453
- const itemIds = tiered.filter((c) => c.type === "item").map((c) => c.id);
625
+ const itemIds = diversified.filter((c) => c.type === "item").map((c) => c.id);
454
626
  const itemMetadataMap = enrichItemMetadata(itemIds);
455
627
 
456
- // ── Step 7: Compute staleness per item ──────────────────────────
628
+ // ── Step 6b: Enrich item candidates with supersedes data ────────
629
+ const itemCandidatesForSupersedes = diversified.filter(
630
+ (c) => c.type === "item",
631
+ );
632
+ if (itemCandidatesForSupersedes.length > 0) {
633
+ try {
634
+ const db = getDb();
635
+ const supersedesRows = db
636
+ .select({ id: memoryItems.id, supersedes: memoryItems.supersedes })
637
+ .from(memoryItems)
638
+ .where(
639
+ inArray(
640
+ memoryItems.id,
641
+ itemCandidatesForSupersedes.map((c) => c.id),
642
+ ),
643
+ )
644
+ .all();
645
+ const supersedesMap = new Map(
646
+ supersedesRows.map((r) => [r.id, r.supersedes]),
647
+ );
648
+ for (const c of itemCandidatesForSupersedes) {
649
+ const sup = supersedesMap.get(c.id);
650
+ if (sup) c.supersedes = sup;
651
+ }
652
+ } catch (err) {
653
+ log.warn({ err }, "Failed to enrich candidates with supersedes data");
654
+ }
655
+ }
656
+
657
+ // ── Step 7: Compute staleness per item (for debugging/logging) ─
457
658
  const now = Date.now();
458
- for (const c of tiered) {
659
+ for (const c of diversified) {
459
660
  if (c.type !== "item") continue;
460
661
  const meta = itemMetadataMap.get(c.id);
461
662
  if (!meta) continue;
@@ -470,10 +671,7 @@ export async function buildMemoryRecall(
470
671
  c.staleness = level;
471
672
  }
472
673
 
473
- // ── Step 8: Demote very_stale tier 1 tier 2 ──────────────────
474
- const afterDemotion = applyStaleDemotion(tiered);
475
-
476
- // ── Step 9: Budget allocation and two-layer injection ──────────
674
+ // ── Step 8: Budget allocation and unified injection ────────────
477
675
  const maxInjectTokens = Math.max(
478
676
  1,
479
677
  Math.floor(
@@ -482,53 +680,24 @@ export async function buildMemoryRecall(
482
680
  ),
483
681
  );
484
682
 
485
- // Split into sections for two-layer injection
486
- const identityItems = afterDemotion.filter(
487
- (c) => c.tier === 1 && IDENTITY_KINDS.has(c.kind),
488
- );
489
- const preferences = afterDemotion.filter(
490
- (c) => c.tier === 1 && PREFERENCE_KINDS.has(c.kind),
491
- );
492
- const capabilities = afterDemotion.filter(
493
- (c) => c.tier === 1 && CAPABILITY_KINDS.has(c.kind),
494
- );
495
- const tier1Candidates = afterDemotion.filter(
496
- (c) =>
497
- c.tier === 1 &&
498
- !IDENTITY_KINDS.has(c.kind) &&
499
- !PREFERENCE_KINDS.has(c.kind) &&
500
- !CAPABILITY_KINDS.has(c.kind),
501
- );
502
- const tier2Candidates = afterDemotion.filter((c) => c.tier === 2);
503
-
504
- const injectedText = buildTwoLayerInjection({
505
- identityItems,
506
- tier1Candidates,
507
- tier2Candidates,
508
- preferences,
509
- capabilities,
683
+ const injectedText = buildMemoryInjection({
684
+ candidates: diversified,
685
+ serendipityItems: serendipityCandidates,
510
686
  totalBudgetTokens: maxInjectTokens,
511
687
  });
512
688
 
513
689
  // ── Assemble result ─────────────────────────────────────────────
514
- const selectedCount =
515
- identityItems.length +
516
- tier1Candidates.length +
517
- tier2Candidates.length +
518
- preferences.length +
519
- capabilities.length;
520
-
521
- const tier1Count = afterDemotion.filter((c) => c.tier === 1).length;
522
- const tier2Count = afterDemotion.filter((c) => c.tier === 2).length;
690
+ const selectedCount = diversified.length + serendipityCandidates.length;
691
+
523
692
  const stalenessStats = {
524
- fresh: afterDemotion.filter((c) => c.staleness === "fresh").length,
525
- aging: afterDemotion.filter((c) => c.staleness === "aging").length,
526
- stale: afterDemotion.filter((c) => c.staleness === "stale").length,
527
- very_stale: afterDemotion.filter((c) => c.staleness === "very_stale")
528
- .length,
693
+ fresh: diversified.filter((c) => c.staleness === "fresh").length,
694
+ aging: diversified.filter((c) => c.staleness === "aging").length,
695
+ stale: diversified.filter((c) => c.staleness === "stale").length,
696
+ very_stale: diversified.filter((c) => c.staleness === "very_stale").length,
529
697
  };
530
698
 
531
- const topCandidates: MemoryRecallCandiateDebug[] = afterDemotion
699
+ const topCandidates: MemoryRecallCandiateDebug[] = [...diversified]
700
+ .sort((a, b) => b.finalScore - a.finalScore)
532
701
  .slice(0, 10)
533
702
  .map((c) => ({
534
703
  key: c.key,
@@ -537,6 +706,7 @@ export async function buildMemoryRecall(
537
706
  finalScore: c.finalScore,
538
707
  semantic: c.semantic,
539
708
  recency: c.recency,
709
+ ...(c.sourceLabel ? { sourceLabel: c.sourceLabel } : {}),
540
710
  }));
541
711
 
542
712
  const latencyMs = Date.now() - start;
@@ -560,13 +730,12 @@ export async function buildMemoryRecall(
560
730
  query: truncate(query, 120),
561
731
  hybridHits: hybridCandidates.length,
562
732
  mergedCount: allCandidates.length,
563
- tier1Count,
564
- tier2Count,
565
733
  stalenessStats,
566
734
  selectedCount,
567
735
  maxInjectTokens,
568
736
  injectedTokens: estimateTextTokens(injectedText),
569
737
  latencyMs,
738
+ ...(hydeExpanded ? { hydeExpanded, hydeDocCount } : {}),
570
739
  },
571
740
  "Memory recall completed",
572
741
  );
@@ -585,10 +754,13 @@ export async function buildMemoryRecall(
585
754
  injectedText,
586
755
  latencyMs,
587
756
  topCandidates,
588
- tier1Count,
589
- tier2Count,
757
+ tier1Count: 0,
758
+ tier2Count: 0,
590
759
  hybridSearchMs,
591
760
  sparseVectorUsed,
761
+ hydeExpanded,
762
+ hydeDocCount,
763
+ mmrApplied: true,
592
764
  };
593
765
 
594
766
  return result;
@@ -758,17 +930,17 @@ function enrichSourceLabels(candidates: TieredCandidate[]): void {
758
930
  try {
759
931
  const db = getDb();
760
932
 
761
- // Collect item IDs for items that need source label lookup
933
+ // ── Items: find conversation via memoryItemSources messages conversations ──
762
934
  const itemCandidates = candidates.filter((c) => c.type === "item");
763
935
  const itemIds = itemCandidates.map((c) => c.id);
764
936
 
765
937
  if (itemIds.length > 0) {
766
- // For items: find conversation titles via memoryItemSources → messages → conversations.
767
- // Pick the most recent conversation title per item.
768
938
  const rows = db
769
939
  .select({
770
940
  memoryItemId: memoryItemSources.memoryItemId,
941
+ conversationId: conversations.id,
771
942
  title: conversations.title,
943
+ conversationCreatedAt: conversations.createdAt,
772
944
  conversationUpdatedAt: conversations.updatedAt,
773
945
  })
774
946
  .from(memoryItemSources)
@@ -783,36 +955,272 @@ function enrichSourceLabels(candidates: TieredCandidate[]): void {
783
955
  .where(inArray(memoryItemSources.memoryItemId, itemIds))
784
956
  .all();
785
957
 
786
- // Group by item ID and pick the most recently updated conversation title
787
- const titleMap = new Map<string, string>();
788
- const updatedAtMap = new Map<string, number>();
958
+ // Group by item ID and pick the most recently updated conversation
959
+ const bestConvMap = new Map<
960
+ string,
961
+ {
962
+ title: string | null;
963
+ conversationId: string;
964
+ createdAt: number;
965
+ updatedAt: number;
966
+ }
967
+ >();
789
968
  for (const row of rows) {
790
- if (!row.title) continue;
791
- const existing = updatedAtMap.get(row.memoryItemId);
792
- if (existing === undefined || row.conversationUpdatedAt > existing) {
793
- titleMap.set(row.memoryItemId, row.title);
794
- updatedAtMap.set(row.memoryItemId, row.conversationUpdatedAt);
969
+ const existing = bestConvMap.get(row.memoryItemId);
970
+ if (
971
+ existing === undefined ||
972
+ row.conversationUpdatedAt > existing.updatedAt
973
+ ) {
974
+ bestConvMap.set(row.memoryItemId, {
975
+ title: row.title,
976
+ conversationId: row.conversationId,
977
+ createdAt: row.conversationCreatedAt,
978
+ updatedAt: row.conversationUpdatedAt,
979
+ });
795
980
  }
796
981
  }
797
982
 
798
983
  for (const c of itemCandidates) {
799
- const title = titleMap.get(c.id);
800
- if (title) {
801
- c.sourceLabel = title;
984
+ const conv = bestConvMap.get(c.id);
985
+ if (conv) {
986
+ if (conv.title) c.sourceLabel = conv.title;
987
+ const dirName = getConversationDirName(
988
+ conv.conversationId,
989
+ conv.createdAt,
990
+ );
991
+ c.sourcePath = `conversations/${dirName}/messages.jsonl`;
802
992
  }
803
993
  }
804
994
  }
805
995
 
806
- // For segment candidates: the key format is "seg:<segmentId>" and the id is the segment's id.
807
- // We can look up the conversation title via the segment's conversationId in memory_segments.
808
- // However, segments already reference a conversationId in the schema — but the Candidate type
809
- // doesn't carry it. For now, skip segment source labels as the join path would require
810
- // importing memorySegments and an additional query. The primary value is item source labels.
996
+ // ── Segments: look up conversation via conversationId on the candidate ──
997
+ const segmentCandidates = candidates.filter(
998
+ (c) => (c.type === "segment" || c.type === "summary") && c.conversationId,
999
+ );
1000
+
1001
+ if (segmentCandidates.length > 0) {
1002
+ const convIds = [
1003
+ ...new Set(segmentCandidates.map((c) => c.conversationId!)),
1004
+ ];
1005
+ const convRows = db
1006
+ .select({
1007
+ id: conversations.id,
1008
+ title: conversations.title,
1009
+ createdAt: conversations.createdAt,
1010
+ })
1011
+ .from(conversations)
1012
+ .where(inArray(conversations.id, convIds))
1013
+ .all();
1014
+
1015
+ const convMap = new Map(convRows.map((r) => [r.id, r]));
1016
+
1017
+ for (const c of segmentCandidates) {
1018
+ const conv = convMap.get(c.conversationId!);
1019
+ if (conv) {
1020
+ if (conv.title) c.sourceLabel = conv.title;
1021
+ const dirName = getConversationDirName(conv.id, conv.createdAt);
1022
+ c.sourcePath = `conversations/${dirName}/messages.jsonl`;
1023
+ }
1024
+ }
1025
+ }
811
1026
  } catch (err) {
812
1027
  log.warn({ err }, "Failed to enrich candidates with source labels");
813
1028
  }
814
1029
  }
815
1030
 
1031
+ /**
1032
+ * Remove items from the array (in-place) whose ALL source messages are
1033
+ * in the given in-context set. This prevents current-turn content from
1034
+ * leaking into the injection via serendipity or other DB-sourced paths.
1035
+ */
1036
+ function filterInContextItems(
1037
+ candidates: TieredCandidate[],
1038
+ inContextMessageIds: Set<string>,
1039
+ ): void {
1040
+ const itemIds = candidates.filter((c) => c.type === "item").map((c) => c.id);
1041
+ if (itemIds.length === 0) return;
1042
+
1043
+ try {
1044
+ const db = getDb();
1045
+ const allSources = db
1046
+ .select({
1047
+ memoryItemId: memoryItemSources.memoryItemId,
1048
+ messageId: memoryItemSources.messageId,
1049
+ })
1050
+ .from(memoryItemSources)
1051
+ .where(inArray(memoryItemSources.memoryItemId, itemIds))
1052
+ .all();
1053
+
1054
+ const itemSourceMap = new Map<string, string[]>();
1055
+ for (const s of allSources) {
1056
+ const existing = itemSourceMap.get(s.memoryItemId);
1057
+ if (existing) existing.push(s.messageId);
1058
+ else itemSourceMap.set(s.memoryItemId, [s.messageId]);
1059
+ }
1060
+
1061
+ for (let i = candidates.length - 1; i >= 0; i--) {
1062
+ const c = candidates[i];
1063
+ if (c.type !== "item") continue;
1064
+ const sourceMessageIds = itemSourceMap.get(c.id);
1065
+ if (!sourceMessageIds || sourceMessageIds.length === 0) continue;
1066
+ if (sourceMessageIds.every((mid) => inContextMessageIds.has(mid))) {
1067
+ candidates.splice(i, 1);
1068
+ }
1069
+ }
1070
+ } catch (err) {
1071
+ log.warn(
1072
+ { err },
1073
+ "Failed to filter in-context serendipity items; skipping",
1074
+ );
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Sample random active memory items for serendipitous recall — items
1080
+ * the user didn't ask about but might spark unexpected connections.
1081
+ *
1082
+ * Queries SQLite for random active items not already in the candidate pool,
1083
+ * then selects up to `count` items with probability proportional to their
1084
+ * importance value (importance-weighted sampling).
1085
+ *
1086
+ * Items with importance >= MIN_SERENDIPITY_IMPORTANCE are eligible, as are
1087
+ * legacy items with NULL importance (not yet backfilled). This ensures
1088
+ * genuinely significant memories and pre-importance-era items can both
1089
+ * surface as echoes.
1090
+ */
1091
+ const MIN_SERENDIPITY_IMPORTANCE = 0.7;
1092
+
1093
+ function sampleSerendipityItems(
1094
+ existingCandidates: TieredCandidate[],
1095
+ count: number,
1096
+ scopeIds?: string[],
1097
+ ): TieredCandidate[] {
1098
+ if (count <= 0) return [];
1099
+
1100
+ try {
1101
+ const db = getDb();
1102
+
1103
+ // Collect IDs of item candidates already in the filtered set to exclude them
1104
+ const existingItemIds = existingCandidates
1105
+ .filter((c) => c.type === "item")
1106
+ .map((c) => c.id);
1107
+
1108
+ const RANDOM_POOL_SIZE = 10;
1109
+
1110
+ // Build scope condition: match allowed scopes, or default to 'default'
1111
+ // when no scope filter is set (prevents leaking private-scope items)
1112
+ const scopeCondition = scopeIds
1113
+ ? inArray(memoryItems.scopeId, scopeIds)
1114
+ : eq(memoryItems.scopeId, "default");
1115
+
1116
+ const importanceFloor = sql`(${memoryItems.importance} >= ${MIN_SERENDIPITY_IMPORTANCE} OR ${memoryItems.importance} IS NULL)`;
1117
+
1118
+ const baseConditions =
1119
+ existingItemIds.length > 0
1120
+ ? and(
1121
+ eq(memoryItems.status, "active"),
1122
+ scopeCondition,
1123
+ importanceFloor,
1124
+ notInArray(memoryItems.id, existingItemIds),
1125
+ )
1126
+ : and(
1127
+ eq(memoryItems.status, "active"),
1128
+ scopeCondition,
1129
+ importanceFloor,
1130
+ );
1131
+
1132
+ // Use rowid-probe sampling instead of ORDER BY RANDOM() to avoid a
1133
+ // full-table sort whose cost grows linearly with memory_items size.
1134
+ // Strategy: get the rowid range, generate random rowids, and probe for
1135
+ // the nearest eligible row with `rowid >= ?`. Each probe is O(log n)
1136
+ // via B-tree lookup, so total cost is O(k·log n) instead of O(n·log n).
1137
+ const range = db
1138
+ .select({
1139
+ minRowid: sql<number>`MIN(rowid)`,
1140
+ maxRowid: sql<number>`MAX(rowid)`,
1141
+ total: sql<number>`COUNT(*)`,
1142
+ })
1143
+ .from(memoryItems)
1144
+ .where(baseConditions)
1145
+ .get();
1146
+
1147
+ if (!range || range.total === 0) return [];
1148
+
1149
+ const columns = {
1150
+ id: memoryItems.id,
1151
+ kind: memoryItems.kind,
1152
+ subject: memoryItems.subject,
1153
+ statement: memoryItems.statement,
1154
+ importance: memoryItems.importance,
1155
+ firstSeenAt: memoryItems.firstSeenAt,
1156
+ };
1157
+
1158
+ let rows;
1159
+ if (range.total <= RANDOM_POOL_SIZE) {
1160
+ // Few enough eligible rows — fetch all, no randomness needed at DB level
1161
+ rows = db
1162
+ .select(columns)
1163
+ .from(memoryItems)
1164
+ .where(baseConditions)
1165
+ .all();
1166
+ } else {
1167
+ // Probe random rowids in the eligible range
1168
+ const seen = new Set<string>();
1169
+ rows = [];
1170
+ const rowidSpan = range.maxRowid - range.minRowid + 1;
1171
+ const maxAttempts = RANDOM_POOL_SIZE * 5;
1172
+ for (let i = 0; i < maxAttempts && rows.length < RANDOM_POOL_SIZE; i++) {
1173
+ const randomRowid =
1174
+ range.minRowid + Math.floor(Math.random() * rowidSpan);
1175
+ const row = db
1176
+ .select(columns)
1177
+ .from(memoryItems)
1178
+ .where(and(baseConditions, sql`rowid >= ${randomRowid}`))
1179
+ .orderBy(sql`rowid`)
1180
+ .limit(1)
1181
+ .get();
1182
+ if (row && !seen.has(row.id)) {
1183
+ seen.add(row.id);
1184
+ rows.push(row);
1185
+ }
1186
+ }
1187
+ }
1188
+
1189
+ if (rows.length === 0) return [];
1190
+
1191
+ // Importance-weighted sampling: sort by importance * random() descending
1192
+ // and take the top `count` items
1193
+ const weighted = rows
1194
+ .map((row) => ({
1195
+ row,
1196
+ score: (row.importance ?? 0.5) * Math.random(),
1197
+ }))
1198
+ .sort((a, b) => b.score - a.score)
1199
+ .slice(0, count);
1200
+
1201
+ // Convert to Candidate-compatible objects
1202
+ return weighted.map(
1203
+ ({ row }): TieredCandidate => ({
1204
+ type: "item",
1205
+ id: row.id,
1206
+ key: `item:${row.id}`,
1207
+ kind: row.kind,
1208
+ text: row.statement,
1209
+ source: "semantic",
1210
+ importance: row.importance ?? 0.5,
1211
+ confidence: 1,
1212
+ semantic: 0,
1213
+ recency: 0,
1214
+ finalScore: 0,
1215
+ createdAt: row.firstSeenAt,
1216
+ }),
1217
+ );
1218
+ } catch (err) {
1219
+ log.warn({ err }, "Failed to sample serendipity items");
1220
+ return [];
1221
+ }
1222
+ }
1223
+
816
1224
  /**
817
1225
  * Inject memory recall as a text content block prepended to the last user
818
1226
  * message. This follows the same pattern as workspace, temporal, and other