@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -24,13 +24,18 @@ mock.module("../../../../security/secure-keys.js", () => ({
24
24
  }));
25
25
 
26
26
  // OAuth helpers are exercised only when no bot_token is cached. The adapter
27
- // imports them at module load route them through a stub that signals any
28
- // OAuth fallback with a distinctive error so tests can assert on it.
27
+ // imports them at module load, so route them through a configurable stub.
29
28
  const OAUTH_FALLBACK_SENTINEL = "OAUTH_FALLBACK_NOT_STUBBED";
30
- mock.module("../../../../oauth/connection-resolver.js", () => ({
31
- resolveOAuthConnection: async (): Promise<OAuthConnection> => {
29
+ const resolveOAuthConnectionMock = mock(
30
+ async (
31
+ _provider: string,
32
+ _opts?: { account?: string },
33
+ ): Promise<OAuthConnection> => {
32
34
  throw new Error(OAUTH_FALLBACK_SENTINEL);
33
35
  },
36
+ );
37
+ mock.module("../../../../oauth/connection-resolver.js", () => ({
38
+ resolveOAuthConnection: resolveOAuthConnectionMock,
34
39
  }));
35
40
  mock.module("../../../../oauth/oauth-store.js", () => ({
36
41
  isProviderConnected: async () => false,
@@ -44,7 +49,7 @@ mock.module("../../../../contacts/contacts-write.js", () => ({
44
49
  upsertContactChannel: () => {},
45
50
  }));
46
51
 
47
- import { slackProvider } from "../adapter.js";
52
+ import { slackProvider, withSlackBotToken } from "../adapter.js";
48
53
 
49
54
  // ── fetch capture ───────────────────────────────────────────────────────────
50
55
 
@@ -108,9 +113,23 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
108
113
  const BOT_TOKEN = "xoxb-BOT";
109
114
  const USER_TOKEN = "xoxp-USER";
110
115
 
116
+ function makeOAuthConnection(account: string, token: string): OAuthConnection {
117
+ return {
118
+ id: `conn-${account}`,
119
+ provider: "slack",
120
+ accountInfo: account,
121
+ request: async () => ({ status: 200, headers: {}, body: { ok: true } }),
122
+ withToken: async <T>(fn: (rawToken: string) => Promise<T>) => fn(token),
123
+ };
124
+ }
125
+
111
126
  describe("Slack adapter token routing", () => {
112
127
  beforeEach(() => {
113
128
  captured.length = 0;
129
+ resolveOAuthConnectionMock.mockReset();
130
+ resolveOAuthConnectionMock.mockImplementation(async () => {
131
+ throw new Error(OAUTH_FALLBACK_SENTINEL);
132
+ });
114
133
  getSecureKeyAsyncMock.mockReset();
115
134
  getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
116
135
  if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
@@ -279,4 +298,25 @@ describe("Slack adapter token routing", () => {
279
298
  OAUTH_FALLBACK_SENTINEL,
280
299
  );
281
300
  });
301
+
302
+ test("raw bot token helper resolves the requested OAuth account even when cache is warm", async () => {
303
+ getSecureKeyAsyncMock.mockImplementation(async () => null);
304
+ resolveOAuthConnectionMock.mockImplementation(
305
+ async (_provider: string, opts?: { account?: string }) => {
306
+ const account = opts?.account ?? "default";
307
+ return makeOAuthConnection(account, `token-${account}`);
308
+ },
309
+ );
310
+
311
+ await slackProvider.resolveConnection!("workspace-a");
312
+
313
+ const result = await withSlackBotToken("workspace-b", async (token) => {
314
+ return token;
315
+ });
316
+
317
+ expect(result).toBe("token-workspace-b");
318
+ expect(resolveOAuthConnectionMock).toHaveBeenCalledWith("slack", {
319
+ account: "workspace-b",
320
+ });
321
+ });
282
322
  });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Unit tests for the assistant-side Slack file downloader used by the
3
+ * thread-backfill image-hydration path.
4
+ *
5
+ * The downloader has three contract-level behaviors worth pinning:
6
+ * 1. URL selection — `url_private_download` is preferred over `url_private`.
7
+ * 2. Bearer auth — the bot token MUST be sent on the initial request.
8
+ * 3. Manual cross-origin redirect handling — the CDN URL is signed and the
9
+ * Authorization header MUST NOT be re-sent on the second hop (Slack
10
+ * rejects the signed URL when an unexpected Authorization is present).
11
+ * 4. Returns null when no usable URL is present.
12
+ */
13
+
14
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
15
+
16
+ mock.module("../../../../util/logger.js", () => ({
17
+ getLogger: () =>
18
+ new Proxy({} as Record<string, unknown>, {
19
+ get: () => () => {},
20
+ }),
21
+ }));
22
+
23
+ import { downloadSlackFile } from "../download.js";
24
+
25
+ interface CapturedFetchCall {
26
+ url: string;
27
+ init: RequestInit | undefined;
28
+ }
29
+
30
+ let calls: CapturedFetchCall[];
31
+ let responses: Response[];
32
+ let originalFetch: typeof fetch;
33
+
34
+ beforeEach(() => {
35
+ calls = [];
36
+ responses = [];
37
+ originalFetch = globalThis.fetch;
38
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
39
+ calls.push({
40
+ url: typeof input === "string" ? input : input.toString(),
41
+ init,
42
+ });
43
+ const next = responses.shift();
44
+ if (!next) {
45
+ throw new Error("downloadSlackFile test: no canned response available");
46
+ }
47
+ return next;
48
+ }) as typeof fetch;
49
+ });
50
+
51
+ afterEach(() => {
52
+ globalThis.fetch = originalFetch;
53
+ });
54
+
55
+ describe("downloadSlackFile", () => {
56
+ test("returns null when neither url_private_download nor url_private is present", async () => {
57
+ const result = await downloadSlackFile(
58
+ { name: "screenshot.png", mimetype: "image/png" },
59
+ "xoxb-test",
60
+ );
61
+ expect(result).toBeNull();
62
+ expect(calls.length).toBe(0);
63
+ });
64
+
65
+ test("prefers url_private_download over url_private", async () => {
66
+ responses.push(
67
+ new Response(new Uint8Array([1, 2, 3]).buffer, {
68
+ status: 200,
69
+ headers: { "Content-Type": "image/png" },
70
+ }),
71
+ );
72
+ await downloadSlackFile(
73
+ {
74
+ id: "F1",
75
+ name: "shot.png",
76
+ mimetype: "image/png",
77
+ urlPrivateDownload: "https://files.slack.com/files-pri/T/F1/download",
78
+ urlPrivate: "https://files.slack.com/files-pri/T/F1/inline",
79
+ },
80
+ "xoxb-test",
81
+ );
82
+ expect(calls.length).toBe(1);
83
+ expect(calls[0].url).toBe(
84
+ "https://files.slack.com/files-pri/T/F1/download",
85
+ );
86
+ });
87
+
88
+ test("sends bot token as Bearer on the initial request and base64-encodes the body", async () => {
89
+ responses.push(
90
+ new Response(new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer, {
91
+ status: 200,
92
+ headers: { "Content-Type": "image/png" },
93
+ }),
94
+ );
95
+ const result = await downloadSlackFile(
96
+ {
97
+ id: "F1",
98
+ name: "shot.png",
99
+ mimetype: "image/png",
100
+ urlPrivate: "https://files.slack.com/files-pri/T/F1/inline",
101
+ },
102
+ "xoxb-test-token",
103
+ );
104
+ expect(calls.length).toBe(1);
105
+ const auth = (calls[0].init?.headers as Record<string, string>)
106
+ ?.Authorization;
107
+ expect(auth).toBe("Bearer xoxb-test-token");
108
+ expect(calls[0].init?.redirect).toBe("manual");
109
+ expect(result).not.toBeNull();
110
+ expect(result?.filename).toBe("shot.png");
111
+ expect(result?.mimeType).toBe("image/png");
112
+ // 0xdeadbeef → "3q2+7w==" in base64.
113
+ expect(result?.data).toBe("3q2+7w==");
114
+ });
115
+
116
+ test("follows a 302 to the signed CDN URL without re-sending the bearer token", async () => {
117
+ responses.push(
118
+ new Response(null, {
119
+ status: 302,
120
+ headers: {
121
+ Location:
122
+ "https://files-edge.slack.com/files-tmb/T-F1-abc/cdn-signed?t=1700000000",
123
+ },
124
+ }),
125
+ );
126
+ responses.push(
127
+ new Response(new Uint8Array([1, 2]).buffer, {
128
+ status: 200,
129
+ headers: { "Content-Type": "image/jpeg" },
130
+ }),
131
+ );
132
+ const result = await downloadSlackFile(
133
+ {
134
+ id: "F1",
135
+ name: "photo.jpg",
136
+ mimetype: "image/jpeg",
137
+ urlPrivateDownload: "https://files.slack.com/files-pri/T/F1/download",
138
+ },
139
+ "xoxb-test",
140
+ );
141
+ expect(calls.length).toBe(2);
142
+ expect(calls[0].init?.redirect).toBe("manual");
143
+ const secondAuth = (calls[1].init?.headers as Record<string, string>)
144
+ ?.Authorization;
145
+ expect(secondAuth).toBeUndefined();
146
+ expect(calls[1].url).toBe(
147
+ "https://files-edge.slack.com/files-tmb/T-F1-abc/cdn-signed?t=1700000000",
148
+ );
149
+ expect(result?.data).toBe(Buffer.from([1, 2]).toString("base64"));
150
+ });
151
+
152
+ test("resolves a relative Location header against the original URL", async () => {
153
+ responses.push(
154
+ new Response(null, {
155
+ status: 302,
156
+ headers: { Location: "/files-tmb/cdn-signed?t=1700" },
157
+ }),
158
+ );
159
+ responses.push(
160
+ new Response(new Uint8Array([9]).buffer, {
161
+ status: 200,
162
+ headers: { "Content-Type": "image/png" },
163
+ }),
164
+ );
165
+ await downloadSlackFile(
166
+ {
167
+ id: "F1",
168
+ name: "x.png",
169
+ urlPrivateDownload: "https://files.slack.com/a/b/download",
170
+ },
171
+ "xoxb-test",
172
+ );
173
+ expect(calls[1].url).toBe(
174
+ "https://files.slack.com/files-tmb/cdn-signed?t=1700",
175
+ );
176
+ });
177
+
178
+ test("throws when the second hop responds non-2xx", async () => {
179
+ responses.push(
180
+ new Response(null, {
181
+ status: 302,
182
+ headers: { Location: "https://files-edge.slack.com/cdn?t=1" },
183
+ }),
184
+ );
185
+ responses.push(
186
+ new Response(null, { status: 403, statusText: "Forbidden" }),
187
+ );
188
+ await expect(
189
+ downloadSlackFile(
190
+ {
191
+ id: "F1",
192
+ name: "x.png",
193
+ urlPrivateDownload: "https://files.slack.com/a/b/download",
194
+ },
195
+ "xoxb-test",
196
+ ),
197
+ ).rejects.toThrow(/403/);
198
+ });
199
+
200
+ test("throws when a redirect has no Location header", async () => {
201
+ responses.push(new Response(null, { status: 302 }));
202
+ await expect(
203
+ downloadSlackFile(
204
+ {
205
+ id: "F1",
206
+ name: "x.png",
207
+ urlPrivateDownload: "https://files.slack.com/a/b/download",
208
+ },
209
+ "xoxb-test",
210
+ ),
211
+ ).rejects.toThrow(/no Location header/);
212
+ });
213
+
214
+ test("falls back to response Content-Type when file.mimetype is absent", async () => {
215
+ responses.push(
216
+ new Response(new Uint8Array([1]).buffer, {
217
+ status: 200,
218
+ headers: { "Content-Type": "image/webp; charset=binary" },
219
+ }),
220
+ );
221
+ const result = await downloadSlackFile(
222
+ {
223
+ id: "F1",
224
+ name: "photo.webp",
225
+ urlPrivate: "https://files.slack.com/files-pri/T/F1/inline",
226
+ },
227
+ "xoxb-test",
228
+ );
229
+ expect(result?.mimeType).toBe("image/webp");
230
+ });
231
+ });
@@ -99,6 +99,30 @@ function getWriteAuth(connection?: OAuthConnection): OAuthConnection | string {
99
99
  return getSlackAuth(connection);
100
100
  }
101
101
 
102
+ /**
103
+ * Resolve the bot token (raw string) and pass it to `fn`. Returns the
104
+ * callback's result, or `null` when no Slack auth is available.
105
+ *
106
+ * Bridges the Socket Mode case (cached string token) and the OAuth case
107
+ * (`OAuthConnection.withToken`) for callers that need a raw token to hand
108
+ * to a non-Slack-client API call — currently `downloadSlackFile` for inline
109
+ * file/image fetches. Slack-client method calls should keep going through
110
+ * `getReadAuth` / `getWriteAuth` and pass the union through.
111
+ */
112
+ export async function withSlackBotToken<T>(
113
+ account: string | undefined,
114
+ fn: (token: string) => Promise<T>,
115
+ ): Promise<T | null> {
116
+ // Resolve for this call's account even when the process cache is warm.
117
+ // Multi-workspace backfills can interleave, so use the returned connection
118
+ // directly instead of accepting any previously cached workspace token.
119
+ const resolvedAuth = await slackProvider.resolveConnection?.(account);
120
+ const auth = resolvedAuth ?? _cachedSlackWriteAuth;
121
+ if (!auth) return null;
122
+ if (typeof auth === "string") return fn(auth);
123
+ return auth.withToken(fn);
124
+ }
125
+
102
126
  /**
103
127
  * Run a read-path Slack call, falling back to the bot token if the cached
104
128
  * user token is rejected with an auth error. On fallback, the read cache is
@@ -192,15 +216,31 @@ function mapConversation(conv: SlackConversation): Conversation {
192
216
  };
193
217
  }
194
218
 
195
- function mapSlackFiles(
196
- files: SlackMessage["files"],
197
- ): Array<{ id?: string; name: string; mimetype?: string }> | undefined {
219
+ function mapSlackFiles(files: SlackMessage["files"]):
220
+ | Array<{
221
+ id?: string;
222
+ name: string;
223
+ mimetype?: string;
224
+ /**
225
+ * Transient — only present on the in-flight `ProviderMessage.metadata`.
226
+ * The persisted `slackFiles` shape carries `{ id, name, mimetype }` only
227
+ * (see `slackFileMetadataSchema`). Callers that hydrate image attachments
228
+ * during backfill rely on this URL; persistence strips it before write.
229
+ */
230
+ urlPrivateDownload?: string;
231
+ urlPrivate?: string;
232
+ }>
233
+ | undefined {
198
234
  if (!files || files.length === 0) return undefined;
199
235
  const mapped = files
200
236
  .map((file) => ({
201
237
  ...(file.id ? { id: file.id } : {}),
202
238
  name: file.name,
203
239
  ...(file.mimetype ? { mimetype: file.mimetype } : {}),
240
+ ...(file.url_private_download
241
+ ? { urlPrivateDownload: file.url_private_download }
242
+ : {}),
243
+ ...(file.url_private ? { urlPrivate: file.url_private } : {}),
204
244
  }))
205
245
  .filter((file) => file.name.length > 0);
206
246
  return mapped.length > 0 ? mapped : undefined;
@@ -419,8 +459,6 @@ export const slackProvider: MessagingProvider = {
419
459
  if (conv.type === "dm" && conv.metadata?.dmUserId) {
420
460
  const dmUserId = conv.metadata.dmUserId as string;
421
461
  conv.name = await resolveUserName(auth, dmUserId);
422
-
423
-
424
462
  }
425
463
  }
426
464
 
@@ -20,8 +20,10 @@ import type {
20
20
  SlackConversationsListResponse,
21
21
  SlackConversationsOpenResponse,
22
22
  SlackPostMessageResponse,
23
+ SlackReactionsAddResponse,
23
24
  SlackSearchMessagesResponse,
24
25
  SlackUserInfoResponse,
26
+ SlackUsersListResponse,
25
27
  } from "./types.js";
26
28
 
27
29
  const SLACK_API_BASE = "https://slack.com/api";
@@ -432,3 +434,28 @@ export async function searchMessages(
432
434
  },
433
435
  );
434
436
  }
437
+
438
+ export async function addReaction(
439
+ connectionOrToken: OAuthConnection | string,
440
+ channel: string,
441
+ timestamp: string,
442
+ name: string,
443
+ ): Promise<SlackReactionsAddResponse> {
444
+ return request<SlackReactionsAddResponse>(
445
+ connectionOrToken,
446
+ "reactions.add",
447
+ undefined,
448
+ { channel, timestamp, name },
449
+ );
450
+ }
451
+
452
+ export async function listUsers(
453
+ connectionOrToken: OAuthConnection | string,
454
+ limit = 200,
455
+ cursor?: string,
456
+ ): Promise<SlackUsersListResponse> {
457
+ return request<SlackUsersListResponse>(connectionOrToken, "users.list", {
458
+ limit: String(limit),
459
+ cursor,
460
+ });
461
+ }
@@ -0,0 +1,65 @@
1
+ export interface SlackMessageDeepLinks {
2
+ appUrl?: string;
3
+ webUrl?: string;
4
+ }
5
+
6
+ export function formatSlackPermalinkTimestamp(ts: string): string {
7
+ return ts.replace(".", "");
8
+ }
9
+
10
+ export function buildSlackAppMessageUrl(params: {
11
+ teamId?: string | null;
12
+ channelId: string;
13
+ messageTs: string;
14
+ }): string | undefined {
15
+ const teamId = params.teamId?.trim();
16
+ if (!teamId) return undefined;
17
+
18
+ const search = new URLSearchParams({
19
+ team: teamId,
20
+ id: params.channelId,
21
+ message: params.messageTs,
22
+ });
23
+ return `slack://channel?${search.toString()}`;
24
+ }
25
+
26
+ function normalizeSlackTeamUrl(teamUrl?: string | null): string | undefined {
27
+ const trimmed = teamUrl?.trim();
28
+ if (!trimmed) return undefined;
29
+
30
+ try {
31
+ const parsed = new URL(trimmed);
32
+ if (parsed.protocol !== "https:") return undefined;
33
+ return parsed.toString().replace(/\/+$/, "");
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
39
+ export function buildSlackWebMessageUrl(params: {
40
+ teamUrl?: string | null;
41
+ channelId: string;
42
+ messageTs: string;
43
+ }): string | undefined {
44
+ const teamUrl = normalizeSlackTeamUrl(params.teamUrl);
45
+ if (!teamUrl) return undefined;
46
+
47
+ return `${teamUrl}/archives/${encodeURIComponent(
48
+ params.channelId,
49
+ )}/p${formatSlackPermalinkTimestamp(params.messageTs)}`;
50
+ }
51
+
52
+ export function buildSlackMessageDeepLinks(params: {
53
+ teamId?: string | null;
54
+ teamUrl?: string | null;
55
+ channelId: string;
56
+ messageTs: string;
57
+ }): SlackMessageDeepLinks | undefined {
58
+ const appUrl = buildSlackAppMessageUrl(params);
59
+ const webUrl = buildSlackWebMessageUrl(params);
60
+ if (!appUrl && !webUrl) return undefined;
61
+ return {
62
+ ...(appUrl ? { appUrl } : {}),
63
+ ...(webUrl ? { webUrl } : {}),
64
+ };
65
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Slack file download for the assistant-side backfill path.
3
+ *
4
+ * The gateway runs the live inbound path and downloads files via its own
5
+ * `gateway/src/slack/download.ts`. The assistant cannot import that module
6
+ * (different package, different fetch infra), so the thread-backfill path
7
+ * has its own minimal downloader here.
8
+ *
9
+ * Both implementations target the same Slack contract:
10
+ * - `url_private_download` (preferred) / `url_private` (fallback) are
11
+ * Slack-hosted URLs requiring bot-token auth.
12
+ * - Slack typically redirects to a CDN host (e.g. `files-edge.slack.com`)
13
+ * where the signed redirect URL is self-authenticating; the WHATWG fetch
14
+ * spec strips `Authorization` on cross-origin redirects, so we manually
15
+ * follow the redirect without re-sending the bot token.
16
+ */
17
+
18
+ import { getLogger } from "../../../util/logger.js";
19
+
20
+ const log = getLogger("slack-download");
21
+
22
+ export interface DownloadedSlackFile {
23
+ filename: string;
24
+ mimeType: string;
25
+ /** Base64-encoded file bytes. */
26
+ data: string;
27
+ }
28
+
29
+ export interface SlackFileDownloadInput {
30
+ id?: string;
31
+ name: string;
32
+ mimetype?: string;
33
+ urlPrivateDownload?: string;
34
+ urlPrivate?: string;
35
+ }
36
+
37
+ const DOWNLOAD_TIMEOUT_MS = 30_000;
38
+
39
+ /**
40
+ * Download a Slack file using a raw bot token for authentication.
41
+ *
42
+ * The caller is responsible for resolving the token from the slack adapter
43
+ * (`withSlackBotToken`); this module stays decoupled from the auth-resolution
44
+ * dispatch so it remains trivially mockable in tests.
45
+ *
46
+ * Returns `null` when no usable URL is present on the file metadata — callers
47
+ * commonly pass file shapes that have already been sanitized for persistence
48
+ * (`{ id, name, mimetype }`) and have no way to download. This is treated as
49
+ * an expected branch rather than an error.
50
+ *
51
+ * Throws on transport / HTTP errors so the caller can decide whether to log
52
+ * and skip or fail the surrounding operation. The thread-backfill caller
53
+ * logs and proceeds with the text-only message rather than failing the whole
54
+ * backfill.
55
+ */
56
+ export async function downloadSlackFile(
57
+ file: SlackFileDownloadInput,
58
+ token: string,
59
+ ): Promise<DownloadedSlackFile | null> {
60
+ const url = file.urlPrivateDownload ?? file.urlPrivate;
61
+ if (!url) {
62
+ log.debug(
63
+ { fileId: file.id, name: file.name },
64
+ "Slack file has no download URL; skipping",
65
+ );
66
+ return null;
67
+ }
68
+
69
+ let response = await fetch(url, {
70
+ headers: { Authorization: `Bearer ${token}` },
71
+ redirect: "manual",
72
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
73
+ });
74
+
75
+ if (response.status >= 300 && response.status < 400) {
76
+ const location = response.headers.get("Location");
77
+ if (!location) {
78
+ throw new Error(
79
+ `Slack file ${file.id ?? file.name} returned ${response.status} redirect with no Location header`,
80
+ );
81
+ }
82
+ // CDN redirect URLs are signed; no Authorization needed. Resolve
83
+ // relative locations against the original URL.
84
+ const resolvedLocation = new URL(location, url).href;
85
+ response = await fetch(resolvedLocation, {
86
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
87
+ });
88
+ }
89
+
90
+ if (!response.ok) {
91
+ throw new Error(
92
+ `Failed to download Slack file ${file.id ?? file.name}: ${response.status} ${response.statusText}`,
93
+ );
94
+ }
95
+
96
+ const buffer = await response.arrayBuffer();
97
+ const mimeType =
98
+ file.mimetype ||
99
+ response.headers.get("Content-Type")?.split(";")[0]?.trim() ||
100
+ "application/octet-stream";
101
+ const filename = file.name || `slack_file_${file.id ?? "unknown"}`;
102
+ const data = Buffer.from(buffer).toString("base64");
103
+ return { filename, mimeType, data };
104
+ }
@@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test";
3
3
  import {
4
4
  mergeSlackMetadata,
5
5
  readSlackMetadata,
6
+ readSlackMetadataFromMessageMetadata,
6
7
  type SlackMessageMetadata,
7
8
  writeSlackMetadata,
8
9
  } from "./message-metadata.js";
@@ -113,6 +114,7 @@ describe("readSlackMetadata", () => {
113
114
  channelTs: "1700000000.000100",
114
115
  threadTs: "1699999999.000000",
115
116
  displayName: "Alice",
117
+ actorExternalUserId: "U_ALICE",
116
118
  eventKind: "message",
117
119
  editedAt: 1700000123,
118
120
  };
@@ -138,6 +140,35 @@ describe("readSlackMetadata", () => {
138
140
  });
139
141
  });
140
142
 
143
+ describe("readSlackMetadataFromMessageMetadata", () => {
144
+ const meta: SlackMessageMetadata = {
145
+ source: "slack",
146
+ channelId: "C123",
147
+ channelTs: "1700000000.000100",
148
+ threadTs: "1699999999.000000",
149
+ eventKind: "message",
150
+ };
151
+
152
+ test("reads nested slackMeta from a message metadata envelope", () => {
153
+ expect(
154
+ readSlackMetadataFromMessageMetadata(
155
+ JSON.stringify({
156
+ userMessageChannel: "slack",
157
+ slackMeta: writeSlackMetadata(meta),
158
+ }),
159
+ ),
160
+ ).toEqual(meta);
161
+ });
162
+
163
+ test("can read flat legacy Slack metadata when explicitly allowed", () => {
164
+ const raw = writeSlackMetadata(meta);
165
+ expect(readSlackMetadataFromMessageMetadata(raw)).toBeNull();
166
+ expect(
167
+ readSlackMetadataFromMessageMetadata(raw, { allowFlatLegacy: true }),
168
+ ).toEqual(meta);
169
+ });
170
+ });
171
+
141
172
  describe("writeSlackMetadata", () => {
142
173
  test("round-trips through readSlackMetadata", () => {
143
174
  const meta: SlackMessageMetadata = {
@@ -146,6 +177,7 @@ describe("writeSlackMetadata", () => {
146
177
  channelTs: "1700000000.000100",
147
178
  threadTs: "1699999999.000000",
148
179
  displayName: "Alice",
180
+ actorExternalUserId: "U_ALICE",
149
181
  eventKind: "message",
150
182
  };
151
183
  const raw = writeSlackMetadata(meta);
@@ -38,6 +38,7 @@ export const slackMessageMetadataSchema = z.object({
38
38
  channelTs: z.string(),
39
39
  threadTs: z.string().optional(),
40
40
  displayName: z.string().optional(),
41
+ actorExternalUserId: z.string().optional(),
41
42
  eventKind: z.enum(["message", "reaction"]),
42
43
  reaction: slackReactionMetadataSchema.optional(),
43
44
  editedAt: z.number().optional(),
@@ -75,6 +76,32 @@ export function readSlackMetadata(
75
76
  return result.success ? result.data : null;
76
77
  }
77
78
 
79
+ export function readSlackMetadataFromMessageMetadata(
80
+ metadata: string | null | undefined,
81
+ opts?: { allowFlatLegacy?: boolean },
82
+ ): SlackMessageMetadata | null {
83
+ if (!metadata) return null;
84
+
85
+ let parent: Record<string, unknown> | null = null;
86
+ try {
87
+ const parsed = JSON.parse(metadata) as unknown;
88
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
89
+ parent = parsed as Record<string, unknown>;
90
+ }
91
+ } catch {
92
+ return null;
93
+ }
94
+ if (!parent) return null;
95
+
96
+ const nested = parent.slackMeta;
97
+ if (typeof nested === "string") {
98
+ const parsedNested = readSlackMetadata(nested);
99
+ if (parsedNested) return parsedNested;
100
+ }
101
+
102
+ return opts?.allowFlatLegacy ? readSlackMetadata(metadata) : null;
103
+ }
104
+
78
105
  /**
79
106
  * Serialize `SlackMessageMetadata` to a JSON string suitable for a fresh
80
107
  * write to the `messages.metadata` column. Use `mergeSlackMetadata` when an