@vellumai/assistant 0.5.15 → 0.6.0

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 (503) hide show
  1. package/ARCHITECTURE.md +3 -3
  2. package/Dockerfile +0 -3
  3. package/docs/architecture/integrations.md +15 -14
  4. package/knip.json +4 -1
  5. package/openapi.yaml +670 -122
  6. package/package.json +1 -1
  7. package/src/__tests__/actor-token-service.test.ts +68 -0
  8. package/src/__tests__/agent-loop.test.ts +0 -32
  9. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  10. package/src/__tests__/anthropic-provider.test.ts +57 -3
  11. package/src/__tests__/app-compiler.test.ts +120 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +5 -377
  13. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  14. package/src/__tests__/call-domain.test.ts +2 -6
  15. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  16. package/src/__tests__/call-recovery.test.ts +2 -6
  17. package/src/__tests__/call-routes-http.test.ts +2 -6
  18. package/src/__tests__/call-store.test.ts +2 -6
  19. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  20. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  21. package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
  22. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  23. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  24. package/src/__tests__/checker.test.ts +84 -3
  25. package/src/__tests__/clawhub.test.ts +54 -24
  26. package/src/__tests__/cli-command-risk-guard.test.ts +108 -6
  27. package/src/__tests__/cli-memory.test.ts +377 -0
  28. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
  29. package/src/__tests__/config-schema.test.ts +1 -3
  30. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  31. package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
  32. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  33. package/src/__tests__/contacts-tools.test.ts +31 -0
  34. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  35. package/src/__tests__/context-token-estimator.test.ts +175 -10
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  37. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  38. package/src/__tests__/conversation-attachments.test.ts +2 -6
  39. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  40. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  41. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  42. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  43. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  44. package/src/__tests__/conversation-error.test.ts +33 -2
  45. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  46. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  47. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  48. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  49. package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
  50. package/src/__tests__/conversation-slash-commands.test.ts +2 -6
  51. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  52. package/src/__tests__/conversation-store.test.ts +2 -6
  53. package/src/__tests__/conversation-usage.test.ts +3 -6
  54. package/src/__tests__/conversation-wipe.test.ts +11 -408
  55. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  56. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  57. package/src/__tests__/credential-security-e2e.test.ts +6 -1
  58. package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
  59. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
  60. package/src/__tests__/followup-tools.test.ts +2 -6
  61. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  62. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  63. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  64. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  65. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  66. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  67. package/src/__tests__/guardian-action-store.test.ts +2 -6
  68. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  69. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  70. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  71. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  72. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  73. package/src/__tests__/guardian-routing-invariants.test.ts +343 -6
  74. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  75. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  76. package/src/__tests__/heartbeat-service.test.ts +1 -3
  77. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  78. package/src/__tests__/injection-block.test.ts +154 -0
  79. package/src/__tests__/install-meta.test.ts +506 -0
  80. package/src/__tests__/install-skill-routing.test.ts +292 -0
  81. package/src/__tests__/intent-routing.test.ts +6 -18
  82. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  83. package/src/__tests__/invite-routes-http.test.ts +2 -6
  84. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  85. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  86. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  87. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  88. package/src/__tests__/llm-usage-store.test.ts +2 -6
  89. package/src/__tests__/log-export-workspace.test.ts +4 -34
  90. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
  91. package/src/__tests__/managed-store.test.ts +40 -21
  92. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  93. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  94. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  95. package/src/__tests__/messaging-send-tool.test.ts +6 -6
  96. package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
  97. package/src/__tests__/migration-export-http.test.ts +3 -34
  98. package/src/__tests__/migration-import-commit-http.test.ts +1 -29
  99. package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
  100. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
  101. package/src/__tests__/non-member-access-request.test.ts +2 -6
  102. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  103. package/src/__tests__/oauth-apps-routes.test.ts +120 -10
  104. package/src/__tests__/oauth-cli.test.ts +364 -2
  105. package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
  106. package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
  107. package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
  108. package/src/__tests__/oauth-providers-routes.test.ts +5 -2
  109. package/src/__tests__/oauth-store.test.ts +0 -5
  110. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  111. package/src/__tests__/outlook-attachments.test.ts +301 -0
  112. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  113. package/src/__tests__/outlook-categories.test.ts +212 -0
  114. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  115. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  116. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  117. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  118. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  119. package/src/__tests__/outlook-messaging-provider.test.ts +1071 -0
  120. package/src/__tests__/outlook-trash.test.ts +77 -0
  121. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  122. package/src/__tests__/path-policy.test.ts +2 -17
  123. package/src/__tests__/permission-types.test.ts +0 -1
  124. package/src/__tests__/platform-callback-registration.test.ts +7 -11
  125. package/src/__tests__/playbook-execution.test.ts +76 -80
  126. package/src/__tests__/playbook-tools.test.ts +5 -7
  127. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  128. package/src/__tests__/provider-error-scenarios.test.ts +21 -2
  129. package/src/__tests__/qdrant-manager.test.ts +68 -21
  130. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  131. package/src/__tests__/registry.test.ts +2 -2
  132. package/src/__tests__/require-fresh-approval.test.ts +64 -3
  133. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  134. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  135. package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
  136. package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
  137. package/src/__tests__/schedule-store.test.ts +2 -6
  138. package/src/__tests__/schedule-tools.test.ts +2 -6
  139. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  140. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  141. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  142. package/src/__tests__/search-skills-unified.test.ts +421 -0
  143. package/src/__tests__/secret-allowlist.test.ts +20 -35
  144. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  145. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  146. package/src/__tests__/sequence-store.test.ts +2 -6
  147. package/src/__tests__/server-history-render.test.ts +2 -6
  148. package/src/__tests__/shell-credential-ref.test.ts +0 -5
  149. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  150. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  151. package/src/__tests__/skill-load-feature-flag.test.ts +13 -54
  152. package/src/__tests__/skill-load-inline-command.test.ts +3 -65
  153. package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
  154. package/src/__tests__/skill-load-tool.test.ts +3 -67
  155. package/src/__tests__/skill-memory.test.ts +480 -195
  156. package/src/__tests__/skills-uninstall.test.ts +2 -2
  157. package/src/__tests__/skills.test.ts +23 -50
  158. package/src/__tests__/slack-channel-config.test.ts +2 -21
  159. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  160. package/src/__tests__/starter-bundle.test.ts +2 -8
  161. package/src/__tests__/stt-hints.test.ts +7 -2
  162. package/src/__tests__/system-prompt.test.ts +25 -45
  163. package/src/__tests__/task-compiler.test.ts +2 -27
  164. package/src/__tests__/task-management-tools.test.ts +2 -27
  165. package/src/__tests__/task-memory-cleanup.test.ts +173 -250
  166. package/src/__tests__/task-runner.test.ts +2 -27
  167. package/src/__tests__/task-scheduler.test.ts +2 -27
  168. package/src/__tests__/terminal-tools.test.ts +1 -17
  169. package/src/__tests__/test-preload.ts +3 -0
  170. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
  171. package/src/__tests__/tool-approval-handler.test.ts +4 -27
  172. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
  173. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
  174. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  175. package/src/__tests__/tool-executor.test.ts +0 -1
  176. package/src/__tests__/tool-grant-request-escalation.test.ts +4 -27
  177. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
  178. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  179. package/src/__tests__/trust-store.test.ts +10 -42
  180. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
  181. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +3 -27
  182. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -28
  183. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -28
  184. package/src/__tests__/trusted-contact-verification.test.ts +2 -28
  185. package/src/__tests__/turn-boundary-resolution.test.ts +2 -34
  186. package/src/__tests__/twilio-provider.test.ts +0 -16
  187. package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
  188. package/src/__tests__/twilio-routes.test.ts +0 -24
  189. package/src/__tests__/update-bulletin.test.ts +17 -89
  190. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -26
  191. package/src/__tests__/usage-routes.test.ts +2 -27
  192. package/src/__tests__/user-reference.test.ts +1 -5
  193. package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
  194. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
  195. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  196. package/src/__tests__/voice-invite-redemption.test.ts +2 -27
  197. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -30
  198. package/src/__tests__/voice-session-bridge.test.ts +2 -27
  199. package/src/__tests__/volume-security-guard.test.ts +2 -0
  200. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  201. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +4 -29
  202. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
  203. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +4 -29
  204. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  205. package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
  206. package/src/__tests__/workspace-policy.test.ts +1 -1
  207. package/src/acp/client-handler.ts +1 -2
  208. package/src/agent/attachments.ts +7 -2
  209. package/src/agent/image-optimize.ts +165 -0
  210. package/src/agent/loop.ts +1 -15
  211. package/src/bundler/app-compiler.ts +179 -2
  212. package/src/bundler/package-resolver.ts +3 -5
  213. package/src/cli/__tests__/notifications.test.ts +1 -24
  214. package/src/cli/cli-memory.ts +179 -0
  215. package/src/cli/commands/avatar.ts +3 -3
  216. package/src/cli/commands/config.ts +26 -13
  217. package/src/cli/commands/doctor.ts +2 -2
  218. package/src/cli/commands/memory.ts +41 -55
  219. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  220. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  221. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  222. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  223. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  224. package/src/cli/commands/oauth/connect.ts +26 -6
  225. package/src/cli/commands/oauth/mode.ts +7 -0
  226. package/src/cli/commands/oauth/providers.ts +49 -42
  227. package/src/cli/commands/oauth/shared.ts +39 -3
  228. package/src/cli/commands/platform/__tests__/connect.test.ts +3 -49
  229. package/src/cli/commands/platform/__tests__/disconnect.test.ts +3 -49
  230. package/src/cli/commands/platform/__tests__/status.test.ts +5 -55
  231. package/src/cli/commands/platform/index.ts +16 -16
  232. package/src/cli/commands/skills.ts +88 -16
  233. package/src/cli/commands/trust.ts +2 -2
  234. package/src/cli/lib/daemon-credential-client.ts +2 -3
  235. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  236. package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
  237. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  238. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  239. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  240. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  241. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  242. package/src/config/bundled-skills/messaging/SKILL.md +26 -19
  243. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  244. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  245. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  246. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  247. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  248. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  249. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  250. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  251. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  252. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  253. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  254. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  255. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  256. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  257. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  258. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  259. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  260. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  261. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  262. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  263. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  264. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  265. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  266. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  267. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  268. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  269. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  270. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  271. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  272. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  273. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  274. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  275. package/src/config/bundled-tool-registry.ts +56 -4
  276. package/src/config/env-registry.ts +15 -8
  277. package/src/config/feature-flag-registry.json +29 -116
  278. package/src/config/loader.ts +4 -0
  279. package/src/config/schemas/platform.ts +8 -0
  280. package/src/config/schemas/security.ts +0 -6
  281. package/src/config/schemas/services.ts +8 -0
  282. package/src/config/schemas/timeouts.ts +1 -1
  283. package/src/config/skills.ts +18 -7
  284. package/src/context/token-estimator.ts +25 -18
  285. package/src/context/window-manager.ts +32 -9
  286. package/src/credential-execution/approval-bridge.ts +0 -1
  287. package/src/credential-execution/process-manager.ts +3 -1
  288. package/src/daemon/config-watcher.ts +51 -0
  289. package/src/daemon/context-overflow-reducer.ts +46 -2
  290. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  291. package/src/daemon/conversation-agent-loop.ts +99 -63
  292. package/src/daemon/conversation-error.ts +31 -8
  293. package/src/daemon/conversation-lifecycle.ts +33 -0
  294. package/src/daemon/conversation-media-retry.ts +85 -7
  295. package/src/daemon/conversation-notifiers.ts +4 -1
  296. package/src/daemon/conversation-process.ts +1 -0
  297. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  298. package/src/daemon/conversation-usage.ts +1 -0
  299. package/src/daemon/conversation.ts +41 -2
  300. package/src/daemon/daemon-control.ts +8 -2
  301. package/src/daemon/handlers/shared.ts +22 -12
  302. package/src/daemon/handlers/skills.ts +423 -201
  303. package/src/daemon/lifecycle.ts +52 -4
  304. package/src/daemon/main.ts +5 -1
  305. package/src/daemon/message-types/conversations.ts +5 -1
  306. package/src/daemon/message-types/messages.ts +3 -1
  307. package/src/daemon/message-types/skills.ts +97 -36
  308. package/src/daemon/providers-setup.ts +7 -0
  309. package/src/daemon/server.ts +35 -22
  310. package/src/daemon/tool-side-effects.ts +27 -5
  311. package/src/events/domain-events.ts +1 -2
  312. package/src/heartbeat/heartbeat-service.ts +1 -0
  313. package/src/hooks/cli.ts +2 -2
  314. package/src/hooks/runner.ts +15 -38
  315. package/src/inbound/platform-callback-registration.ts +14 -14
  316. package/src/memory/admin.ts +11 -45
  317. package/src/memory/conversation-bootstrap.ts +2 -0
  318. package/src/memory/conversation-crud.ts +242 -348
  319. package/src/memory/conversation-group-migration.ts +157 -0
  320. package/src/memory/conversation-queries.ts +4 -2
  321. package/src/memory/db-init.ts +39 -3
  322. package/src/memory/embed.ts +73 -0
  323. package/src/memory/embedding-backend.ts +8 -14
  324. package/src/memory/embedding-runtime-manager.ts +12 -114
  325. package/src/memory/fingerprint.ts +2 -2
  326. package/src/memory/graph/bootstrap.ts +512 -0
  327. package/src/memory/graph/capability-seed.ts +297 -0
  328. package/src/memory/graph/consolidation.ts +691 -0
  329. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  330. package/src/memory/graph/decay.test.ts +208 -0
  331. package/src/memory/graph/decay.ts +195 -0
  332. package/src/memory/graph/extraction-job.ts +69 -0
  333. package/src/memory/graph/extraction.test.ts +936 -0
  334. package/src/memory/graph/extraction.ts +1254 -0
  335. package/src/memory/graph/graph-search.ts +266 -0
  336. package/src/memory/graph/image-ref-utils.ts +29 -0
  337. package/src/memory/graph/injection.test.ts +513 -0
  338. package/src/memory/graph/injection.ts +439 -0
  339. package/src/memory/graph/inspect.ts +534 -0
  340. package/src/memory/graph/narrative.ts +267 -0
  341. package/src/memory/graph/pattern-scan.ts +269 -0
  342. package/src/memory/graph/retriever.ts +1008 -0
  343. package/src/memory/graph/scoring.test.ts +548 -0
  344. package/src/memory/graph/scoring.ts +232 -0
  345. package/src/memory/graph/serendipity.ts +65 -0
  346. package/src/memory/graph/store.test.ts +1050 -0
  347. package/src/memory/graph/store.ts +699 -0
  348. package/src/memory/graph/tool-handlers.ts +426 -0
  349. package/src/memory/graph/tools.ts +141 -0
  350. package/src/memory/graph/triggers.test.ts +487 -0
  351. package/src/memory/graph/triggers.ts +223 -0
  352. package/src/memory/graph/types.ts +271 -0
  353. package/src/memory/group-crud.ts +191 -0
  354. package/src/memory/indexer.ts +37 -19
  355. package/src/memory/job-handlers/cleanup.ts +0 -53
  356. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  357. package/src/memory/job-handlers/embedding.test.ts +3 -27
  358. package/src/memory/job-handlers/embedding.ts +5 -31
  359. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  360. package/src/memory/job-handlers/summarization.ts +32 -17
  361. package/src/memory/job-utils.ts +1 -1
  362. package/src/memory/jobs-store.ts +50 -70
  363. package/src/memory/jobs-worker.ts +147 -112
  364. package/src/memory/llm-usage-store.ts +35 -2
  365. package/src/memory/message-content.ts +1 -0
  366. package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
  367. package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
  368. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  369. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  370. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  371. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  372. package/src/memory/migrations/index.ts +6 -0
  373. package/src/memory/migrations/registry.ts +8 -0
  374. package/src/memory/qdrant-client.ts +44 -17
  375. package/src/memory/qdrant-manager.ts +26 -5
  376. package/src/memory/schema/index.ts +1 -0
  377. package/src/memory/schema/memory-graph.ts +139 -0
  378. package/src/memory/schema/oauth.ts +1 -1
  379. package/src/memory/search/semantic.ts +47 -91
  380. package/src/memory/slack-thread-store.ts +17 -0
  381. package/src/memory/task-memory-cleanup.ts +28 -50
  382. package/src/messaging/providers/outlook/adapter.ts +200 -0
  383. package/src/messaging/providers/outlook/client.ts +610 -0
  384. package/src/messaging/providers/outlook/types.ts +201 -0
  385. package/src/notifications/adapters/macos.ts +1 -0
  386. package/src/notifications/adapters/slack.ts +1 -1
  387. package/src/notifications/copy-composer.ts +9 -0
  388. package/src/notifications/signal.ts +16 -0
  389. package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
  390. package/src/oauth/connect-orchestrator.ts +10 -3
  391. package/src/oauth/oauth-store.ts +10 -11
  392. package/src/oauth/provider-serializer.ts +3 -0
  393. package/src/oauth/provider-visibility.ts +16 -0
  394. package/src/oauth/seed-providers.ts +50 -17
  395. package/src/permissions/checker.ts +62 -9
  396. package/src/permissions/defaults.ts +4 -4
  397. package/src/permissions/types.ts +2 -4
  398. package/src/permissions/workspace-policy.ts +1 -1
  399. package/src/playbooks/playbook-compiler.ts +19 -18
  400. package/src/playbooks/types.ts +4 -3
  401. package/src/prompts/system-prompt.ts +6 -93
  402. package/src/prompts/templates/UPDATES.md +6 -0
  403. package/src/providers/anthropic/client.ts +47 -19
  404. package/src/providers/gemini/client.ts +1 -1
  405. package/src/providers/openai/client.ts +1 -1
  406. package/src/providers/registry.ts +1 -1
  407. package/src/providers/retry.ts +19 -3
  408. package/src/runtime/actor-trust-resolver.ts +5 -1
  409. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
  410. package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
  411. package/src/runtime/auth/route-policy.ts +7 -4
  412. package/src/runtime/guardian-reply-router.ts +10 -2
  413. package/src/runtime/http-server.ts +23 -3
  414. package/src/runtime/middleware/auth.ts +20 -0
  415. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  416. package/src/runtime/routes/attachment-routes.ts +106 -16
  417. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  418. package/src/runtime/routes/btw-routes.ts +8 -0
  419. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  420. package/src/runtime/routes/conversation-query-routes.ts +2 -58
  421. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  422. package/src/runtime/routes/debug-routes.ts +1 -1
  423. package/src/runtime/routes/global-search-routes.ts +21 -19
  424. package/src/runtime/routes/group-routes.ts +207 -0
  425. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  426. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  427. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  428. package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
  429. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  430. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  431. package/src/runtime/routes/memory-item-routes.test.ts +2 -31
  432. package/src/runtime/routes/memory-item-routes.ts +385 -341
  433. package/src/runtime/routes/oauth-apps.ts +18 -1
  434. package/src/runtime/routes/oauth-providers.ts +13 -1
  435. package/src/runtime/routes/schedule-routes.ts +2 -0
  436. package/src/runtime/routes/settings-routes.ts +1 -0
  437. package/src/runtime/routes/skills-routes.ts +103 -37
  438. package/src/runtime/routes/usage-routes.ts +19 -2
  439. package/src/runtime/routes/work-items-routes.test.ts +2 -27
  440. package/src/runtime/routes/workspace-routes.test.ts +3 -27
  441. package/src/schedule/scheduler.ts +8 -1
  442. package/src/security/oauth2.ts +1 -1
  443. package/src/security/secret-allowlist.ts +4 -4
  444. package/src/security/secure-keys.ts +4 -8
  445. package/src/shared/provider-env-vars.ts +19 -0
  446. package/src/skills/catalog-cache.ts +5 -0
  447. package/src/skills/catalog-install.ts +15 -14
  448. package/src/skills/clawhub.ts +134 -154
  449. package/src/skills/install-meta.ts +208 -0
  450. package/src/skills/managed-store.ts +27 -16
  451. package/src/skills/skill-memory.ts +210 -96
  452. package/src/skills/skillssh-registry.ts +19 -17
  453. package/src/tasks/task-runner.ts +3 -1
  454. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  455. package/src/tools/browser/runtime-check.ts +3 -1
  456. package/src/tools/memory/register.ts +63 -46
  457. package/src/tools/permission-checker.ts +7 -19
  458. package/src/tools/shared/filesystem/image-read.ts +22 -85
  459. package/src/tools/skills/skill-script-runner.ts +1 -1
  460. package/src/tools/terminal/safe-env.ts +1 -0
  461. package/src/tools/tool-manifest.ts +3 -3
  462. package/src/util/browser.ts +25 -10
  463. package/src/util/bun-runtime.ts +172 -0
  464. package/src/util/device-id.ts +3 -65
  465. package/src/watcher/providers/outlook-calendar.ts +343 -0
  466. package/src/watcher/providers/outlook.ts +198 -0
  467. package/src/workspace/git-service.ts +27 -6
  468. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  469. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  470. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  471. package/src/workspace/migrations/registry.ts +6 -0
  472. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  473. package/src/__tests__/journal-context.test.ts +0 -268
  474. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  475. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  476. package/src/__tests__/memory-query-builder.test.ts +0 -59
  477. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  478. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  479. package/src/__tests__/memory-regressions.test.ts +0 -3696
  480. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  481. package/src/daemon/conversation-memory.ts +0 -207
  482. package/src/memory/conversation-starters-cadence.ts +0 -74
  483. package/src/memory/items-extractor.ts +0 -860
  484. package/src/memory/job-handlers/batch-extraction.ts +0 -741
  485. package/src/memory/job-handlers/extraction.ts +0 -40
  486. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -383
  487. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  488. package/src/memory/journal-memory.ts +0 -224
  489. package/src/memory/query-builder.ts +0 -47
  490. package/src/memory/query-expansion.ts +0 -83
  491. package/src/memory/retriever.test.ts +0 -1590
  492. package/src/memory/retriever.ts +0 -1323
  493. package/src/memory/search/formatting.test.ts +0 -140
  494. package/src/memory/search/formatting.ts +0 -262
  495. package/src/memory/search/mmr.ts +0 -136
  496. package/src/memory/search/ranking.ts +0 -15
  497. package/src/memory/search/staleness.ts +0 -40
  498. package/src/memory/search/tier-classifier.ts +0 -18
  499. package/src/memory/search/types.ts +0 -121
  500. package/src/prompts/journal-context.ts +0 -156
  501. package/src/tools/memory/definitions.ts +0 -69
  502. package/src/tools/memory/handlers.test.ts +0 -590
  503. package/src/tools/memory/handlers.ts +0 -434
