@vellumai/assistant 0.6.3 → 0.6.4

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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -12,9 +12,9 @@ This skill provides Gmail-specific tools. For cross-platform messaging (send, re
12
12
 
13
13
  ## Email Routing Priority
14
14
 
15
- When the user mentions "email" - sending, reading, checking, decluttering, drafting, or anything else - **always default to the user's own email (Gmail)** unless they explicitly ask about the assistant's own email address (e.g., "set up your email", "send from your address", "check your inbox"). The vast majority of email requests are about the user's Gmail, not the assistant's AgentMail address.
15
+ When the user mentions "email" - sending, reading, checking, decluttering, drafting, or anything else - **always default to the user's own email (Gmail)** unless they explicitly ask about the assistant's own email address (e.g., "set up your email", "send from your address", "check your inbox"). The vast majority of email requests are about the user's Gmail, not the assistant's @vellum.me address.
16
16
 
17
- Do not offer AgentMail as an option or mention it unless the user specifically asks. If Gmail is not connected, guide them through Gmail setup - do not suggest AgentMail as an alternative.
17
+ Do not offer the assistant's own email as an option unless the user specifically asks. If Gmail is not connected, guide them through Gmail setup.
18
18
 
19
19
  ## Communication Style
20
20
 
@@ -106,7 +106,7 @@ When searching Gmail, the query uses Gmail's search operators:
106
106
 
107
107
  When a user asks to declutter, clean up, or organize their email - start scanning immediately. Don't ask what kind of cleanup they want or request permission to read their inbox. Go straight to scanning - but once results are ready, always show them via `ui_show` and let the user choose actions before archiving or unsubscribing.
108
108
 
109
- **CRITICAL**: Never call `gmail_archive`, `gmail_unsubscribe`, or `messaging_archive_by_sender` unless the user has clicked an action button on the table for that specific batch. Each batch of results requires its own explicit user confirmation via the table UI. If the user says "keep going" or "keep decluttering," that means scan and present a new table - NOT auto-archive. Previous batch approvals do not carry forward, but **deselections DO carry forward**: when the user deselects senders from a cleanup table, the system records those deselections as user preferences. Before building the next cleanup table, check `<dynamic-user-profile>` for previously deselected senders and exclude them from future cleanup tables - the user already indicated they want to keep those.
109
+ **CRITICAL**: Never call `gmail_archive`, `gmail_unsubscribe`, or `messaging_archive_by_sender` unless the user has clicked an action button on the table for that specific batch. Each batch of results requires its own explicit user confirmation via the table UI. If the user says "keep going" or "keep decluttering," that means scan and present a new table - NOT auto-archive. Previous batch approvals do not carry forward, but **deselections DO carry forward**: when the user deselects senders from a cleanup table, call `gmail_preferences` with `action: "add_safelist"` to persist those senders. Before building the next cleanup table, call `gmail_preferences` with `action: "list"` and exclude safelisted senders from the table the user already indicated they want to keep those.
110
110
 
111
111
  ### Workflow
112
112
 
@@ -135,19 +135,65 @@ When a user asks to declutter, clean up, or organize their email - start scannin
135
135
  - If yes, call `gmail_filters` with `action: "create"` for each sender with `from` set to the sender's email and `remove_label_ids: ["INBOX"]`.
136
136
  - Then offer a recurring declutter schedule: "Want me to scan for new clutter monthly?" If yes, use `schedule_create` to set up a monthly declutter check.
137
137
 
138
+ ### Cold Outreach Cleanup
139
+
140
+ After the newsletter/promotions pass, offer to clean up cold outreach — unsolicited emails from senders without unsubscribe links. This catches sales pitches, recruiting spam, and mass outreach that newsletter filters miss.
141
+
142
+ 1. **Scan**: Call `gmail_outreach_scan` (default: last 90 days, senders without `List-Unsubscribe` headers). The scan includes a `has_prior_reply` flag per sender — true means the user has previously replied to that sender.
143
+ 2. **Filter out known contacts**: Exclude senders where `has_prior_reply: true` — these are conversations, not cold outreach. If the `contacts` skill is loaded, also cross-reference against Google Contacts and exclude matches.
144
+ 3. **Classify senders** using sample subjects, email domains, and message patterns. Categorize into:
145
+ - **Clear junk** (pre-select for archive): loan/LOC offers, generic SaaS pitches, mass marketing from unknown domains, senders with random/concatenated domain names
146
+ - **Sales outreach** (pre-select for archive): targeted product pitches with personalised subject lines ("Hi [name]", "for [company]"), outreach tool domains (apollo.io, outreach.io, lemlist.com, instantly.ai, etc.)
147
+ - **Potentially useful** (deselect / keep by default): recruiting, investor outreach, partnership proposals, vendor introductions that reference the user's specific product or role
148
+ - **Ambiguous** (deselect / keep by default): anything you're not confident about
149
+ 4. **Present as a table** following the same `ui_show` pattern as the newsletter workflow. Use two visual sections:
150
+ - Pre-selected rows: clear junk + sales outreach
151
+ - Deselected rows: potentially useful + ambiguous senders (user reviews these)
152
+ - **Caption**: "Cold outreach from the last 90 days (senders without unsubscribe links). Pre-selected senders look like spam or sales pitches. Deselected senders may be useful — review before archiving."
153
+ 5. **Archive on user action**: Same flow as newsletter cleanup — wait for surface action button click, then batch archive.
154
+
155
+ **Key principle**: Not all cold outreach is unwanted. Recruiting, investor, and partnership emails can be valuable. When uncertain, default to keeping the sender (deselected) and let the user decide.
156
+
157
+ ### Large Inbox Handling
158
+
159
+ When a scan returns `truncated: true` or `time_budget_exceeded: true`, the inbox has more messages than a single scan pass can cover. Split subsequent scans by date range to ensure full coverage:
160
+
161
+ ```
162
+ Pass 1: in:inbox older_than:90d (oldest backlog)
163
+ Pass 2: in:inbox newer_than:90d older_than:30d (recent months)
164
+ Pass 3: in:inbox newer_than:30d older_than:7d (recent weeks)
165
+ Pass 4: in:inbox newer_than:7d (this week)
166
+ ```
167
+
168
+ Merge results from all passes before presenting the final table. Each pass covers a smaller window, reducing per-scan message count and avoiding timeouts. Only split when a scan actually reports truncation — most inboxes are handled fine in a single pass.
169
+
138
170
  ### Edge Cases
139
171
 
140
172
  - **Zero results**: Tell the user "No newsletter emails found" and suggest broadening the query (e.g. removing the category filter or extending the date range)
141
173
  - **Unsubscribe failures**: Report per-sender success/failure; the existing `gmail_unsubscribe` tool handles edge cases
142
- - **Truncation handling**: The scan covers up to 5,000 messages by default (cap 10,000). If `truncated` is true, the top senders are still captured - don't offer to scan more. Tell the user: "Scanned [N] messages - here are your top senders."
143
- - **Time budget exceeded**: If the scan returns `time_budget_exceeded: true`, present whatever results were collected. Do not retry or continue - the partial results are useful as-is.
174
+ - **Truncation handling**: The scan covers up to 5,000 messages by default (cap 10,000). If `truncated` is true, the top senders are still captured. Offer to run additional date-range passes to cover the remaining messages (see Large Inbox Handling above).
175
+ - **Time budget exceeded**: If the scan returns `time_budget_exceeded: true`, present whatever results were collected. Offer to run additional date-range passes for uncovered periods.
176
+
177
+ ## Cleanup Preferences (Blocklist & Safelist)
178
+
179
+ The `gmail_preferences` tool persists sender preferences across cleanup sessions:
180
+
181
+ - **Blocklist**: Sender emails archived in previous sessions. On future cleanups, pre-pass archive all blocklisted senders before scanning (use `gmail_archive` with `query: "from:email1 OR from:email2 ... in:inbox"`).
182
+ - **Safelist**: Sender emails the user explicitly deselected (chose to keep). Exclude these senders from future cleanup tables entirely.
183
+
184
+ ### Workflow integration
185
+
186
+ 1. **Before scanning**: Call `gmail_preferences` with `action: "list"`. If blocklisted senders exist, offer to auto-archive them first ("I have N previously archived senders — want me to clean those up first?"). Remove safelisted senders from scan results before presenting the table.
187
+ 2. **After archiving**: The blocklist is updated automatically when `gmail_archive` runs with `scan_id` + `sender_ids`.
188
+ 3. **After user deselects**: When the user deselects senders from a cleanup table, call `gmail_preferences` with `action: "add_safelist"` and the deselected sender emails.
189
+ 4. **User overrides**: If the user asks to stop blocking or stop keeping a sender, use `remove_blocklist` or `remove_safelist` accordingly.
144
190
 
145
191
  ## Scan ID
146
192
 
147
- Scan tools (`gmail_sender_digest`, `gmail_outreach_scan`) return a `scan_id` that references message IDs stored server-side. This keeps thousands of message IDs out of the conversation context. `gmail_outreach_scan` is a pure data aggregation tool - it finds senders without List-Unsubscribe headers (potential cold outreach) and does not use LLM classification.
193
+ Scan tools (`gmail_sender_digest`, `gmail_outreach_scan`) return a `scan_id` that references message IDs stored server-side. This keeps thousands of message IDs out of the conversation context. `gmail_outreach_scan` finds senders without List-Unsubscribe headers (potential cold outreach) and enriches each sender with `has_prior_reply` (whether the user has ever sent an email to that address). Use this signal to filter out legitimate correspondents before classifying cold outreach.
148
194
 
149
195
  - Pass `scan_id` + `sender_ids` to `gmail_archive` instead of `message_ids`
150
- - Scan results expire after **30 minutes** - if archiving fails with an expiration error, re-run the scan
196
+ - Scan results expire after **30 minutes**. When a scan expires (`resolved === null`), archiving automatically falls back to query-based archiving per sender. If sender IDs don't match the scan results (`resolved` is empty), the tool returns an error — re-run the scan to get fresh results.
151
197
  - Raw `message_ids` still work as a fallback for non-scan workflows
152
198
 
153
199
  ## Batch Operations
@@ -494,15 +494,15 @@
494
494
  },
