@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
@@ -1,6 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- import type { SpeechToTextProvider } from "../../../providers/speech-to-text/types.js";
3
+ import type { BatchTranscriber } from "../../../stt/types.js";
4
+ import { SttError } from "../../../stt/types.js";
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // Mocks — must be set up before importing the module under test
@@ -17,7 +18,7 @@ let mockAttachments: Array<{
17
18
  thumbnailBase64: string | null;
18
19
  createdAt: number;
19
20
  }> = [];
20
- let mockProvider: SpeechToTextProvider | null = null;
21
+ let mockTranscriber: BatchTranscriber | null = null;
21
22
 
22
23
  mock.module("../../../config/assistant-feature-flags.js", () => ({
23
24
  isAssistantFeatureFlagEnabled: () => mockFeatureFlagEnabled,
@@ -35,7 +36,18 @@ mock.module("../../../memory/attachments-store.js", () => ({
35
36
  }));
36
37
 
37
38
  mock.module("../../../providers/speech-to-text/resolve.js", () => ({
38
- resolveSpeechToTextProvider: async () => mockProvider,
39
+ resolveBatchTranscriber: async () => mockTranscriber,
40
+ }));
41
+
42
+ mock.module("../../../stt/daemon-batch-transcriber.js", () => ({
43
+ normalizeSttError: (err: unknown): SttError => {
44
+ if (err instanceof SttError) return err;
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ if (err instanceof Error && err.name === "AbortError") {
47
+ return new SttError("timeout", message);
48
+ }
49
+ return new SttError("provider-error", message);
50
+ },
39
51
  }));
40
52
 
41
53
  mock.module("../../../util/logger.js", () => ({
@@ -105,7 +117,7 @@ describe("tryTranscribeAudioAttachments", () => {
105
117
  beforeEach(() => {
106
118
  mockFeatureFlagEnabled = true;
107
119
  mockAttachments = [];
108
- mockProvider = null;
120
+ mockTranscriber = null;
109
121
  });
110
122
 
111
123
  afterEach(() => {
@@ -115,7 +127,9 @@ describe("tryTranscribeAudioAttachments", () => {
115
127
  test("audio attachment is transcribed and returns transcribed result", async () => {
116
128
  const audio = makeAudioAttachment("a1");
117
129
  mockAttachments = [audio];
118
- mockProvider = {
130
+ mockTranscriber = {
131
+ providerId: "openai-whisper",
132
+ boundaryId: "daemon-batch",
119
133
  transcribe: async () => ({ text: "Hello, how are you?" }),
120
134
  };
121
135
 
@@ -131,7 +145,9 @@ describe("tryTranscribeAudioAttachments", () => {
131
145
  const doc = makeDocumentAttachment("d1");
132
146
  const img = makeImageAttachment("i1");
133
147
  mockAttachments = [doc, img];
134
- mockProvider = {
148
+ mockTranscriber = {
149
+ providerId: "openai-whisper",
150
+ boundaryId: "daemon-batch",
135
151
  transcribe: async () => ({ text: "should not be called" }),
136
152
  };
137
153
 
@@ -140,23 +156,26 @@ describe("tryTranscribeAudioAttachments", () => {
140
156
  expect(result).toEqual({ status: "no_audio" });
141
157
  });
142
158
 
143
- test("no API key returns no_provider with helpful reason string", async () => {
159
+ test("no provider returns no_provider with service-agnostic reason", async () => {
144
160
  const audio = makeAudioAttachment("a1");
145
161
  mockAttachments = [audio];
146
- mockProvider = null; // No provider resolved
162
+ mockTranscriber = null; // No transcriber resolved
147
163
 
148
164
  const result = await tryTranscribeAudioAttachments(["a1"]);
149
165
 
150
166
  expect(result.status).toBe("no_provider");
151
- expect((result as { reason: string }).reason).toContain(
152
- "No OpenAI API key configured",
153
- );
167
+ const reason = (result as { reason: string }).reason;
168
+ expect(reason).toContain("speech-to-text");
169
+ expect(reason).toContain("voice message transcription");
170
+ expect(reason).not.toContain("OpenAI");
154
171
  });
155
172
 
156
173
  test("API failure returns error with reason", async () => {
157
174
  const audio = makeAudioAttachment("a1");
158
175
  mockAttachments = [audio];
159
- mockProvider = {
176
+ mockTranscriber = {
177
+ providerId: "openai-whisper",
178
+ boundaryId: "daemon-batch",
160
179
  transcribe: async () => {
161
180
  throw new Error("API rate limit exceeded");
162
181
  },
@@ -183,29 +202,9 @@ describe("tryTranscribeAudioAttachments", () => {
183
202
  test("30-second timeout fires and returns error without blocking", async () => {
184
203
  const audio = makeAudioAttachment("a1");
185
204
  mockAttachments = [audio];
186
- mockProvider = {
187
- transcribe: async (_audio, _mime, signal) => {
188
- // Simulate a provider that respects the abort signal
189
- return new Promise((_resolve, reject) => {
190
- if (signal?.aborted) {
191
- reject(new DOMException("The operation was aborted", "AbortError"));
192
- return;
193
- }
194
- const onAbort = () => {
195
- reject(new DOMException("The operation was aborted", "AbortError"));
196
- };
197
- signal?.addEventListener("abort", onAbort, { once: true });
198
- });
199
- },
200
- };
201
-
202
- // The timeout is 30s in the real code, but the test's mock provider
203
- // aborts immediately when signaled. We verify the error path works
204
- // by checking the result type. For a true timeout test we'd need
205
- // to override the timeout constant, but this confirms the abort
206
- // path produces the correct result.
207
- // Instead, let's test with a provider that checks signal state:
208
- mockProvider = {
205
+ mockTranscriber = {
206
+ providerId: "openai-whisper",
207
+ boundaryId: "daemon-batch",
209
208
  transcribe: async () => {
210
209
  throw new DOMException("The operation was aborted", "AbortError");
211
210
  },
@@ -225,7 +224,9 @@ describe("tryTranscribeAudioAttachments", () => {
225
224
  mockAttachments = [a1, a2];
226
225
 
227
226
  let callCount = 0;
228
- mockProvider = {
227
+ mockTranscriber = {
228
+ providerId: "openai-whisper",
229
+ boundaryId: "daemon-batch",
229
230
  transcribe: async () => {
230
231
  callCount++;
231
232
  return { text: callCount === 1 ? "First message" : "Second message" };
@@ -247,7 +248,9 @@ describe("tryTranscribeAudioAttachments", () => {
247
248
  mockAttachments = [audio, doc];
248
249
 
249
250
  let transcribeCallCount = 0;
250
- mockProvider = {
251
+ mockTranscriber = {
252
+ providerId: "openai-whisper",
253
+ boundaryId: "daemon-batch",
251
254
  transcribe: async () => {
252
255
  transcribeCallCount++;
253
256
  return { text: "Voice transcription" };
@@ -264,7 +267,9 @@ describe("tryTranscribeAudioAttachments", () => {
264
267
  });
265
268
 
266
269
  test("empty attachment IDs returns no_audio", async () => {
267
- mockProvider = {
270
+ mockTranscriber = {
271
+ providerId: "openai-whisper",
272
+ boundaryId: "daemon-batch",
268
273
  transcribe: async () => ({ text: "should not be called" }),
269
274
  };
270
275
 
@@ -276,7 +281,9 @@ describe("tryTranscribeAudioAttachments", () => {
276
281
  test("attachment with empty transcription returns no_audio", async () => {
277
282
  const audio = makeAudioAttachment("a1");
278
283
  mockAttachments = [audio];
279
- mockProvider = {
284
+ mockTranscriber = {
285
+ providerId: "openai-whisper",
286
+ boundaryId: "daemon-batch",
280
287
  transcribe: async () => ({ text: " " }), // whitespace-only
281
288
  };
282
289
 
@@ -10,7 +10,8 @@
10
10
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
11
11
  import { getConfig } from "../../../config/loader.js";
12
12
  import * as attachmentsStore from "../../../memory/attachments-store.js";
13
- import { resolveSpeechToTextProvider } from "../../../providers/speech-to-text/resolve.js";
13
+ import { resolveBatchTranscriber } from "../../../providers/speech-to-text/resolve.js";
14
+ import { normalizeSttError } from "../../../stt/daemon-batch-transcriber.js";
14
15
  import { getLogger } from "../../../util/logger.js";
15
16
 
16
17
  const log = getLogger("transcribe-audio");
@@ -55,13 +56,13 @@ export async function tryTranscribeAudioAttachments(
55
56
  return { status: "no_audio" };
56
57
  }
57
58
 
58
- // Resolve STT provider
59
- const provider = await resolveSpeechToTextProvider();
60
- if (!provider) {
59
+ // Resolve STT provider via daemon batch transcriber facade
60
+ const transcriber = await resolveBatchTranscriber();
61
+ if (!transcriber) {
61
62
  return {
62
63
  status: "no_provider",
63
64
  reason:
64
- "No OpenAI API key configured. Set one up to enable voice message transcription.",
65
+ "No speech-to-text provider configured. Add an API key for your STT service to enable voice message transcription.",
65
66
  };
66
67
  }
67
68
 
@@ -89,11 +90,11 @@ export async function tryTranscribeAudioAttachments(
89
90
  }
90
91
 
91
92
  const buffer = Buffer.from(hydrated.dataBase64, "base64");
92
- const result = await provider.transcribe(
93
- buffer,
94
- attachment.mimeType,
95
- abortController.signal,
96
- );
93
+ const result = await transcriber.transcribe({
94
+ audio: buffer,
95
+ mimeType: attachment.mimeType,
96
+ signal: abortController.signal,
97
+ });
97
98
 
98
99
  if (result.text.trim()) {
99
100
  transcriptions.push(result.text.trim());
@@ -109,12 +110,11 @@ export async function tryTranscribeAudioAttachments(
109
110
  clearTimeout(timeoutId);
110
111
  }
111
112
  } catch (err: unknown) {
113
+ const sttErr = normalizeSttError(err);
112
114
  const reason =
113
- err instanceof Error
114
- ? err.name === "AbortError"
115
- ? "Transcription timed out"
116
- : err.message
117
- : String(err);
115
+ sttErr.category === "timeout"
116
+ ? "Transcription timed out"
117
+ : sttErr.message;
118
118
  log.warn({ err }, "Audio transcription failed");
119
119
  return { status: "error", reason };
120
120
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Unit tests for the POST /v1/integrations/slack/channel/config HTTP route.
3
+ *
4
+ * Mocks `setSlackChannelConfig` in the config-slack-channel handler module so
5
+ * the test can observe which arguments the HTTP handler forwards from the
6
+ * request body — particularly the optional `userToken` field.
7
+ */
8
+
9
+ import { afterEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ import type { SlackChannelConfigResult } from "../../../../../daemon/handlers/config-slack-channel.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Module mock — must appear before importing the module under test
15
+ // ---------------------------------------------------------------------------
16
+
17
+ interface SetConfigCall {
18
+ botToken?: string;
19
+ appToken?: string;
20
+ userToken?: string;
21
+ }
22
+
23
+ let lastSetConfigCall: SetConfigCall | null = null;
24
+ let mockSetConfigResult: SlackChannelConfigResult = {
25
+ success: true,
26
+ hasBotToken: false,
27
+ hasAppToken: false,
28
+ hasUserToken: false,
29
+ connected: false,
30
+ };
31
+
32
+ mock.module("../../../../../daemon/handlers/config-slack-channel.js", () => ({
33
+ setSlackChannelConfig: async (
34
+ botToken?: string,
35
+ appToken?: string,
36
+ userToken?: string,
37
+ ): Promise<SlackChannelConfigResult> => {
38
+ lastSetConfigCall = { botToken, appToken, userToken };
39
+ return mockSetConfigResult;
40
+ },
41
+ getSlackChannelConfig: async (): Promise<SlackChannelConfigResult> => ({
42
+ success: true,
43
+ hasBotToken: false,
44
+ hasAppToken: false,
45
+ hasUserToken: false,
46
+ connected: false,
47
+ }),
48
+ clearSlackChannelConfig: async (): Promise<SlackChannelConfigResult> => ({
49
+ success: true,
50
+ hasBotToken: false,
51
+ hasAppToken: false,
52
+ hasUserToken: false,
53
+ connected: false,
54
+ }),
55
+ }));
56
+
57
+ const { handleSetSlackChannelConfig } = await import("../channel.js");
58
+
59
+ describe("POST /v1/integrations/slack/channel/config", () => {
60
+ afterEach(() => {
61
+ lastSetConfigCall = null;
62
+ mockSetConfigResult = {
63
+ success: true,
64
+ hasBotToken: false,
65
+ hasAppToken: false,
66
+ hasUserToken: false,
67
+ connected: false,
68
+ };
69
+ });
70
+
71
+ test("forwards userToken from request body as the third argument", async () => {
72
+ const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
73
+ method: "POST",
74
+ body: JSON.stringify({ userToken: "xoxp-test-user-token" }),
75
+ });
76
+
77
+ const res = await handleSetSlackChannelConfig(req);
78
+ expect(res.status).toBe(200);
79
+
80
+ expect(lastSetConfigCall).not.toBeNull();
81
+ expect(lastSetConfigCall?.botToken).toBeUndefined();
82
+ expect(lastSetConfigCall?.appToken).toBeUndefined();
83
+ expect(lastSetConfigCall?.userToken).toBe("xoxp-test-user-token");
84
+ });
85
+
86
+ test("forwards all three tokens when present in body", async () => {
87
+ const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
88
+ method: "POST",
89
+ body: JSON.stringify({
90
+ botToken: "xoxb-bot",
91
+ appToken: "xapp-app",
92
+ userToken: "xoxp-user",
93
+ }),
94
+ });
95
+
96
+ const res = await handleSetSlackChannelConfig(req);
97
+ expect(res.status).toBe(200);
98
+
99
+ expect(lastSetConfigCall?.botToken).toBe("xoxb-bot");
100
+ expect(lastSetConfigCall?.appToken).toBe("xapp-app");
101
+ expect(lastSetConfigCall?.userToken).toBe("xoxp-user");
102
+ });
103
+
104
+ test("leaves userToken undefined when absent from body", async () => {
105
+ const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
106
+ method: "POST",
107
+ body: JSON.stringify({ botToken: "xoxb-bot", appToken: "xapp-app" }),
108
+ });
109
+
110
+ const res = await handleSetSlackChannelConfig(req);
111
+ expect(res.status).toBe(200);
112
+
113
+ expect(lastSetConfigCall?.botToken).toBe("xoxb-bot");
114
+ expect(lastSetConfigCall?.appToken).toBe("xapp-app");
115
+ expect(lastSetConfigCall?.userToken).toBeUndefined();
116
+ });
117
+
118
+ test("returns 400 when handler reports success: false", async () => {
119
+ mockSetConfigResult = {
120
+ success: false,
121
+ hasBotToken: false,
122
+ hasAppToken: false,
123
+ hasUserToken: false,
124
+ connected: false,
125
+ error: "Invalid user token: must start with \"xoxp-\"",
126
+ };
127
+
128
+ const req = new Request("http://localhost/v1/integrations/slack/channel/config", {
129
+ method: "POST",
130
+ body: JSON.stringify({ userToken: "abc-123" }),
131
+ });
132
+
133
+ const res = await handleSetSlackChannelConfig(req);
134
+ expect(res.status).toBe(400);
135
+ expect(lastSetConfigCall?.userToken).toBe("abc-123");
136
+ });
137
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Unit tests for the Share UI Slack route handlers.
3
+ *
4
+ * Verifies the read/write auth split mirrors `messaging/providers/slack/adapter.ts`:
5
+ * - Channel enumeration (GET /v1/slack/channels) is a read path and must
6
+ * prefer the user_token when present so the picker surfaces channels the
7
+ * user is in but the bot isn't.
8
+ * - Channel sharing (POST /v1/slack/share) is a write path and must always
9
+ * use the bot_token so posts come from the bot identity.
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ import { credentialKey } from "../../../../../security/credential-key.js";
15
+
16
+ // ── Module mocks ────────────────────────────────────────────────────────────
17
+
18
+ const getSecureKeyAsyncMock = mock(
19
+ async (_key: string): Promise<string | null> => null,
20
+ );
21
+ mock.module("../../../../../security/secure-keys.js", () => ({
22
+ getSecureKeyAsync: getSecureKeyAsyncMock,
23
+ }));
24
+
25
+ // share.ts imports getConnectionByProvider from oauth-store at module load.
26
+ // Stub it to return undefined so Socket Mode tokens are the only source.
27
+ mock.module("../../../../../oauth/oauth-store.js", () => ({
28
+ getConnectionByProvider: () => undefined,
29
+ }));
30
+
31
+ // Stub the app store so handleShareToSlackChannel finds the app.
32
+ const FAKE_APP = { id: "app-1", name: "Test App", description: "desc" };
33
+ mock.module("../../../../../memory/app-store.js", () => ({
34
+ getApp: (id: string) => (id === FAKE_APP.id ? FAKE_APP : undefined),
35
+ }));
36
+
37
+ const { handleListSlackChannels, handleShareToSlackChannel } = await import(
38
+ "../share.js"
39
+ );
40
+
41
+ // ── fetch capture ───────────────────────────────────────────────────────────
42
+
43
+ type CapturedRequest = {
44
+ url: string;
45
+ method: string;
46
+ authorization: string | null;
47
+ };
48
+
49
+ const captured: CapturedRequest[] = [];
50
+ const originalFetch = globalThis.fetch;
51
+
52
+ function installFetchStub() {
53
+ globalThis.fetch = (async (
54
+ input: RequestInfo | URL,
55
+ init?: RequestInit,
56
+ ): Promise<Response> => {
57
+ const url =
58
+ typeof input === "string"
59
+ ? input
60
+ : input instanceof URL
61
+ ? input.toString()
62
+ : input.url;
63
+ const method = (init?.method ?? "GET").toUpperCase();
64
+ const headers = new Headers(init?.headers ?? {});
65
+ captured.push({
66
+ url,
67
+ method,
68
+ authorization: headers.get("authorization"),
69
+ });
70
+
71
+ // Minimal OK Slack envelopes so handlers don't throw on shape mismatches.
72
+ const body = fakeSlackResponse(url);
73
+ return new Response(JSON.stringify(body), {
74
+ status: 200,
75
+ headers: { "Content-Type": "application/json" },
76
+ });
77
+ }) as typeof fetch;
78
+ }
79
+
80
+ function fakeSlackResponse(url: string): Record<string, unknown> {
81
+ if (url.includes("/conversations.list")) {
82
+ return { ok: true, channels: [], response_metadata: { next_cursor: "" } };
83
+ }
84
+ if (url.includes("/chat.postMessage")) {
85
+ return { ok: true, ts: "1700000000.000100", channel: "C123" };
86
+ }
87
+ return { ok: true };
88
+ }
89
+
90
+ // ── Test fixtures ───────────────────────────────────────────────────────────
91
+
92
+ const BOT_TOKEN = "xoxb-BOT";
93
+ const USER_TOKEN = "xoxp-USER";
94
+
95
+ describe("Slack share route token routing", () => {
96
+ beforeEach(() => {
97
+ captured.length = 0;
98
+ getSecureKeyAsyncMock.mockReset();
99
+ installFetchStub();
100
+ });
101
+
102
+ afterEach(() => {
103
+ globalThis.fetch = originalFetch;
104
+ });
105
+
106
+ test("GET /v1/slack/channels: bot-only install reads with bot token", async () => {
107
+ getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
108
+ if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
109
+ return null;
110
+ });
111
+
112
+ const res = await handleListSlackChannels();
113
+ expect(res.status).toBe(200);
114
+
115
+ const listCall = captured.find((c) =>
116
+ c.url.includes("/conversations.list"),
117
+ );
118
+ expect(listCall).toBeDefined();
119
+ expect(listCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
120
+ });
121
+
122
+ test("GET /v1/slack/channels: bot + user tokens prefer user_token for reads", async () => {
123
+ // Core fix: with both tokens stored, the Share UI picker must see every
124
+ // channel the USER can see — not just ones the bot is a member of.
125
+ getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
126
+ if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
127
+ if (key === credentialKey("slack_channel", "user_token"))
128
+ return USER_TOKEN;
129
+ return null;
130
+ });
131
+
132
+ const res = await handleListSlackChannels();
133
+ expect(res.status).toBe(200);
134
+
135
+ const listCall = captured.find((c) =>
136
+ c.url.includes("/conversations.list"),
137
+ );
138
+ expect(listCall).toBeDefined();
139
+ expect(listCall!.authorization).toBe(`Bearer ${USER_TOKEN}`);
140
+ });
141
+
142
+ test("POST /v1/slack/share: bot + user tokens still write with bot token", async () => {
143
+ // SAFETY invariant: posts MUST come from the bot identity. If the handler
144
+ // ever routed the write through user_token, the posted message would
145
+ // appear as the user — unambiguously wrong.
146
+ getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
147
+ if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
148
+ if (key === credentialKey("slack_channel", "user_token"))
149
+ return USER_TOKEN;
150
+ return null;
151
+ });
152
+
153
+ const req = new Request("http://localhost/v1/slack/share", {
154
+ method: "POST",
155
+ body: JSON.stringify({ appId: FAKE_APP.id, channelId: "C123" }),
156
+ });
157
+
158
+ const res = await handleShareToSlackChannel(req);
159
+ expect(res.status).toBe(200);
160
+
161
+ const postCall = captured.find((c) => c.url.includes("/chat.postMessage"));
162
+ expect(postCall).toBeDefined();
163
+ expect(postCall!.authorization).toBe(`Bearer ${BOT_TOKEN}`);
164
+ });
165
+
166
+ test("no tokens configured: both handlers return 503", async () => {
167
+ getSecureKeyAsyncMock.mockImplementation(async () => null);
168
+
169
+ const listRes = await handleListSlackChannels();
170
+ expect(listRes.status).toBe(503);
171
+
172
+ const shareReq = new Request("http://localhost/v1/slack/share", {
173
+ method: "POST",
174
+ body: JSON.stringify({ appId: FAKE_APP.id, channelId: "C123" }),
175
+ });
176
+ const shareRes = await handleShareToSlackChannel(shareReq);
177
+ expect(shareRes.status).toBe(503);
178
+ });
179
+ });
@@ -28,13 +28,21 @@ export async function handleGetSlackChannelConfig(): Promise<Response> {
28
28
  /**
29
29
  * POST /v1/integrations/slack/channel/config
30
30
  *
31
- * Body: { botToken?: string, appToken?: string }
31
+ * Body: { botToken?: string, appToken?: string, userToken?: string }
32
32
  */
33
33
  export async function handleSetSlackChannelConfig(
34
34
  req: Request,
35
35
  ): Promise<Response> {
36
- const body = (await req.json()) as { botToken?: string; appToken?: string };
37
- const result = await setSlackChannelConfig(body.botToken, body.appToken);
36
+ const body = (await req.json()) as {
37
+ botToken?: string;
38
+ appToken?: string;
39
+ userToken?: string;
40
+ };
41
+ const result = await setSlackChannelConfig(
42
+ body.botToken,
43
+ body.appToken,
44
+ body.userToken,
45
+ );
38
46
  const status = result.success ? 200 : 400;
39
47
  return Response.json(result, { status });
40
48
  }
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../../../messaging/providers/slack/client.js";
14
14
  import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
15
15
  import { getConnectionByProvider } from "../../../../oauth/oauth-store.js";
16
+ import { credentialKey } from "../../../../security/credential-key.js";
16
17
  import { getSecureKeyAsync } from "../../../../security/secure-keys.js";
17
18
  import { getLogger } from "../../../../util/logger.js";
18
19
  import { httpError } from "../../../http-errors.js";
@@ -25,13 +26,46 @@ const log = getLogger("slack-share");
25
26
  // ---------------------------------------------------------------------------
26
27
 
27
28
  /**
28
- * Resolve the Slack bot token from the OAuth connection store.
29
+ * Resolve a Slack token for the Share UI, mirroring the read/write auth split
30
+ * in `messaging/providers/slack/adapter.ts`.
31
+ *
32
+ * For Socket Mode installs (tokens stored under `credential/slack_channel/*`),
33
+ * prefer the user OAuth token (xoxp-) for reads when present — this lets the
34
+ * channel picker surface channels the user belongs to but the bot doesn't.
35
+ * Fall back to the bot token (xoxb-) otherwise.
36
+ *
37
+ * Writes MUST always use the bot token so posted messages come from the bot
38
+ * identity, never the user. Passing `user_token` to chat.postMessage would
39
+ * post as the user — unambiguously wrong for Share UI behavior.
40
+ *
41
+ * For legacy OAuth installs (no Socket Mode tokens), fall back to the OAuth
42
+ * connection's access_token, which is the bot token in Slack's OAuth v2 flow.
29
43
  */
30
- async function resolveSlackToken(): Promise<string | undefined> {
44
+ async function resolveSlackToken(
45
+ mode: "read" | "write",
46
+ ): Promise<string | undefined> {
47
+ // Socket Mode path — tokens stored directly in the credential vault.
48
+ const botToken = await getSecureKeyAsync(
49
+ credentialKey("slack_channel", "bot_token"),
50
+ );
51
+ if (botToken) {
52
+ if (mode === "read") {
53
+ const userToken = await getSecureKeyAsync(
54
+ credentialKey("slack_channel", "user_token"),
55
+ );
56
+ return userToken ?? botToken;
57
+ }
58
+ // SAFETY: writes must use the bot token. Using the user token here would
59
+ // post as the user rather than the bot.
60
+ return botToken;
61
+ }
62
+
63
+ // Legacy OAuth path. Slack's OAuth v2 access_token is the bot token; there
64
+ // is no separate user token stored for this install, so reads and writes
65
+ // both use access_token.
31
66
  const conn = getConnectionByProvider("slack");
32
- return conn
33
- ? await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`)
34
- : undefined;
67
+ if (!conn) return undefined;
68
+ return await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`);
35
69
  }
36
70
 
37
71
  // ---------------------------------------------------------------------------
@@ -61,7 +95,9 @@ const TYPE_SORT_ORDER: Record<string, number> = {
61
95
  };
62
96
 
63
97
  export async function handleListSlackChannels(): Promise<Response> {
64
- const token = await resolveSlackToken();
98
+ // Channel enumeration is a read path — prefer user_token when present so
99
+ // the picker surfaces channels the user is in but the bot isn't.
100
+ const token = await resolveSlackToken("read");
65
101
  if (!token) {
66
102
  return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
67
103
  }
@@ -137,7 +173,9 @@ export async function handleListSlackChannels(): Promise<Response> {
137
173
  export async function handleShareToSlackChannel(
138
174
  req: Request,
139
175
  ): Promise<Response> {
140
- const token = await resolveSlackToken();
176
+ // Posting a message is a write path — must use the bot token so the message
177
+ // comes from the bot identity, never the user.
178
+ const token = await resolveSlackToken("write");
141
179
  if (!token) {
142
180
  return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
143
181
  }