@@ -0,0 +1,585 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { OutlookMessage } from "../messaging/providers/outlook/types.js";
4
+
5
+ // ── Mocks ─────────────────────────────────────────────────────────────────────
6
+
7
+ mock.module("../util/logger.js", () => ({
8
+ getLogger: () =>
9
+ new Proxy({} as Record<string, unknown>, {
10
+ get: () => () => {},
11
+ }),
12
+ }));
13
+
14
+ mock.module("../config/loader.js", () => ({
15
+ getConfig: () => ({
16
+ ui: {},
17
+ memory: {},
18
+ }),
19
+ }));
20
+
21
+ const mockListMessages = mock(
22
+ (): Promise<any> => Promise.resolve({ value: [] as OutlookMessage[] }),
23
+ );
24
+ const mockBatchGetMessages = mock(
25
+ (): Promise<any> => Promise.resolve([] as OutlookMessage[]),
26
+ );
27
+
28
+ mock.module("../messaging/providers/outlook/client.js", () => ({
29
+ listMessages: mockListMessages,
30
+ batchGetMessages: mockBatchGetMessages,
31
+ }));
32
+
33
+ const mockResolveOAuthConnection = mock(() =>
34
+ Promise.resolve({ request: () => Promise.resolve({}) }),
35
+ );
36
+
37
+ mock.module("../oauth/connection-resolver.js", () => ({
38
+ resolveOAuthConnection: mockResolveOAuthConnection,
39
+ }));
40
+
41
+ import {
42
+ clearScanStore,
43
+ getSenderMessageIds,
44
+ } from "../config/bundled-skills/gmail/tools/scan-result-store.js";
45
+ import { run as runOutreachScan } from "../config/bundled-skills/outlook/tools/outlook-outreach-scan.js";
46
+ import { run as runSenderDigest } from "../config/bundled-skills/outlook/tools/outlook-sender-digest.js";
47
+ import type { ToolContext } from "../tools/types.js";
48
+
49
+ const fakeContext = {} as ToolContext;
50
+
51
+ /** Helper to build an OutlookMessage with optional internet headers. */
52
+ function makeMessage(
53
+ id: string,
54
+ fromEmail: string,
55
+ fromName: string,
56
+ subject: string,
57
+ receivedDateTime: string,
58
+ headers?: Array<{ name: string; value: string }>,
59
+ ): OutlookMessage {
60
+ return {
61
+ id,
62
+ conversationId: `conv-${id}`,
63
+ subject,
64
+ bodyPreview: "",
65
+ body: { contentType: "text", content: "" },
66
+ from: {
67
+ emailAddress: { address: fromEmail, name: fromName },
68
+ },
69
+ toRecipients: [],
70
+ ccRecipients: [],
71
+ receivedDateTime,
72
+ isRead: true,
73
+ hasAttachments: false,
74
+ parentFolderId: "inbox",
75
+ categories: [],
76
+ flag: { flagStatus: "notFlagged" },
77
+ internetMessageHeaders: headers,
78
+ };
79
+ }
80
+
81
+ beforeEach(() => {
82
+ mockListMessages.mockReset();
83
+ mockBatchGetMessages.mockReset();
84
+ mockResolveOAuthConnection.mockReset();
85
+ mockResolveOAuthConnection.mockImplementation(() =>
86
+ Promise.resolve({ request: () => Promise.resolve({}) } as any),
87
+ );
88
+ clearScanStore();
89
+ });
90
+
91
+ // ── outlook_sender_digest ──────────────────────────────────────────────────────
92
+
93
+ describe("outlook_sender_digest", () => {
94
+ test("groups messages by sender and detects unsubscribe headers", async () => {
95
+ const msgs: OutlookMessage[] = [
96
+ makeMessage(
97
+ "m1",
98
+ "news@co.com",
99
+ "News Co",
100
+ "Newsletter #1",
101
+ "2025-01-15T10:00:00Z",
102
+ [{ name: "List-Unsubscribe", value: "<mailto:unsub@co.com>" }],
103
+ ),
104
+ makeMessage(
105
+ "m2",
106
+ "news@co.com",
107
+ "News Co",
108
+ "Newsletter #2",
109
+ "2025-01-20T10:00:00Z",
110
+ [{ name: "List-Unsubscribe", value: "<mailto:unsub@co.com>" }],
111
+ ),
112
+ makeMessage(
113
+ "m3",
114
+ "alice@example.com",
115
+ "Alice",
116
+ "Hello",
117
+ "2025-01-18T10:00:00Z",
118
+ ),
119
+ ];
120
+
121
+ mockListMessages.mockImplementationOnce(() =>
122
+ Promise.resolve({
123
+ value: msgs.map((m) => ({
124
+ id: m.id,
125
+ from: m.from,
126
+ receivedDateTime: m.receivedDateTime,
127
+ hasAttachments: false,
128
+ subject: m.subject,
129
+ })),
130
+ }),
131
+ );
132
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
133
+
134
+ const result = await runSenderDigest({}, fakeContext);
135
+ expect(result.isError).toBe(false);
136
+
137
+ const data = JSON.parse(result.content);
138
+ expect(data.scan_id).toBeDefined();
139
+ expect(data.total_scanned).toBe(3);
140
+ expect(data.senders).toHaveLength(2);
141
+
142
+ // news@co.com should be first (2 messages)
143
+ const newsSender = data.senders[0];
144
+ expect(newsSender.email).toBe("news@co.com");
145
+ expect(newsSender.message_count).toBe(2);
146
+ expect(newsSender.has_unsubscribe).toBe(true);
147
+ expect(newsSender.sample_subjects).toContain("Newsletter #1");
148
+
149
+ // alice@example.com should be second (1 message)
150
+ const aliceSender = data.senders[1];
151
+ expect(aliceSender.email).toBe("alice@example.com");
152
+ expect(aliceSender.message_count).toBe(1);
153
+ expect(aliceSender.has_unsubscribe).toBe(false);
154
+ });
155
+
156
+ test("returns empty results when no messages found", async () => {
157
+ mockListMessages.mockImplementationOnce(() =>
158
+ Promise.resolve({ value: [] }),
159
+ );
160
+
161
+ const result = await runSenderDigest({}, fakeContext);
162
+ expect(result.isError).toBe(false);
163
+
164
+ const data = JSON.parse(result.content);
165
+ expect(data.senders).toHaveLength(0);
166
+ expect(data.total_scanned).toBe(0);
167
+ });
168
+
169
+ test("stores scan result for subsequent retrieval", async () => {
170
+ const msgs: OutlookMessage[] = [
171
+ makeMessage(
172
+ "m1",
173
+ "sender@test.com",
174
+ "Sender",
175
+ "Sub1",
176
+ "2025-01-15T10:00:00Z",
177
+ ),
178
+ makeMessage(
179
+ "m2",
180
+ "sender@test.com",
181
+ "Sender",
182
+ "Sub2",
183
+ "2025-01-16T10:00:00Z",
184
+ ),
185
+ ];
186
+
187
+ mockListMessages.mockImplementationOnce(() =>
188
+ Promise.resolve({
189
+ value: msgs.map((m) => ({
190
+ id: m.id,
191
+ from: m.from,
192
+ receivedDateTime: m.receivedDateTime,
193
+ hasAttachments: false,
194
+ subject: m.subject,
195
+ })),
196
+ }),
197
+ );
198
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
199
+
200
+ const result = await runSenderDigest({}, fakeContext);
201
+ const data = JSON.parse(result.content);
202
+ const scanId = data.scan_id;
203
+ const senderId = data.senders[0].id;
204
+
205
+ // Verify we can retrieve message IDs from the scan store
206
+ const messageIds = getSenderMessageIds(scanId, [senderId]);
207
+ expect(messageIds).not.toBeNull();
208
+ expect(messageIds).toContain("m1");
209
+ expect(messageIds).toContain("m2");
210
+ });
211
+
212
+ test("paginates through multiple pages of messages", async () => {
213
+ const page1 = Array.from({ length: 100 }, (_, i) =>
214
+ makeMessage(
215
+ `p1-${i}`,
216
+ `sender${i % 5}@test.com`,
217
+ `Sender ${i % 5}`,
218
+ `Subject ${i}`,
219
+ `2025-01-${String(15 + (i % 10)).padStart(2, "0")}T10:00:00Z`,
220
+ ),
221
+ );
222
+ const page2 = Array.from({ length: 50 }, (_, i) =>
223
+ makeMessage(
224
+ `p2-${i}`,
225
+ `sender${i % 3}@test.com`,
226
+ `Sender ${i % 3}`,
227
+ `Subject ${100 + i}`,
228
+ `2025-01-${String(15 + (i % 10)).padStart(2, "0")}T10:00:00Z`,
229
+ ),
230
+ );
231
+
232
+ let callCount = 0;
233
+ mockListMessages.mockImplementation(() => {
234
+ callCount++;
235
+ if (callCount === 1) {
236
+ return Promise.resolve({
237
+ value: page1.map((m) => ({
238
+ id: m.id,
239
+ from: m.from,
240
+ receivedDateTime: m.receivedDateTime,
241
+ hasAttachments: false,
242
+ subject: m.subject,
243
+ })),
244
+ });
245
+ }
246
+ return Promise.resolve({
247
+ value: page2.map((m) => ({
248
+ id: m.id,
249
+ from: m.from,
250
+ receivedDateTime: m.receivedDateTime,
251
+ hasAttachments: false,
252
+ subject: m.subject,
253
+ })),
254
+ });
255
+ });
256
+
257
+ let batchCallCount = 0;
258
+ mockBatchGetMessages.mockImplementation(() => {
259
+ batchCallCount++;
260
+ return Promise.resolve(batchCallCount === 1 ? page1 : page2);
261
+ });
262
+
263
+ const result = await runSenderDigest({}, fakeContext);
264
+ expect(result.isError).toBe(false);
265
+
266
+ const data = JSON.parse(result.content);
267
+ expect(data.total_scanned).toBe(150);
268
+ expect(data.senders.length).toBeGreaterThan(0);
269
+ });
270
+
271
+ test("respects max_senders parameter", async () => {
272
+ const msgs: OutlookMessage[] = [
273
+ makeMessage("m1", "a@test.com", "A", "Sub1", "2025-01-15T10:00:00Z"),
274
+ makeMessage("m2", "b@test.com", "B", "Sub2", "2025-01-16T10:00:00Z"),
275
+ makeMessage("m3", "c@test.com", "C", "Sub3", "2025-01-17T10:00:00Z"),
276
+ ];
277
+
278
+ mockListMessages.mockImplementationOnce(() =>
279
+ Promise.resolve({
280
+ value: msgs.map((m) => ({
281
+ id: m.id,
282
+ from: m.from,
283
+ receivedDateTime: m.receivedDateTime,
284
+ hasAttachments: false,
285
+ subject: m.subject,
286
+ })),
287
+ }),
288
+ );
289
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
290
+
291
+ const result = await runSenderDigest({ max_senders: 2 }, fakeContext);
292
+ expect(result.isError).toBe(false);
293
+
294
+ const data = JSON.parse(result.content);
295
+ expect(data.senders).toHaveLength(2);
296
+ });
297
+
298
+ test("sender IDs are base64url-encoded email addresses", async () => {
299
+ const msgs: OutlookMessage[] = [
300
+ makeMessage(
301
+ "m1",
302
+ "test@example.com",
303
+ "Test",
304
+ "Sub1",
305
+ "2025-01-15T10:00:00Z",
306
+ ),
307
+ ];
308
+
309
+ mockListMessages.mockImplementationOnce(() =>
310
+ Promise.resolve({
311
+ value: msgs.map((m) => ({
312
+ id: m.id,
313
+ from: m.from,
314
+ receivedDateTime: m.receivedDateTime,
315
+ hasAttachments: false,
316
+ subject: m.subject,
317
+ })),
318
+ }),
319
+ );
320
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
321
+
322
+ const result = await runSenderDigest({}, fakeContext);
323
+ const data = JSON.parse(result.content);
324
+
325
+ const expectedId = Buffer.from("test@example.com").toString("base64url");
326
+ expect(data.senders[0].id).toBe(expectedId);
327
+ });
328
+ });
329
+
330
+ // ── outlook_outreach_scan ──────────────────────────────────────────────────────
331
+
332
+ describe("outlook_outreach_scan", () => {
333
+ test("filters out senders with List-Unsubscribe headers", async () => {
334
+ const msgs: OutlookMessage[] = [
335
+ makeMessage(
336
+ "m1",
337
+ "newsletter@co.com",
338
+ "Newsletter",
339
+ "Weekly Update",
340
+ "2025-01-15T10:00:00Z",
341
+ [{ name: "List-Unsubscribe", value: "<mailto:unsub@co.com>" }],
342
+ ),
343
+ makeMessage(
344
+ "m2",
345
+ "outreach@sales.com",
346
+ "Sales Bot",
347
+ "Can we chat?",
348
+ "2025-01-18T10:00:00Z",
349
+ ),
350
+ makeMessage(
351
+ "m3",
352
+ "outreach@sales.com",
353
+ "Sales Bot",
354
+ "Following up",
355
+ "2025-01-20T10:00:00Z",
356
+ ),
357
+ makeMessage(
358
+ "m4",
359
+ "friend@gmail.com",
360
+ "Friend",
361
+ "Hey!",
362
+ "2025-01-19T10:00:00Z",
363
+ ),
364
+ ];
365
+
366
+ mockListMessages.mockImplementationOnce(() =>
367
+ Promise.resolve({
368
+ value: msgs.map((m) => ({
369
+ id: m.id,
370
+ from: m.from,
371
+ receivedDateTime: m.receivedDateTime,
372
+ subject: m.subject,
373
+ })),
374
+ }),
375
+ );
376
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
377
+
378
+ const result = await runOutreachScan({}, fakeContext);
379
+ expect(result.isError).toBe(false);
380
+
381
+ const data = JSON.parse(result.content);
382
+ expect(data.scan_id).toBeDefined();
383
+
384
+ // newsletter@co.com should be excluded (has List-Unsubscribe)
385
+ const emails = data.senders.map((s: { email: string }) => s.email);
386
+ expect(emails).not.toContain("newsletter@co.com");
387
+
388
+ // outreach@sales.com and friend@gmail.com should be included
389
+ expect(emails).toContain("outreach@sales.com");
390
+ expect(emails).toContain("friend@gmail.com");
391
+
392
+ // outreach@sales.com should be first (2 messages)
393
+ const outreachSender = data.senders.find(
394
+ (s: { email: string }) => s.email === "outreach@sales.com",
395
+ );
396
+ expect(outreachSender.message_count).toBe(2);
397
+
398
+ // Outreach scan results should not have has_unsubscribe field
399
+ expect(outreachSender).not.toHaveProperty("has_unsubscribe");
400
+ });
401
+
402
+ test("returns empty results when no messages found", async () => {
403
+ mockListMessages.mockImplementationOnce(() =>
404
+ Promise.resolve({ value: [] }),
405
+ );
406
+
407
+ const result = await runOutreachScan({}, fakeContext);
408
+ expect(result.isError).toBe(false);
409
+
410
+ const data = JSON.parse(result.content);
411
+ expect(data.senders).toHaveLength(0);
412
+ expect(data.total_scanned).toBe(0);
413
+ });
414
+
415
+ test("stores scan result in shared scan store", async () => {
416
+ const msgs: OutlookMessage[] = [
417
+ makeMessage(
418
+ "m1",
419
+ "cold@outreach.com",
420
+ "Cold",
421
+ "Intro",
422
+ "2025-01-15T10:00:00Z",
423
+ ),
424
+ ];
425
+
426
+ mockListMessages.mockImplementationOnce(() =>
427
+ Promise.resolve({
428
+ value: msgs.map((m) => ({
429
+ id: m.id,
430
+ from: m.from,
431
+ receivedDateTime: m.receivedDateTime,
432
+ subject: m.subject,
433
+ })),
434
+ }),
435
+ );
436
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
437
+
438
+ const result = await runOutreachScan({}, fakeContext);
439
+ const data = JSON.parse(result.content);
440
+
441
+ const messageIds = getSenderMessageIds(data.scan_id, [data.senders[0].id]);
442
+ expect(messageIds).not.toBeNull();
443
+ expect(messageIds).toContain("m1");
444
+ });
445
+
446
+ test("respects time_range parameter", async () => {
447
+ mockListMessages.mockImplementationOnce(() =>
448
+ Promise.resolve({ value: [] }),
449
+ );
450
+
451
+ await runOutreachScan({ time_range: "30d" }, fakeContext);
452
+
453
+ // Verify the OData filter uses the correct date range
454
+ expect(mockListMessages).toHaveBeenCalledTimes(1);
455
+ const callArgs = mockListMessages.mock.calls[0] as unknown[];
456
+ const options = callArgs[1] as { filter?: string };
457
+ expect(options.filter).toMatch(/receivedDateTime ge /);
458
+
459
+ // The date in the filter should be approximately 30 days ago
460
+ const filterDate = options.filter!.replace("receivedDateTime ge ", "");
461
+ const parsedDate = new Date(filterDate);
462
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
463
+ // Allow 10 seconds of drift for test execution time
464
+ expect(
465
+ Math.abs(parsedDate.getTime() - thirtyDaysAgo.getTime()),
466
+ ).toBeLessThan(10_000);
467
+ });
468
+
469
+ test("excludes sender if ANY of their messages have List-Unsubscribe", async () => {
470
+ const msgs: OutlookMessage[] = [
471
+ // This sender has one message WITH unsubscribe and one WITHOUT
472
+ makeMessage(
473
+ "m1",
474
+ "mixed@co.com",
475
+ "Mixed",
476
+ "Promo",
477
+ "2025-01-15T10:00:00Z",
478
+ [{ name: "List-Unsubscribe", value: "<mailto:unsub@co.com>" }],
479
+ ),
480
+ makeMessage(
481
+ "m2",
482
+ "mixed@co.com",
483
+ "Mixed",
484
+ "Direct",
485
+ "2025-01-16T10:00:00Z",
486
+ ),
487
+ // This sender has no unsubscribe headers
488
+ makeMessage(
489
+ "m3",
490
+ "human@co.com",
491
+ "Human",
492
+ "Hi there",
493
+ "2025-01-17T10:00:00Z",
494
+ ),
495
+ ];
496
+
497
+ mockListMessages.mockImplementationOnce(() =>
498
+ Promise.resolve({
499
+ value: msgs.map((m) => ({
500
+ id: m.id,
501
+ from: m.from,
502
+ receivedDateTime: m.receivedDateTime,
503
+ subject: m.subject,
504
+ })),
505
+ }),
506
+ );
507
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
508
+
509
+ const result = await runOutreachScan({}, fakeContext);
510
+ const data = JSON.parse(result.content);
511
+
512
+ const emails = data.senders.map((s: { email: string }) => s.email);
513
+ // mixed@co.com should be excluded because at least one message has List-Unsubscribe
514
+ expect(emails).not.toContain("mixed@co.com");
515
+ expect(emails).toContain("human@co.com");
516
+ });
517
+
518
+ test("respects max_senders parameter", async () => {
519
+ const msgs: OutlookMessage[] = [
520
+ makeMessage("m1", "a@test.com", "A", "Sub1", "2025-01-15T10:00:00Z"),
521
+ makeMessage("m2", "b@test.com", "B", "Sub2", "2025-01-16T10:00:00Z"),
522
+ makeMessage("m3", "c@test.com", "C", "Sub3", "2025-01-17T10:00:00Z"),
523
+ ];
524
+
525
+ mockListMessages.mockImplementationOnce(() =>
526
+ Promise.resolve({
527
+ value: msgs.map((m) => ({
528
+ id: m.id,
529
+ from: m.from,
530
+ receivedDateTime: m.receivedDateTime,
531
+ subject: m.subject,
532
+ })),
533
+ }),
534
+ );
535
+ mockBatchGetMessages.mockImplementationOnce(() => Promise.resolve(msgs));
536
+
537
+ const result = await runOutreachScan({ max_senders: 1 }, fakeContext);
538
+ const data = JSON.parse(result.content);
539
+ expect(data.senders).toHaveLength(1);
540
+ });
541
+ });
542
+
543
+ // ── time budget ────────────────────────────────────────────────────────────────
544
+
545
+ describe("time budget enforcement", () => {
546
+ test("sender digest flags time_budget_exceeded when scan takes too long", async () => {
547
+ // Simulate a slow listMessages that exceeds the time budget
548
+ let callCount = 0;
549
+ mockListMessages.mockImplementation(async () => {
550
+ callCount++;
551
+ if (callCount === 1) {
552
+ // Return a page of results
553
+ return {
554
+ value: [
555
+ {
556
+ id: "m1",
557
+ from: { emailAddress: { address: "a@test.com", name: "A" } },
558
+ receivedDateTime: "2025-01-15T10:00:00Z",
559
+ hasAttachments: false,
560
+ subject: "Test",
561
+ },
562
+ ],
563
+ };
564
+ }
565
+ // On subsequent calls, sleep long enough to simulate timeout
566
+ // We can't actually wait 90s, so we test the mechanism by checking
567
+ // that the time check exists in the output format
568
+ return { value: [] };
569
+ });
570
+
571
+ mockBatchGetMessages.mockImplementation(() =>
572
+ Promise.resolve([
573
+ makeMessage("m1", "a@test.com", "A", "Test", "2025-01-15T10:00:00Z"),
574
+ ]),
575
+ );
576
+
577
+ const result = await runSenderDigest({}, fakeContext);
578
+ expect(result.isError).toBe(false);
579
+
580
+ const data = JSON.parse(result.content);
581
+ // The scan completed normally (under time budget) so these flags should not be set
582
+ expect(data.time_budget_exceeded).toBeUndefined();
583
+ expect(data.total_scanned).toBe(1);
584
+ });
585
+ });