495
495
  "max_messages": {
496
496
  "type": "number",
497
- "description": "Maximum messages to scan (default 5000, cap 10000)"
497
+ "description": "Maximum messages to scan (default 2000, cap 5000)"
498
498
  },
499
499
  "max_senders": {
500
500
  "type": "number",
501
- "description": "Maximum senders to return (default 50)"
501
+ "description": "Maximum senders to return (default 50, max 75)"
502
502
  },
503
503
  "page_token": {
504
504
  "type": "string",
505
- "description": "Resume token from a previous scan (rarely needed - scans now cover up to 10,000 messages in a single call)"
505
+ "description": "Resume token from a previous scan (rarely needed - scans now cover up to 5,000 messages in a single call)"
506
506
  },
507
507
  "activity": {
508
508
  "type": "string",
@@ -553,6 +553,36 @@
553
553
  },
554
554
  "executor": "tools/gmail-outreach-scan.ts",
555
555
  "execution_target": "host"
556
+ },
557
+ {
558
+ "name": "gmail_preferences",
559
+ "description": "Manage Gmail cleanup preferences (blocklist and safelist). The blocklist contains senders to auto-archive in future cleanups. The safelist contains senders the user wants to keep (excluded from cleanup tables).",
560
+ "category": "gmail",
561
+ "risk": "low",
562
+ "input_schema": {
563
+ "type": "object",
564
+ "properties": {
565
+ "action": {
566
+ "type": "string",
567
+ "enum": ["list", "add_blocklist", "add_safelist", "remove_blocklist", "remove_safelist"],
568
+ "description": "Preference action: list all preferences, or add/remove emails from blocklist or safelist"
569
+ },
570
+ "emails": {
571
+ "type": "array",
572
+ "items": {
573
+ "type": "string"
574
+ },
575
+ "description": "Email addresses to add or remove (required for add/remove actions)"
576
+ },
577
+ "activity": {
578
+ "type": "string",
579
+ "description": "Brief non-technical explanation of why this tool is being called"
580
+ }
581
+ },
582
+ "required": ["action"]
583
+ },
584
+ "executor": "tools/gmail-preferences-tool.ts",
585
+ "execution_target": "host"
556
586
  }
