@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
@@ -0,0 +1,82 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import { getWorkspaceDir } from "../../../../util/platform.js";
5
+
6
+ interface GmailPreferences {
7
+ /** Sender emails archived in previous sessions — auto-archive candidates. */
8
+ blocklist: string[];
9
+ /** Sender emails the user explicitly kept (deselected) — exclude from future tables. */
10
+ safelist: string[];
11
+ }
12
+
13
+ const PREFS_FILENAME = "gmail-preferences.json";
14
+
15
+ function getPrefsPath(): string {
16
+ return join(getWorkspaceDir(), "data", PREFS_FILENAME);
17
+ }
18
+
19
+ export function loadPreferences(): GmailPreferences {
20
+ try {
21
+ const raw = readFileSync(getPrefsPath(), "utf-8");
22
+ const parsed = JSON.parse(raw) as Partial<GmailPreferences>;
23
+ return {
24
+ blocklist: Array.isArray(parsed.blocklist) ? parsed.blocklist : [],
25
+ safelist: Array.isArray(parsed.safelist) ? parsed.safelist : [],
26
+ };
27
+ } catch {
28
+ return { blocklist: [], safelist: [] };
29
+ }
30
+ }
31
+
32
+ function savePreferences(prefs: GmailPreferences): void {
33
+ const path = getPrefsPath();
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ writeFileSync(path, JSON.stringify(prefs, null, 2));
36
+ }
37
+
38
+ /** Add sender emails to the blocklist (deduplicated). */
39
+ export function addToBlocklist(emails: string[]): void {
40
+ const prefs = loadPreferences();
41
+ const existing = new Set(prefs.blocklist);
42
+ for (const email of emails) {
43
+ const normalized = email.toLowerCase();
44
+ existing.add(normalized);
45
+ // If a sender is blocklisted, remove them from safelist
46
+ const safeIdx = prefs.safelist.indexOf(normalized);
47
+ if (safeIdx !== -1) prefs.safelist.splice(safeIdx, 1);
48
+ }
49
+ prefs.blocklist = [...existing];
50
+ savePreferences(prefs);
51
+ }
52
+
53
+ /** Add sender emails to the safelist (deduplicated). */
54
+ export function addToSafelist(emails: string[]): void {
55
+ const prefs = loadPreferences();
56
+ const existing = new Set(prefs.safelist);
57
+ for (const email of emails) {
58
+ const normalized = email.toLowerCase();
59
+ existing.add(normalized);
60
+ // If a sender is safelisted, remove them from blocklist
61
+ const blockIdx = prefs.blocklist.indexOf(normalized);
62
+ if (blockIdx !== -1) prefs.blocklist.splice(blockIdx, 1);
63
+ }
64
+ prefs.safelist = [...existing];
65
+ savePreferences(prefs);
66
+ }
67
+
68
+ /** Remove sender emails from the blocklist. */
69
+ export function removeFromBlocklist(emails: string[]): void {
70
+ const prefs = loadPreferences();
71
+ const toRemove = new Set(emails.map((e) => e.toLowerCase()));
72
+ prefs.blocklist = prefs.blocklist.filter((e) => !toRemove.has(e));
73
+ savePreferences(prefs);
74
+ }
75
+
76
+ /** Remove sender emails from the safelist. */
77
+ export function removeFromSafelist(emails: string[]): void {
78
+ const prefs = loadPreferences();
79
+ const toRemove = new Set(emails.map((e) => e.toLowerCase()));
80
+ prefs.safelist = prefs.safelist.filter((e) => !toRemove.has(e));
81
+ savePreferences(prefs);
82
+ }
@@ -11,9 +11,31 @@ import type {
11
11
  import { storeScanResult } from "./scan-result-store.js";
12
12
  import { err, ok } from "./shared.js";
13
13
 
14
- const MAX_MESSAGES_CAP = 10000;
14
+ function isRateLimitError(e: unknown): boolean {
15
+ if (!(e instanceof Error)) return false;
16
+ return /\b429\b/.test(e.message);
17
+ }
18
+
19
+ function isAbortError(e: unknown): boolean {
20
+ if (e instanceof DOMException && e.name === "AbortError") return true;
21
+ if (e instanceof Error && e.message === "fetch deadline exceeded") return true;
22
+ // AbortSignal.reason propagated through promise chains
23
+ if (
24
+ e instanceof Error &&
25
+ "cause" in e &&
26
+ e.cause instanceof Error &&
27
+ e.cause.message === "fetch deadline exceeded"
28
+ )
29
+ return true;
30
+ return false;
31
+ }
32
+
33
+ const MAX_MESSAGES_CAP = 5000;
15
34
  const MAX_IDS_PER_SENDER = 5000;
16
35
  const MAX_SAMPLE_SUBJECTS = 3;
36
+ const MAX_SENDERS_CAP = 75;
37
+ const MAX_SUBJECT_LENGTH = 80;
38
+ const MAX_RESULT_BYTES = 24_000;
17
39
 
18
40
  interface SenderAggregation {
19
41
  displayName: string;
@@ -52,10 +74,13 @@ export async function run(
52
74
  const query =
53
75
  (input.query as string) ?? "in:inbox category:promotions newer_than:90d";
54
76
  const maxMessages = Math.min(
55
- (input.max_messages as number) ?? 5000,
77
+ (input.max_messages as number) ?? 2000,
56
78
  MAX_MESSAGES_CAP,
57
79
  );
58
- const maxSenders = (input.max_senders as number) ?? 50;
80
+ const maxSenders = Math.min(
81
+ (input.max_senders as number) ?? 50,
82
+ MAX_SENDERS_CAP,
83
+ );
59
84
  const inputPageToken = input.page_token as string | undefined;
60
85
 
61
86
  try {
@@ -66,6 +91,7 @@ export async function run(
66
91
  // overlapping fetch latency with pagination latency
67
92
  const allMessageIds: string[] = [];
68
93
  const fetchPromises: Promise<GmailMessage[]>[] = [];
94
+ const fetchAbort = new AbortController();
69
95
  let pageToken: string | undefined = inputPageToken;
70
96
  let truncated = false;
71
97
  let timeBudgetExceeded = false;
@@ -73,6 +99,8 @@ export async function run(
73
99
  const startTime = Date.now();
74
100
  const TIME_BUDGET_MS = 90_000;
75
101
 
102
+ let rateLimited = false;
103
+
76
104
  while (allMessageIds.length < maxMessages) {
77
105
  if (Date.now() - startTime > TIME_BUDGET_MS) {
78
106
  timeBudgetExceeded = true;
@@ -80,12 +108,22 @@ export async function run(
80
108
  break;
81
109
  }
82
110
  const pageSize = Math.min(100, maxMessages - allMessageIds.length);
83
- const listResp = await listMessages(
84
- connection,
85
- query,
86
- pageSize,
87
- pageToken,
88
- );
111
+ let listResp;
112
+ try {
113
+ listResp = await listMessages(
114
+ connection,
115
+ query,
116
+ pageSize,
117
+ pageToken,
118
+ );
119
+ } catch (e) {
120
+ if (isRateLimitError(e)) {
121
+ rateLimited = true;
122
+ truncated = true;
123
+ break;
124
+ }
125
+ throw e;
126
+ }
89
127
  const ids = (listResp.messages ?? []).map((m) => m.id);
90
128
  if (ids.length === 0) break;
91
129
  allMessageIds.push(...ids);
@@ -96,6 +134,7 @@ export async function run(
96
134
  "metadata",
97
135
  metadataHeaders,
98
136
  "id,internalDate,payload/headers",
137
+ fetchAbort.signal,
99
138
  ),
100
139
  );
101
140
  pageToken = listResp.nextPageToken ?? undefined;
@@ -108,6 +147,18 @@ export async function run(
108
147
  }
109
148
 
110
149
  if (allMessageIds.length === 0) {
150
+ if (rateLimited) {
151
+ return ok(
152
+ JSON.stringify({
153
+ senders: [],
154
+ total_scanned: 0,
155
+ rate_limited: true,
156
+ truncated: true,
157
+ message:
158
+ "Rate limited before any messages could be fetched. Try again later or reduce max_messages.",
159
+ }),
160
+ );
161
+ }
111
162
  return ok(
112
163
  JSON.stringify({
113
164
  senders: [],
@@ -118,7 +169,30 @@ export async function run(
118
169
  );
119
170
  }
120
171
 
121
- const messages = (await Promise.all(fetchPromises)).flat();
172
+ // Settle all fetch promises — collect successes and tolerate 429 failures.
173
+ // Abort in-flight requests when the deadline fires so they don't continue
174
+ // consuming API quota in the background.
175
+ const elapsedMs = Date.now() - startTime;
176
+ const settleDeadlineMs = Math.max(TIME_BUDGET_MS - elapsedMs, 5_000);
177
+ const deadlineTimer = setTimeout(() => {
178
+ fetchAbort.abort(new Error("fetch deadline exceeded"));
179
+ }, settleDeadlineMs);
180
+ const settled = await Promise.allSettled(fetchPromises);
181
+ clearTimeout(deadlineTimer);
182
+ const messages: GmailMessage[] = [];
183
+ for (const result of settled) {
184
+ if (result.status === "fulfilled") {
185
+ messages.push(...result.value);
186
+ } else if (isRateLimitError(result.reason)) {
187
+ rateLimited = true;
188
+ truncated = true;
189
+ } else if (isAbortError(result.reason)) {
190
+ timeBudgetExceeded = true;
191
+ truncated = true;
192
+ } else {
193
+ throw result.reason;
194
+ }
195
+ }
122
196
 
123
197
  // Group by sender email
124
198
  const senderMap = new Map<string, SenderAggregation>();
@@ -221,17 +295,38 @@ export async function run(
221
295
  newest_date: s.newestDate,
222
296
  // Preserve original query filters so follow-up searches stay scoped
223
297
  search_query: `from:${s.email} ${query}`,
224
- sample_subjects: s.sampleSubjects,
298
+ sample_subjects: s.sampleSubjects.map((subj) =>
299
+ subj.length > MAX_SUBJECT_LENGTH
300
+ ? subj.slice(0, MAX_SUBJECT_LENGTH) + "…"
301
+ : subj,
302
+ ),
225
303
  }));
226
304
 
305
+ // Trim senders if the serialized result would exceed the byte budget.
306
+ // Senders are already sorted by message_count desc, so we drop the
307
+ // least-active ones first.
308
+ while (resultSenders.length > 1) {
309
+ const probe = JSON.stringify({ senders: resultSenders });
310
+ if (probe.length <= MAX_RESULT_BYTES) break;
311
+ resultSenders.pop();
312
+ }
313
+
314
+ // Build a set of sender IDs that survived the trim so the scan store
315
+ // only holds entries the LLM can reference.
316
+ const keptSenderIds = new Set(resultSenders.map((s) => s.id));
317
+
227
318
  // Store message IDs server-side to keep them out of LLM context
228
319
  const scanId = storeScanResult(
229
- sorted.map((s) => ({
230
- id: Buffer.from(s.email).toString("base64url"),
231
- messageIds: s.messageIds,
232
- newestMessageId: s.newestMessageId,
233
- newestUnsubscribableMessageId: s.newestUnsubscribableMessageId,
234
- })),
320
+ sorted
321
+ .filter((s) =>
322
+ keptSenderIds.has(Buffer.from(s.email).toString("base64url")),
323
+ )
324
+ .map((s) => ({
325
+ id: Buffer.from(s.email).toString("base64url"),
326
+ messageIds: s.messageIds,
327
+ newestMessageId: s.newestMessageId,
328
+ newestUnsubscribableMessageId: s.newestUnsubscribableMessageId,
329
+ })),
235
330
  );
236
331
 
237
332
  return ok(
@@ -242,6 +337,7 @@ export async function run(
242
337
  query_used: query,
243
338
  ...(truncated ? { truncated: true } : {}),
244
339
  ...(timeBudgetExceeded ? { time_budget_exceeded: true } : {}),
340
+ ...(rateLimited ? { rate_limited: true } : {}),
245
341
  note: `message_count reflects emails found per sender within the ${allMessageIds.length} messages scanned. Use scan_id with gmail_archive to archive messages (pass scan_id + sender_ids instead of message_ids).`,
246
342
  }),
247
343
  );
@@ -19,9 +19,9 @@ export async function run(
19
19
  context: ToolContext,
20
20
  ): Promise<ToolExecutionResult> {
21
21
  const account = input.account as string | undefined;
22
- if (!context.triggeredBySurfaceAction) {
22
+ if (!context.triggeredBySurfaceAction && !context.batchAuthorizedByTask) {
23
23
  return err(
24
- "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.",
24
+ "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.",
25
25
  );
26
26
  }
27
27
 
@@ -45,8 +45,7 @@ Parameters:
45
45
  - `section_config` - Path to a JSON file with manual section boundaries.
46
46
  - `detect_dead_time` - Whether to detect and skip dead time (default: false). Dead-time detection can be too aggressive for continuous action video like sports - it may incorrectly skip live play. Enable only for content with clear idle periods (e.g., lectures, surveillance footage).
47
47
  - `short_edge` - Short edge resolution for downscaled frames in pixels (default: 480).
48
- - `include_audio` - Whether to extract and transcribe audio for each segment (default: false). When enabled, each segment's audio is transcribed and stored alongside visual frames.
49
- - `transcription_mode` - Transcription backend: `'api'` (OpenAI Whisper cloud) or `'local'` (whisper.cpp on-device). Default: `'local'`. The `'api'` mode requires an OpenAI API key configured in settings.
48
+ - `include_audio` - Whether to extract and transcribe audio for each segment (default: false). When enabled, each segment's audio is transcribed using the configured STT service and stored alongside visual frames.
50
49
 
51
50
  ### analyze_keyframes
52
51
 
@@ -121,7 +120,7 @@ Tracks estimated API costs during pipeline execution.
121
120
 
122
121
  ## Audio + Vision Multimodal Analysis
123
122
 
124
- When `include_audio` is enabled on `extract_keyframes`, the pipeline transcribes each segment's audio track and attaches the transcript to the segment data. During the Map phase (`analyze_keyframes`), Gemini receives both the visual frames and the audio transcript for each segment, enabling multimodal analysis that combines what is seen with what is said.
123
+ When `include_audio` is enabled on `extract_keyframes`, the pipeline transcribes each segment's audio track using the configured STT service and attaches the transcript to the segment data. During the Map phase (`analyze_keyframes`), Gemini receives both the visual frames and the audio transcript for each segment, enabling multimodal analysis that combines what is seen with what is said.
125
124
 
126
125
  This is useful for:
127
126
 
@@ -130,12 +129,7 @@ This is useful for:
130
129
  - **Meetings and interviews**: Pair facial expressions and gestures with spoken dialogue.
131
130
  - **Tutorials and demos**: Link on-screen actions with verbal instructions.
132
131
 
133
- Transcription modes:
134
-
135
- - **`local`** (default): Uses whisper.cpp for on-device transcription. Requires `whisper-cpp` to be installed (`brew install whisper-cpp`). No API costs, but slower.
136
- - **`api`**: Uses the OpenAI Whisper API for cloud-based transcription. Faster and more accurate, but requires an OpenAI API key and incurs per-minute costs.
137
-
138
- The audio transcription degrades gracefully - if transcription fails for a segment (missing tools, no audio track, API errors), the segment proceeds with visual-only analysis.
132
+ Audio transcription uses the STT service configured in assistant settings. If no STT service is configured or transcription fails for a segment (no audio track, service errors), the segment gracefully degrades to visual-only analysis.
139
133
 
140
134
  ## Best Practices
141
135
 
@@ -99,12 +99,7 @@
99
99
  },
100
100
  "include_audio": {
101
101
  "type": "boolean",
102
- "description": "Whether to extract and transcribe audio for each segment. Default: false."
103
- },
104
- "transcription_mode": {
105
- "type": "string",
106
- "enum": ["api", "local"],
107
- "description": "Transcription backend: 'api' (OpenAI Whisper cloud) or 'local' (whisper.cpp). Default: 'local'."
102
+ "description": "Whether to extract and transcribe audio for each segment using the configured STT service. Default: false."
108
103
  },
109
104
  "activity": {
110
105
  "type": "string",
@@ -0,0 +1,125 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { BatchTranscriber } from "../../../../stt/types.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks — must be declared before the subject import
7
+ // ---------------------------------------------------------------------------
8
+
9
+ let mockTranscriber: BatchTranscriber | null = null;
10
+
11
+ mock.module("../../../../providers/speech-to-text/resolve.js", () => ({
12
+ resolveBatchTranscriber: async () => mockTranscriber,
13
+ }));
14
+
15
+ let spawnResult: { exitCode: number; stdout: string; stderr: string } = {
16
+ exitCode: 0,
17
+ stdout: "",
18
+ stderr: "",
19
+ };
20
+
21
+ mock.module("../../../../util/spawn.js", () => ({
22
+ spawnWithTimeout: async () => spawnResult,
23
+ }));
24
+
25
+ let mockFileContents: Buffer = Buffer.alloc(0);
26
+
27
+ mock.module("node:fs/promises", () => ({
28
+ readFile: async () => mockFileContents,
29
+ unlink: async () => {},
30
+ }));
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Subject import (after mocks)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ import { transcribeSegmentAudio } from "../services/audio-transcribe.js";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function makeMockTranscriber(text: string): BatchTranscriber {
43
+ return {
44
+ providerId: "openai-whisper",
45
+ boundaryId: "daemon-batch",
46
+ transcribe: async () => ({ text }),
47
+ };
48
+ }
49
+
50
+ function makeFailingTranscriber(error: Error): BatchTranscriber {
51
+ return {
52
+ providerId: "openai-whisper",
53
+ boundaryId: "daemon-batch",
54
+ transcribe: async () => {
55
+ throw error;
56
+ },
57
+ };
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Tests
62
+ // ---------------------------------------------------------------------------
63
+
64
+ describe("transcribeSegmentAudio", () => {
65
+ beforeEach(() => {
66
+ mockTranscriber = null;
67
+ spawnResult = { exitCode: 0, stdout: "", stderr: "" };
68
+ mockFileContents = Buffer.from("fake-wav-data");
69
+ });
70
+
71
+ test("returns transcript text on successful transcription", async () => {
72
+ mockTranscriber = makeMockTranscriber("Hello, this is a test transcript.");
73
+
74
+ const result = await transcribeSegmentAudio("/tmp/video.mp4", 10, 15);
75
+
76
+ expect(result).toBe("Hello, this is a test transcript.");
77
+ });
78
+
79
+ test("returns empty string when no STT provider is configured", async () => {
80
+ mockTranscriber = null;
81
+
82
+ const result = await transcribeSegmentAudio("/tmp/video.mp4", 0, 30);
83
+
84
+ expect(result).toBe("");
85
+ });
86
+
87
+ test("returns empty string when ffmpeg extraction fails", async () => {
88
+ mockTranscriber = makeMockTranscriber("should not reach here");
89
+ spawnResult = {
90
+ exitCode: 1,
91
+ stdout: "",
92
+ stderr: "ffmpeg: error extracting audio",
93
+ };
94
+
95
+ const result = await transcribeSegmentAudio("/tmp/video.mp4", 5, 10);
96
+
97
+ expect(result).toBe("");
98
+ });
99
+
100
+ test("returns empty string when provider throws an error", async () => {
101
+ mockTranscriber = makeFailingTranscriber(
102
+ new Error("Provider API rate limited"),
103
+ );
104
+
105
+ const result = await transcribeSegmentAudio("/tmp/video.mp4", 0, 20);
106
+
107
+ expect(result).toBe("");
108
+ });
109
+
110
+ test("trims whitespace from transcript result", async () => {
111
+ mockTranscriber = makeMockTranscriber(" trimmed text ");
112
+
113
+ const result = await transcribeSegmentAudio("/tmp/video.mp4", 0, 10);
114
+
115
+ expect(result).toBe("trimmed text");
116
+ });
117
+
118
+ test("returns empty string when provider returns empty text", async () => {
119
+ mockTranscriber = makeMockTranscriber("");
120
+
121
+ const result = await transcribeSegmentAudio("/tmp/video.mp4", 0, 10);
122
+
123
+ expect(result).toBe("");
124
+ });
125
+ });
@@ -0,0 +1,181 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { PreprocessManifest } from "../services/preprocess.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks — must be declared before the subject import
7
+ // ---------------------------------------------------------------------------
8
+
9
+ let lastPreprocessOptions: Record<string, unknown> | undefined;
10
+ let mockManifest: PreprocessManifest;
11
+ let mockPreprocessError: Error | null = null;
12
+
13
+ mock.module("../services/preprocess.js", () => ({
14
+ preprocessForAsset: async (
15
+ _assetId: string,
16
+ options: Record<string, unknown>,
17
+ ) => {
18
+ lastPreprocessOptions = options;
19
+ if (mockPreprocessError) throw mockPreprocessError;
20
+ return mockManifest;
21
+ },
22
+ }));
23
+
24
+ mock.module("../../../../memory/media-store.js", () => ({
25
+ getMediaAssetById: () => ({ filePath: "/tmp/videos/test.mp4" }),
26
+ getKeyframesForAsset: () => [{ id: "kf-1" }, { id: "kf-2" }, { id: "kf-3" }],
27
+ }));
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Subject import (after mocks)
31
+ // ---------------------------------------------------------------------------
32
+
33
+ import { run } from "../tools/extract-keyframes.js";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function makeManifest(
40
+ overrides: Partial<PreprocessManifest> = {},
41
+ ): PreprocessManifest {
42
+ return {
43
+ assetId: "asset-1",
44
+ videoPath: "/tmp/videos/test.mp4",
45
+ durationSeconds: 60,
46
+ segments: [
47
+ {
48
+ id: "seg-001",
49
+ startSeconds: 0,
50
+ endSeconds: 15,
51
+ framePaths: ["/tmp/frame-1.jpg"],
52
+ frameTimestamps: [0],
53
+ },
54
+ ],
55
+ deadTimeRanges: [],
56
+ subjectRegistry: { groups: [] },
57
+ sectionBoundaries: [],
58
+ config: {
59
+ intervalSeconds: 1,
60
+ segmentDuration: 15,
61
+ deadTimeThreshold: 0.02,
62
+ shortEdge: 480,
63
+ },
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ function makeContext() {
69
+ return {
70
+ workingDir: "/tmp",
71
+ conversationId: "conv-1",
72
+ trustClass: "guardian" as const,
73
+ onOutput: () => {},
74
+ };
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Tests
79
+ // ---------------------------------------------------------------------------
80
+
81
+ describe("extract_keyframes tool", () => {
82
+ beforeEach(() => {
83
+ lastPreprocessOptions = undefined;
84
+ mockPreprocessError = null;
85
+ mockManifest = makeManifest();
86
+ });
87
+
88
+ test("returns error when asset_id is missing", async () => {
89
+ const result = await run({}, makeContext());
90
+
91
+ expect(result.isError).toBe(true);
92
+ expect(result.content).toBe("asset_id is required.");
93
+ });
94
+
95
+ test("passes include_audio through to preprocessForAsset", async () => {
96
+ await run({ asset_id: "asset-1", include_audio: true }, makeContext());
97
+
98
+ expect(lastPreprocessOptions?.includeAudio).toBe(true);
99
+ });
100
+
101
+ test("defaults include_audio to false when not provided", async () => {
102
+ await run({ asset_id: "asset-1" }, makeContext());
103
+
104
+ expect(lastPreprocessOptions?.includeAudio).toBe(false);
105
+ });
106
+
107
+ test("maps all numeric option fields from tool input", async () => {
108
+ await run(
109
+ {
110
+ asset_id: "asset-1",
111
+ interval_seconds: 2,
112
+ segment_duration: 30,
113
+ dead_time_threshold: 0.05,
114
+ short_edge: 720,
115
+ },
116
+ makeContext(),
117
+ );
118
+
119
+ expect(lastPreprocessOptions?.intervalSeconds).toBe(2);
120
+ expect(lastPreprocessOptions?.segmentDuration).toBe(30);
121
+ expect(lastPreprocessOptions?.deadTimeThreshold).toBe(0.05);
122
+ expect(lastPreprocessOptions?.shortEdge).toBe(720);
123
+ });
124
+
125
+ test("passes detect_dead_time and section_config through", async () => {
126
+ await run(
127
+ {
128
+ asset_id: "asset-1",
129
+ detect_dead_time: true,
130
+ section_config: "/tmp/sections.json",
131
+ },
132
+ makeContext(),
133
+ );
134
+
135
+ expect(lastPreprocessOptions?.detectDeadTime).toBe(true);
136
+ expect(lastPreprocessOptions?.sectionConfigPath).toBe("/tmp/sections.json");
137
+ });
138
+
139
+ test("does not pass transcriptionMode or openaiApiKey", async () => {
140
+ await run(
141
+ {
142
+ asset_id: "asset-1",
143
+ include_audio: true,
144
+ transcription_mode: "api",
145
+ },
146
+ makeContext(),
147
+ );
148
+
149
+ expect(lastPreprocessOptions).toBeDefined();
150
+ expect("transcriptionMode" in lastPreprocessOptions!).toBe(false);
151
+ expect("openaiApiKey" in lastPreprocessOptions!).toBe(false);
152
+ });
153
+
154
+ test("returns successful result with manifest summary", async () => {
155
+ const result = await run({ asset_id: "asset-1" }, makeContext());
156
+
157
+ expect(result.isError).toBe(false);
158
+ const parsed = JSON.parse(result.content);
159
+ expect(parsed.assetId).toBe("asset-1");
160
+ expect(parsed.segmentCount).toBe(1);
161
+ expect(parsed.keyframeCount).toBe(3);
162
+ });
163
+
164
+ test("returns error content for known preprocess failures", async () => {
165
+ mockPreprocessError = new Error("Media asset not found: asset-bad");
166
+
167
+ const result = await run({ asset_id: "asset-bad" }, makeContext());
168
+
169
+ expect(result.isError).toBe(true);
170
+ expect(result.content).toBe("Media asset not found: asset-bad");
171
+ });
172
+
173
+ test("wraps unknown errors in generic message", async () => {
174
+ mockPreprocessError = new Error("Something unexpected");
175
+
176
+ const result = await run({ asset_id: "asset-1" }, makeContext());
177
+
178
+ expect(result.isError).toBe(true);
179
+ expect(result.content).toBe("Preprocess failed: Something unexpected");
180
+ });
181
+ });