@vellumai/assistant 0.5.13 → 0.5.14

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,8 +1,9 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
  import type { AssistantConfig } from "../config/types.js";
3
3
  import { getLogger } from "../util/logger.js";
4
- import { rawRun } from "./db.js";
4
+ import { rawAll, rawRun } from "./db.js";
5
5
  import { backfillJob } from "./job-handlers/backfill.js";
6
+ import { batchExtractJob } from "./job-handlers/batch-extraction.js";
6
7
  import {
7
8
  cleanupStaleSupersededItemsJob,
8
9
  pruneOldConversationsJob,
@@ -21,8 +22,8 @@ import {
21
22
  deleteQdrantVectorsJob,
22
23
  rebuildIndexJob,
23
24
  } from "./job-handlers/index-maintenance.js";
25
+ import { journalCarryForwardJob } from "./job-handlers/journal-carry-forward.js";
24
26
  import { mediaProcessingJob } from "./job-handlers/media-processing.js";
25
- import { buildConversationSummaryJob } from "./job-handlers/summarization.js";
26
27
  import {
27
28
  BackendUnavailableError,
28
29
  classifyError,
@@ -34,6 +35,7 @@ import {
34
35
  completeMemoryJob,
35
36
  deferMemoryJob,
36
37
  enqueueCleanupStaleSupersededItemsJob,
38
+ enqueueMemoryJob,
37
39
  enqueuePruneOldConversationsJob,
38
40
  failMemoryJob,
39
41
  failStalledJobs,
@@ -58,6 +60,31 @@ export function startMemoryJobsWorker(): MemoryJobsWorker {
58
60
  log.info({ recovered }, "Recovered stale running memory jobs");
59
61
  }
60
62
 
63
+ // Startup recovery: enqueue batch_extract for conversations with pending
64
+ // unextracted messages (e.g. after a crash mid-conversation).
65
+ try {
66
+ const pendingRows = rawAll<{ key: string; value: string }>(
67
+ `SELECT key, value FROM memory_checkpoints WHERE key LIKE 'batch_extract:%:pending_count' AND CAST(value AS INTEGER) > 0`,
68
+ );
69
+ for (const row of pendingRows) {
70
+ // Extract conversationId from key: "batch_extract:<conversationId>:pending_count"
71
+ const parts = row.key.split(":");
72
+ if (parts.length >= 3) {
73
+ const conversationId = parts.slice(1, -1).join(":");
74
+ enqueueMemoryJob("batch_extract", { conversationId });
75
+ log.info(
76
+ { conversationId, pendingCount: row.value },
77
+ "Recovered pending batch extraction on startup",
78
+ );
79
+ }
80
+ }
81
+ } catch (err) {
82
+ log.warn(
83
+ { err: err instanceof Error ? err.message : String(err) },
84
+ "Failed to recover pending batch extractions on startup",
85
+ );
86
+ }
87
+
61
88
  let stopped = false;
62
89
  let tickRunning = false;
63
90
  let timer: ReturnType<typeof setTimeout>;
@@ -137,20 +164,29 @@ export async function runMemoryJobsOnce(
137
164
  return 0;
138
165
  }
139
166
 
140
- // Group jobs by type so same-type jobs run sequentially (preventing
141
- // checkpoint races for backfill, etc.), while different types run concurrently.
142
- const jobsByType = new Map<string, MemoryJob[]>();
167
+ // Group jobs so they can run concurrently across independent work units.
168
+ // Jobs targeting different conversations (via payload.conversationId) are
169
+ // placed in separate groups and can run in parallel. Jobs targeting the
170
+ // same conversation, or global jobs without a conversationId (backfill,
171
+ // cleanup, rebuild_index), are grouped together and run sequentially to
172
+ // prevent checkpoint races.
173
+ const jobGroups = new Map<string, MemoryJob[]>();
143
174
  for (const job of jobs) {
144
- let group = jobsByType.get(job.type);
175
+ const convId =
176
+ typeof job.payload.conversationId === "string"
177
+ ? job.payload.conversationId
178
+ : null;
179
+ const groupKey = convId ? `${job.type}:${convId}` : job.type;
180
+ let group = jobGroups.get(groupKey);
145
181
  if (!group) {
146
182
  group = [];
147
- jobsByType.set(job.type, group);
183
+ jobGroups.set(groupKey, group);
148
184
  }
149
185
  group.push(job);
150
186
  }
151
187
 
152
188
  let processed = 0;
153
- const typeGroups = [...jobsByType.values()];
189
+ const typeGroups = [...jobGroups.values()];
154
190
 
155
191
  // Run type groups concurrently using a task pool (up to workerConcurrency
156
192
  // active at a time). Unlike the old wave approach, a new group starts as
@@ -294,6 +330,9 @@ async function processJob(
294
330
  case "extract_items":
295
331
  await extractItemsJob(job);
296
332
  return;
333
+ case "batch_extract":
334
+ await batchExtractJob(job);
335
+ return;
297
336
  case "extract_entities":
298
337
  // Entity extraction has been removed — silently drop legacy jobs
299
338
  return;
@@ -304,7 +343,12 @@ async function processJob(
304
343
  pruneOldConversationsJob(job, config);
305
344
  return;
306
345
  case "build_conversation_summary":
307
- await buildConversationSummaryJob(job, config);
346
+ // Deprecated: conversation summaries are now produced as a side-effect
347
+ // of batch extraction. Silently skip legacy jobs.
348
+ log.debug(
349
+ { jobId: job.id },
350
+ "Skipping deprecated build_conversation_summary job — handled by batch extraction",
351
+ );
308
352
  return;
309
353
  case "backfill":
310
354
  await backfillJob(job, config);
@@ -331,6 +375,9 @@ async function processJob(
331
375
  case "embed_attachment":
332
376
  await embedAttachmentJob(job, config);
333
377
  return;
378
+ case "journal_carry_forward":
379
+ await journalCarryForwardJob(job);
380
+ return;
334
381
  case "generate_conversation_starters":
335
382
  await generateConversationStartersJob(job);
336
383
  return;
@@ -1,6 +1,7 @@
1
1
  import { and, asc, eq, gt, or } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
+ import { getConfig } from "../config/loader.js";
4
5
  import { getDb } from "./db.js";
5
6
  import { lifecycleEvents } from "./schema.js";
6
7
 
@@ -10,8 +11,9 @@ export interface LifecycleEvent {
10
11
  createdAt: number;
11
12
  }
12
13
 
13
- /** Record a lifecycle event (e.g. app_open, hatch). */
14
- export function recordLifecycleEvent(eventName: string): LifecycleEvent {
14
+ /** Record a lifecycle event (e.g. app_open, hatch). Returns null when usage data collection is disabled. */
15
+ export function recordLifecycleEvent(eventName: string): LifecycleEvent | null {
16
+ if (!getConfig().collectUsageData) return null;
15
17
  const db = getDb();
16
18
  const event: LifecycleEvent = {
17
19
  id: uuid(),
@@ -26,11 +26,12 @@ export function recordRequestLog(
26
26
  responsePayload: string,
27
27
  messageId?: string,
28
28
  provider?: string,
29
- ): void {
29
+ ): string {
30
30
  const db = getDb();
31
+ const id = uuid();
31
32
  db.insert(llmRequestLogs)
32
33
  .values({
33
- id: uuid(),
34
+ id,
34
35
  conversationId,
35
36
  messageId: messageId ?? null,
36
37
  provider: provider ?? null,
@@ -39,6 +40,7 @@ export function recordRequestLog(
39
40
  createdAt: Date.now(),
40
41
  })
41
42
  .run();
43
+ return id;
42
44
  }
43
45
 
44
46
  export function queryRequestLogs(
@@ -83,6 +85,42 @@ export function backfillMessageIdOnLogs(
83
85
  .run();
84
86
  }
85
87
 
88
+ /**
89
+ * Set `messageId` on specific log rows identified by their IDs.
90
+ * Unlike `backfillMessageIdOnLogs` (which updates all null-messageId rows
91
+ * for a conversation), this targets only the given log rows — safe for
92
+ * concurrent watch/assistant turns.
93
+ */
94
+ export function setMessageIdOnLogs(
95
+ logIds: string[],
96
+ messageId: string,
97
+ ): void {
98
+ if (logIds.length === 0) return;
99
+ const db = getDb();
100
+ db.update(llmRequestLogs)
101
+ .set({ messageId })
102
+ .where(inArray(llmRequestLogs.id, logIds))
103
+ .run();
104
+ }
105
+
106
+ /**
107
+ * Re-link LLM request logs from a set of source message IDs to a target
108
+ * message. Used during message consolidation so logs from deleted
109
+ * intermediate messages survive and remain queryable via the consolidated
110
+ * message.
111
+ */
112
+ export function relinkLlmRequestLogs(
113
+ fromMessageIds: string[],
114
+ toMessageId: string,
115
+ ): void {
116
+ if (fromMessageIds.length === 0) return;
117
+ const db = getDb();
118
+ db.update(llmRequestLogs)
119
+ .set({ messageId: toMessageId })
120
+ .where(inArray(llmRequestLogs.messageId, fromMessageIds))
121
+ .run();
122
+ }
123
+
86
124
  /**
87
125
  * Internal helper: query `llm_request_logs` for rows matching any of the
88
126
  * given message IDs, ordered by `createdAt ASC`. Uses the existing
@@ -42,6 +42,7 @@ export function recordUsageEvent(
42
42
  cacheReadInputTokens: event.cacheReadInputTokens,
43
43
  estimatedCostUsd: event.estimatedCostUsd,
44
44
  pricingStatus: event.pricingStatus,
45
+ llmCallCount: event.llmCallCount ?? 1,
45
46
  metadataJson: null,
46
47
  })
47
48
  .run();
@@ -213,7 +214,7 @@ export function getUsageTotals(range: UsageTimeRange): UsageTotals {
213
214
  COALESCE(SUM(cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
214
215
  COALESCE(SUM(cache_read_input_tokens), 0) AS total_cache_read_tokens,
215
216
  COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
216
- COUNT(*) AS event_count,
217
+ COALESCE(SUM(COALESCE(llm_call_count, 1)), 0) AS event_count,
217
218
  COUNT(CASE WHEN pricing_status = 'priced' THEN 1 END) AS priced_event_count,
218
219
  COUNT(CASE WHEN pricing_status = 'unpriced' THEN 1 END) AS unpriced_event_count
219
220
  FROM llm_usage_events
@@ -249,7 +250,7 @@ export function getUsageDayBuckets(range: UsageTimeRange): UsageDayBucket[] {
249
250
  COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
250
251
  COALESCE(SUM(output_tokens), 0) AS total_output_tokens,
251
252
  COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
252
- COUNT(*) AS event_count
253
+ COALESCE(SUM(COALESCE(llm_call_count, 1)), 0) AS event_count
253
254
  FROM llm_usage_events
254
255
  WHERE created_at >= ?1 AND created_at <= ?2
255
256
  GROUP BY date
@@ -292,7 +293,7 @@ export function getUsageGroupBreakdown(
292
293
  COALESCE(SUM(cache_creation_input_tokens), 0) AS total_cache_creation_tokens,
293
294
  COALESCE(SUM(cache_read_input_tokens), 0) AS total_cache_read_tokens,
294
295
  COALESCE(SUM(estimated_cost_usd), 0) AS total_estimated_cost_usd,
295
- COUNT(*) AS event_count
296
+ COALESCE(SUM(COALESCE(llm_call_count, 1)), 0) AS event_count
296
297
  FROM llm_usage_events
297
298
  WHERE created_at >= ?1 AND created_at <= ?2
298
299
  GROUP BY ${column}
@@ -0,0 +1,71 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+ import { tableHasColumn } from "./schema-introspection.js";
4
+ import { withCrashRecovery } from "./validate-migration-state.js";
5
+
6
+ /**
7
+ * Add enrichment columns to canonical_guardian_requests for guardian
8
+ * approval UX:
9
+ *
10
+ * - command_preview: truncated command/input preview
11
+ * - risk_level: "low", "medium", "high"
12
+ * - activity_text: LLM's explanation of why it's calling the tool
13
+ * - execution_target: "sandbox" or "host"
14
+ *
15
+ * All columns are nullable TEXT — existing rows default to NULL.
16
+ */
17
+ export function migrateGuardianRequestEnrichmentColumns(
18
+ database: DrizzleDb,
19
+ ): void {
20
+ withCrashRecovery(
21
+ database,
22
+ "migration_guardian_request_enrichment_columns_v1",
23
+ () => {
24
+ const raw = getSqliteFrom(database);
25
+
26
+ if (
27
+ !tableHasColumn(
28
+ database,
29
+ "canonical_guardian_requests",
30
+ "command_preview",
31
+ )
32
+ ) {
33
+ raw.exec(
34
+ /*sql*/ `ALTER TABLE canonical_guardian_requests ADD COLUMN command_preview TEXT`,
35
+ );
36
+ }
37
+
38
+ if (
39
+ !tableHasColumn(database, "canonical_guardian_requests", "risk_level")
40
+ ) {
41
+ raw.exec(
42
+ /*sql*/ `ALTER TABLE canonical_guardian_requests ADD COLUMN risk_level TEXT`,
43
+ );
44
+ }
45
+
46
+ if (
47
+ !tableHasColumn(
48
+ database,
49
+ "canonical_guardian_requests",
50
+ "activity_text",
51
+ )
52
+ ) {
53
+ raw.exec(
54
+ /*sql*/ `ALTER TABLE canonical_guardian_requests ADD COLUMN activity_text TEXT`,
55
+ );
56
+ }
57
+
58
+ if (
59
+ !tableHasColumn(
60
+ database,
61
+ "canonical_guardian_requests",
62
+ "execution_target",
63
+ )
64
+ ) {
65
+ raw.exec(
66
+ /*sql*/ `ALTER TABLE canonical_guardian_requests ADD COLUMN execution_target TEXT`,
67
+ );
68
+ }
69
+ },
70
+ );
71
+ }
@@ -0,0 +1,20 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+ import { tableHasColumn } from "./schema-introspection.js";
4
+
5
+ /**
6
+ * Add llm_call_count column to llm_usage_events so each row records
7
+ * how many actual LLM API calls it represents (an exchange/turn may
8
+ * contain multiple tool-use iterations, each making a separate API call).
9
+ *
10
+ * Nullable INTEGER — existing rows default to NULL and are treated as 1
11
+ * by the aggregation queries via COALESCE.
12
+ */
13
+ export function migrateUsageLlmCallCount(database: DrizzleDb): void {
14
+ const raw = getSqliteFrom(database);
15
+ if (!tableHasColumn(database, "llm_usage_events", "llm_call_count")) {
16
+ raw.exec(
17
+ /*sql*/ `ALTER TABLE llm_usage_events ADD COLUMN llm_call_count INTEGER`,
18
+ );
19
+ }
20
+ }
@@ -138,6 +138,8 @@ export { migrateMessagesConversationCreatedAtIndex } from "./196-messages-conver
138
138
  export { migrateStripIntegrationPrefixFromProviderKeys } from "./196-strip-integration-prefix-from-provider-keys.js";
139
139
  export { migrateOAuthProvidersBehaviorColumns } from "./197-oauth-providers-behavior-columns.js";
140
140
  export { migrateDropSetupSkillIdColumn } from "./198-drop-setup-skill-id-column.js";
141
+ export { migrateGuardianRequestEnrichmentColumns } from "./199-guardian-request-enrichment-columns.js";
142
+ export { migrateUsageLlmCallCount } from "./200-usage-llm-call-count.js";
141
143
  export {
142
144
  MIGRATION_REGISTRY,
143
145
  type MigrationRegistryEntry,
@@ -0,0 +1,83 @@
1
+ /**
2
+ * HyDE (Hypothetical Document Embeddings) query expansion for memory retrieval.
3
+ *
4
+ * Generates hypothetical memory documents that bridge the semantic gap between
5
+ * how users query ("moments that changed everything") and how memories are
6
+ * actually stored ("Sidd gave Velissa a collar on March 24"). The expanded
7
+ * queries are embedded alongside the original query to improve recall.
8
+ */
9
+
10
+ import type { AssistantConfig } from "../config/types.js";
11
+ import {
12
+ extractText,
13
+ getConfiguredProvider,
14
+ userMessage,
15
+ } from "../providers/provider-send-message.js";
16
+ import { getLogger } from "../util/logger.js";
17
+
18
+ const log = getLogger("memory-query-expansion");
19
+
20
+ const SYSTEM_PROMPT = `Generate 3 short hypothetical memory entries that would match this search query. Each should describe what a stored memory about this topic would contain — specific details, emotional context, relationship dynamics. Write from the perspective of stored memory items, not the query itself. Keep each under 80 words. Separate entries with ---`;
21
+
22
+ /**
23
+ * Generate hypothetical memory documents for a query using HyDE.
24
+ *
25
+ * Returns 1-3 hypothetical document strings that can be embedded alongside
26
+ * the original query to improve semantic recall. Returns `[]` on any error
27
+ * (provider unavailable, LLM failure, empty response) — the caller should
28
+ * fall back to the raw query only.
29
+ *
30
+ * The raw query is NOT included in the returned array; the caller handles
31
+ * that separately.
32
+ */
33
+ export async function expandQueryWithHyDE(
34
+ query: string,
35
+ _config: AssistantConfig,
36
+ signal?: AbortSignal,
37
+ ): Promise<string[]> {
38
+ try {
39
+ const provider = await getConfiguredProvider();
40
+ if (!provider) {
41
+ log.warn("No provider available for HyDE query expansion");
42
+ return [];
43
+ }
44
+
45
+ const response = await provider.sendMessage(
46
+ [userMessage(query)],
47
+ undefined,
48
+ SYSTEM_PROMPT,
49
+ {
50
+ config: {
51
+ modelIntent: "latency-optimized" as const,
52
+ },
53
+ signal,
54
+ },
55
+ );
56
+
57
+ const text = extractText(response);
58
+ if (!text) {
59
+ log.warn("Empty response from HyDE query expansion");
60
+ return [];
61
+ }
62
+
63
+ const entries = text
64
+ .split("---")
65
+ .map((entry) => entry.trim())
66
+ .filter((entry) => entry.length > 0)
67
+ .slice(0, 3);
68
+
69
+ if (entries.length === 0) {
70
+ log.warn("No entries parsed from HyDE query expansion response");
71
+ return [];
72
+ }
73
+
74
+ return entries;
75
+ } catch (err) {
76
+ if (err instanceof DOMException && err.name === "AbortError") throw err;
77
+ log.warn(
78
+ { err: err instanceof Error ? err.message : String(err) },
79
+ "HyDE query expansion failed",
80
+ );
81
+ return [];
82
+ }
83
+ }
@@ -1468,4 +1468,123 @@ describe("Memory Retriever Pipeline", () => {
1468
1468
  expect(result.mergedCount).toBe(0);
1469
1469
  });
1470
1470
  });
1471
+
1472
+ // -----------------------------------------------------------------------
1473
+ // Serendipity layer
1474
+ // -----------------------------------------------------------------------
1475
+
1476
+ describe("serendipity sampling", () => {
1477
+ test("samples random active items and renders them in <echoes>", async () => {
1478
+ const db = getDb();
1479
+ const now = Date.now();
1480
+ const convId = "conv-serendipity";
1481
+
1482
+ insertConversation(db, convId, now - 60_000);
1483
+ insertMessage(db, "msg-s-1", convId, "user", "hello", now - 50_000);
1484
+
1485
+ // Items sourced from a different conversation so in-context filtering
1486
+ // doesn't remove them (serendipity is cross-conversation recall).
1487
+ const otherConvId = "conv-serendipity-other";
1488
+ insertConversation(db, otherConvId, now - 120_000);
1489
+ insertMessage(db, "msg-s-other", otherConvId, "user", "other", now - 110_000);
1490
+
1491
+ // Insert several active items that are NOT returned by Qdrant
1492
+ for (let i = 1; i <= 5; i++) {
1493
+ insertItem(db, {
1494
+ id: `serendipity-item-${i}`,
1495
+ kind: "fact",
1496
+ subject: `topic ${i}`,
1497
+ statement: `Serendipity fact number ${i}`,
1498
+ importance: i * 0.15, // 0.15..0.75
1499
+ firstSeenAt: now - i * 10_000,
1500
+ });
1501
+ insertItemSource(db, `serendipity-item-${i}`, "msg-s-other", now - i * 10_000);
1502
+ }
1503
+
1504
+ // Qdrant returns nothing — no recalled candidates
1505
+ mockQdrantResults.length = 0;
1506
+
1507
+ const result = await buildMemoryRecall(
1508
+ "unrelated query",
1509
+ convId,
1510
+ TEST_CONFIG,
1511
+ );
1512
+
1513
+ expect(result.enabled).toBe(true);
1514
+ // No semantic hits, so no recalled candidates
1515
+ expect(result.mergedCount).toBe(0);
1516
+ // But serendipity items should appear in the injection
1517
+ expect(result.injectedText).toContain("<echoes>");
1518
+ expect(result.injectedText).toContain("</echoes>");
1519
+ // At most 3 serendipity items
1520
+ const itemMatches = result.injectedText.match(/<item /g);
1521
+ expect(itemMatches).toBeTruthy();
1522
+ expect(itemMatches!.length).toBeLessThanOrEqual(3);
1523
+ expect(itemMatches!.length).toBeGreaterThanOrEqual(1);
1524
+ // selectedCount includes serendipity items
1525
+ expect(result.selectedCount).toBeGreaterThan(0);
1526
+ });
1527
+
1528
+ test("excludes items already in the candidate pool from serendipity", async () => {
1529
+ const db = getDb();
1530
+ const now = Date.now();
1531
+ const convId = "conv-serendipity-excl";
1532
+
1533
+ insertConversation(db, convId, now - 60_000);
1534
+ insertMessage(db, "msg-se-1", convId, "user", "query about X", now - 50_000);
1535
+
1536
+ // This item will be returned by Qdrant as a recalled candidate
1537
+ insertItem(db, {
1538
+ id: "recalled-item",
1539
+ kind: "fact",
1540
+ subject: "X",
1541
+ statement: "Recalled fact about X",
1542
+ importance: 0.9,
1543
+ firstSeenAt: now - 30_000,
1544
+ });
1545
+ insertItemSource(db, "recalled-item", "msg-se-1", now - 30_000);
1546
+
1547
+ // Qdrant returns the recalled item
1548
+ mockQdrantResults.push({
1549
+ id: "qdrant-recalled",
1550
+ score: 0.9,
1551
+ payload: {
1552
+ target_type: "item",
1553
+ target_id: "recalled-item",
1554
+ text: "X: Recalled fact about X",
1555
+ created_at: now - 30_000,
1556
+ },
1557
+ });
1558
+
1559
+ const result = await buildMemoryRecall(
1560
+ "query about X",
1561
+ convId,
1562
+ TEST_CONFIG,
1563
+ );
1564
+
1565
+ expect(result.enabled).toBe(true);
1566
+ // The recalled item is in <recalled>, not in <echoes>
1567
+ if (result.injectedText.includes("<echoes>")) {
1568
+ // If echoes exists, the recalled item should NOT be duplicated there
1569
+ const echoesMatch = result.injectedText.match(
1570
+ /<echoes>([\s\S]*?)<\/echoes>/,
1571
+ );
1572
+ if (echoesMatch) {
1573
+ expect(echoesMatch[1]).not.toContain("recalled-item");
1574
+ }
1575
+ }
1576
+ });
1577
+
1578
+ test("no <echoes> section when no active items exist", async () => {
1579
+ // No items seeded at all
1580
+ const result = await buildMemoryRecall(
1581
+ "anything",
1582
+ "conv-empty-seren",
1583
+ TEST_CONFIG,
1584
+ );
1585
+
1586
+ expect(result.enabled).toBe(true);
1587
+ expect(result.injectedText).not.toContain("<echoes>");
1588
+ });
1589
+ });
1471
1590
  });