557
587
  ]
558
588
  }
@@ -8,12 +8,49 @@ import type {
8
8
  ToolContext,
9
9
  ToolExecutionResult,
10
10
  } from "../../../../tools/types.js";
11
+ import { addToBlocklist } from "./gmail-preferences.js";
11
12
  import { getSenderMessageIds } from "./scan-result-store.js";
12
13
  import { err, ok } from "./shared.js";
13
14
 
14
15
  const BATCH_MODIFY_LIMIT = 1000;
15
16
  const MAX_MESSAGES = 5000;
16
17
 
18
+ function decodeSenderEmail(senderId: string): string | null {
19
+ try {
20
+ return Buffer.from(senderId, "base64url").toString("utf-8");
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Persist archived sender emails to the blocklist for future sessions.
28
+ * Only runs when the archive was initiated via a validated scan_id path
29
+ * (not bare message_ids) to prevent unverified emails from being recorded.
30
+ */
31
+ function recordBlocklist(
32
+ scanId: string | undefined,
33
+ senderIds: string[] | undefined,
34
+ ): void {
35
+ if (!scanId || !senderIds?.length) return;
36
+ const archivedEmails: string[] = [];
37
+ for (const sid of senderIds) {
38
+ try {
39
+ const email = Buffer.from(sid, "base64url").toString("utf-8");
40
+ if (email.includes("@")) archivedEmails.push(email);
41
+ } catch {
42
+ // Skip undecodable sender IDs
43
+ }
44
+ }
45
+ if (archivedEmails.length > 0) {
46
+ try {
47
+ addToBlocklist(archivedEmails);
48
+ } catch {
49
+ // Non-fatal — preferences are best-effort
50
+ }
51
+ }
52
+ }
53
+
17
54
  export async function run(
18
55
  input: Record<string, unknown>,
19
56
  context: ToolContext,
@@ -28,9 +65,9 @@ export async function run(
28
65
  // Resolve message IDs via priority: query → scan_id+sender_ids → message_ids → message_id
29
66
  if (query) {
30
67
  // Query path requires surface action confirmation
31
- if (!context.triggeredBySurfaceAction) {
68
+ if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
32
69
  return err(
33
- "This tool requires user confirmation via a surface action. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
70
+ "This tool requires either a surface action or a scheduled task run with this tool in required_tools. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
34
71
  );
35
72
  }
36
73
 
@@ -83,24 +120,92 @@ export async function run(
83
120
  }
84
121
  } else if (scanId && senderIds?.length) {
85
122
  // Scan path requires surface action confirmation
86
- if (!context.triggeredBySurfaceAction) {
123
+ if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
87
124
  return err(
88
- "This tool requires user confirmation via a surface action. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
125
+ "This tool requires either a surface action or a scheduled task run with this tool in required_tools. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
89
126
  );
90
127
  }
91
128
 
92
129
  const resolved = getSenderMessageIds(scanId, senderIds);
93
- if (!resolved) {
130
+ if (resolved !== null && resolved.length > 0) {
131
+ messageIds = resolved;
132
+ } else if (resolved === null) {
133
+ // Scan expired or sender IDs unresolved — fall back to query-based archiving
134
+ const emails: string[] = [];
135
+ const undecodable: string[] = [];
136
+ for (const sid of senderIds) {
137
+ const email = decodeSenderEmail(sid);
138
+ if (email && email.includes("@")) {
139
+ emails.push(email);
140
+ } else {
141
+ undecodable.push(sid);
142
+ }
143
+ }
144
+
145
+ if (emails.length === 0) {
146
+ return err(
147
+ "Scan results have expired and sender IDs could not be decoded. Please re-run the scan.",
148
+ );
149
+ }
150
+
151
+ try {
152
+ const connection = await resolveOAuthConnection("google", {
153
+ account,
154
+ });
155
+ const allMessageIds: string[] = [];
156
+
157
+ for (const email of emails) {
158
+ const fallbackQuery = `from:"${email.replace(/"/g, "")}" in:inbox`;
159
+ let pageToken: string | undefined;
160
+ while (allMessageIds.length < MAX_MESSAGES) {
161
+ const listResp = await listMessages(
162
+ connection,
163
+ fallbackQuery,
164
+ Math.min(500, MAX_MESSAGES - allMessageIds.length),
165
+ pageToken,
166
+ );
167
+ const ids = (listResp.messages ?? []).map((m) => m.id);
168
+ if (ids.length === 0) break;
169
+ allMessageIds.push(...ids);
170
+ pageToken = listResp.nextPageToken ?? undefined;
171
+ if (!pageToken) break;
172
+ }
173
+ if (allMessageIds.length >= MAX_MESSAGES) break;
174
+ }
175
+
176
+ if (allMessageIds.length === 0) {
177
+ return ok("No inbox messages found for the selected senders.");
178
+ }
179
+
180
+ for (let i = 0; i < allMessageIds.length; i += BATCH_MODIFY_LIMIT) {
181
+ const chunk = allMessageIds.slice(i, i + BATCH_MODIFY_LIMIT);
182
+ await batchModifyMessages(connection, chunk, {
183
+ removeLabelIds: ["INBOX"],
184
+ });
185
+ }
186
+
187
+ const parts = [
188
+ `Archived ${allMessageIds.length} message(s) via query fallback (scan results had expired).`,
189
+ ];
190
+ if (undecodable.length > 0) {
191
+ parts.push(
192
+ `${undecodable.length} sender ID(s) could not be decoded and were skipped.`,
193
+ );
194
+ }
195
+ return ok(parts.join(" "));
196
+ } catch (e) {
197
+ return err(e instanceof Error ? e.message : String(e));
198
+ }
199
+ } else {
94
200
  return err(
95
- "Scan results have expired (30-minute window). Please re-run the scan to get fresh results.",
201
+ "The provided sender IDs do not match the scan results. Please re-run the scan.",
96
202
  );
97
203
  }
98
- messageIds = resolved;
99
204
  } else if (messageIds?.length) {
100
205
  // Batch message_ids path requires surface action confirmation
101
- if (!context.triggeredBySurfaceAction) {
206
+ if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
102
207
  return err(
103
- "This tool requires user confirmation via a surface action. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
208
+ "This tool requires either a surface action or a scheduled task run with this tool in required_tools. Present results in a selection table with action buttons and wait for the user to click before proceeding.",
104
209
  );
105
210
  }
106
211
  } else if (messageId) {
@@ -133,6 +238,7 @@ export async function run(
133
238
  await modifyMessage(connection, messageIds[0], {
134
239
  removeLabelIds: ["INBOX"],
135
240
  });
241
+ recordBlocklist(scanId, senderIds);
136
242
  return ok("Message archived.");
137
243
  }
138
244
 
@@ -142,6 +248,7 @@ export async function run(
142
248
  removeLabelIds: ["INBOX"],
143
249
  });
144
250
  }
251
+ recordBlocklist(scanId, senderIds);
145
252
  return ok(`Archived ${messageIds.length} message(s).`);
146
253
  } catch (e) {
147
254
  return err(e instanceof Error ? e.message : String(e));
@@ -11,6 +11,11 @@ import type {
11
11
  import { storeScanResult } from "./scan-result-store.js";
12
12
  import { err, ok } from "./shared.js";
13
13
 
14
+ function isRateLimitError(e: unknown): boolean {
15
+ if (!(e instanceof Error)) return false;
16
+ return /\b429\b/.test(e.message);
17
+ }
18
+
14
19
  const MAX_MESSAGES_CAP = 5000;
15
20
  const MAX_IDS_PER_SENDER = 5000;
16
21
  const MAX_SAMPLE_SUBJECTS = 3;
@@ -69,6 +74,8 @@ export async function run(
69
74
  const startTime = Date.now();
70
75
  const TIME_BUDGET_MS = 90_000;
71
76
 
77
+ let rateLimited = false;
78
+
72
79
  while (allMessageIds.length < maxMessages) {
73
80
  if (Date.now() - startTime > TIME_BUDGET_MS) {
74
81
  timeBudgetExceeded = true;
@@ -76,12 +83,17 @@ export async function run(
76
83
  break;
77
84
  }
78
85
  const pageSize = Math.min(100, maxMessages - allMessageIds.length);
79
- const listResp = await listMessages(
80
- connection,
81
- query,
82
- pageSize,
83
- pageToken,
84
- );
86
+ let listResp;
87
+ try {
88
+ listResp = await listMessages(connection, query, pageSize, pageToken);
89
+ } catch (e) {
90
+ if (isRateLimitError(e)) {
91
+ rateLimited = true;
92
+ truncated = true;
93
+ break;
94
+ }
95
+ throw e;
96
+ }
85
97
  const ids = (listResp.messages ?? []).map((m) => m.id);
86
98
  if (ids.length === 0) break;
87
99
  allMessageIds.push(...ids);
@@ -103,6 +115,17 @@ export async function run(
103
115
  }
104
116
 
105
117
  if (allMessageIds.length === 0) {
118
+ if (rateLimited) {
119
+ return ok(
120
+ JSON.stringify({
121
+ senders: [],
122
+ total_scanned: 0,
123
+ rate_limited: true,
124
+ truncated: true,
125
+ note: "Rate limited before any messages could be fetched. Try again later or reduce max_messages.",
126
+ }),
127
+ );
128
+ }
106
129
  return ok(
107
130
  JSON.stringify({
108
131
  senders: [],
@@ -112,7 +135,35 @@ export async function run(
112
135
  );
113
136
  }
114
137
 
115
- const messages = (await Promise.all(fetchPromises)).flat();
138
+ // Settle all fetch promises — collect successes and tolerate 429 failures.
139
+ const elapsedMs = Date.now() - startTime;
140
+ const settleDeadlineMs = Math.max(TIME_BUDGET_MS - elapsedMs, 5_000);
141
+ const deadlineRejection = new Promise<never>((_, reject) =>
142
+ setTimeout(
143
+ () => reject(new Error("fetch deadline exceeded")),
144
+ settleDeadlineMs,
145
+ ),
146
+ );
147
+ const settled = await Promise.allSettled(
148
+ fetchPromises.map((p) => Promise.race([p, deadlineRejection])),
149
+ );
150
+ const messages: GmailMessage[] = [];
151
+ for (const result of settled) {
152
+ if (result.status === "fulfilled") {
153
+ messages.push(...result.value);
154
+ } else if (isRateLimitError(result.reason)) {
155
+ rateLimited = true;
156
+ truncated = true;
157
+ } else if (
158
+ result.reason instanceof Error &&
159
+ result.reason.message === "fetch deadline exceeded"
160
+ ) {
161
+ timeBudgetExceeded = true;
162
+ truncated = true;
163
+ } else {
164
+ throw result.reason;
165
+ }
166
+ }
116
167
 
117
168
  // Aggregate all fetched messages by sender
118
169
  const senderMap = new Map<string, OutreachSenderAggregation>();
@@ -177,16 +228,91 @@ export async function run(
177
228
  }
178
229
  }
179
230
 
180
- // Sort by message count desc, take top N
231
+ // Sort by message count desc — over-fetch before enrichment, cap after
181
232
  const sorted = [...senderMap.values()]
182
233
  .sort((a, b) => b.messageCount - a.messageCount)
183
- .slice(0, maxSenders);
234
+ .slice(0, maxSenders * 3);
235
+
236
+ // Enrich with prior-reply signal: check if user has ever sent to each sender.
237
+ // Uses bounded concurrency (batches of 10) and AbortController to cancel
238
+ // in-flight requests when the time budget expires.
239
+ const ENRICHMENT_CONCURRENCY = 10;
240
+ const priorReplyMap = new Map<string, boolean>();
241
+ if (!rateLimited) {
242
+ const enrichmentBudgetMs = Math.max(
243
+ TIME_BUDGET_MS - (Date.now() - startTime),
244
+ 5_000,
245
+ );
246
+ const abortController = new AbortController();
247
+ const budgetTimer = setTimeout(
248
+ () => abortController.abort(),
249
+ enrichmentBudgetMs,
250
+ );
251
+
252
+ try {
253
+ // Process in waves to limit concurrency and stop on budget expiry
254
+ for (
255
+ let i = 0;
256
+ i < sorted.length && !abortController.signal.aborted;
257
+ i += ENRICHMENT_CONCURRENCY
258
+ ) {
259
+ const batch = sorted.slice(i, i + ENRICHMENT_CONCURRENCY);
260
+ const batchChecks = batch.map(async (s) => {
261
+ if (abortController.signal.aborted) return;
262
+ try {
263
+ const resp = await listMessages(
264
+ connection,
265
+ `from:me to:${s.email}`,
266
+ 1,
267
+ );
268
+ priorReplyMap.set(s.email, (resp.messages?.length ?? 0) > 0);
269
+ } catch {
270
+ // Non-fatal — default to safe direction (assume prior reply exists)
271
+ priorReplyMap.set(s.email, true);
272
+ }
273
+ });
274
+ await Promise.race([
275
+ Promise.all(batchChecks),
276
+ new Promise<void>((resolve) =>
277
+ abortController.signal.addEventListener(
278
+ "abort",
279
+ () => resolve(),
280
+ {
281
+ once: true,
282
+ },
283
+ ),
284
+ ),
285
+ ]);
286
+ }
287
+ } finally {
288
+ clearTimeout(budgetTimer);
289
+ }
290
+
291
+ // Default any un-enriched senders to safe direction
292
+ for (const s of sorted) {
293
+ if (!priorReplyMap.has(s.email)) {
294
+ priorReplyMap.set(s.email, true);
295
+ }
296
+ }
297
+ } else {
298
+ // Rate limited — default all to safe direction
299
+ for (const s of sorted) {
300
+ priorReplyMap.set(s.email, true);
301
+ }
302
+ }
184
303
 
185
- const senders = sorted.map((s) => ({
304
+ // Filter out senders with prior replies, then cap to maxSenders.
305
+ // This is the purpose of over-fetching (maxSenders * 3): enrich more
306
+ // candidates, discard those with existing replies, then take the top N.
307
+ const capped = sorted
308
+ .filter((s) => !priorReplyMap.get(s.email))
309
+ .slice(0, maxSenders);
310
+ const senders = capped.map((s) => ({
186
311
  id: Buffer.from(s.email).toString("base64url"),
187
312
  display_name: s.displayName || s.email.split("@")[0],
188
313
  email: s.email,
189
314
  message_count: s.messageCount,
315
+ has_prior_reply: priorReplyMap.get(s.email) ?? true,
190
316
  newest_message_id: s.newestMessageId,
191
317
  oldest_date: s.oldestDate,
192
318
  newest_date: s.newestDate,
@@ -196,7 +322,7 @@ export async function run(
196
322
 
197
323
  // Store message IDs server-side to keep them out of LLM context
198
324
  const scanId = storeScanResult(
199
- sorted.map((s) => ({
325
+ capped.map((s) => ({
200
326
  id: Buffer.from(s.email).toString("base64url"),
201
327
  messageIds: s.messageIds,
202
328
  newestMessageId: s.newestMessageId,
@@ -211,6 +337,7 @@ export async function run(
211
337
  total_scanned: allMessageIds.length,
212
338
  ...(truncated ? { truncated: true } : {}),
213
339
  ...(timeBudgetExceeded ? { time_budget_exceeded: true } : {}),
340
+ ...(rateLimited ? { rate_limited: true } : {}),
214
341
  note: "Scanned inbox for senders without List-Unsubscribe headers (potential cold outreach). Use gmail_archive and gmail_filters for cleanup.",
215
342
  }),
216
343
  );
@@ -0,0 +1,59 @@
1
+ import type {
2
+ ToolContext,
3
+ ToolExecutionResult,
4
+ } from "../../../../tools/types.js";
5
+ import {
6
+ addToBlocklist,
7
+ addToSafelist,
8
+ loadPreferences,
9
+ removeFromBlocklist,
10
+ removeFromSafelist,
11
+ } from "./gmail-preferences.js";
12
+ import { err, ok } from "./shared.js";
13
+
14
+ export async function run(
15
+ input: Record<string, unknown>,
16
+ _context: ToolContext,
17
+ ): Promise<ToolExecutionResult> {
18
+ const action = input.action as string;
19
+ const emails = input.emails as string[] | undefined;
20
+
21
+ switch (action) {
22
+ case "list": {
23
+ const prefs = loadPreferences();
24
+ return ok(
25
+ JSON.stringify({
26
+ blocklist_count: prefs.blocklist.length,
27
+ safelist_count: prefs.safelist.length,
28
+ blocklist: prefs.blocklist,
29
+ safelist: prefs.safelist,
30
+ }),
31
+ );
32
+ }
33
+ case "add_blocklist": {
34
+ if (!emails?.length) return err("emails is required for add_blocklist");
35
+ addToBlocklist(emails);
36
+ return ok(`Added ${emails.length} sender(s) to blocklist.`);
37
+ }
38
+ case "add_safelist": {
39
+ if (!emails?.length) return err("emails is required for add_safelist");
40
+ addToSafelist(emails);
41
+ return ok(`Added ${emails.length} sender(s) to safelist.`);
42
+ }
43
+ case "remove_blocklist": {
44
+ if (!emails?.length)
45
+ return err("emails is required for remove_blocklist");
46
+ removeFromBlocklist(emails);
47
+ return ok(`Removed ${emails.length} sender(s) from blocklist.`);
48
+ }
49
+ case "remove_safelist": {
50
+ if (!emails?.length) return err("emails is required for remove_safelist");
51
+ removeFromSafelist(emails);
52
+ return ok(`Removed ${emails.length} sender(s) from safelist.`);
53
+ }
54
+ default:
55
+ return err(
56
+ `Unknown action "${action}". Use list, add_blocklist, add_safelist, remove_blocklist, or remove_safelist.`,
57
+ );
58
+ }
59
+ }