@vellumai/assistant 0.11.1 → 0.11.2-staging.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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Low-level Slack Web API wrapper.
2
+ * Typed Slack Web API method wrappers.
3
3
  *
4
4
  * All methods accept either an OAuthConnection or a raw token string.
5
- * Throws SlackApiError on failures, with status: 401 on auth errors
6
- * for withValidToken compatibility.
5
+ * Transport (retries, envelope checking, the unified `SlackApiError`) lives
6
+ * in `web-api-transport.ts`.
7
7
  *
8
8
  * String overloads are retained for non-OAuth callers (e.g. slack/share.ts)
9
9
  * that pass raw bot tokens via resolveSlackToken(). These bypass the
@@ -16,6 +16,7 @@ import type {
16
16
  SlackAuthTestResponse,
17
17
  SlackBotsInfoResponse,
18
18
  SlackConversationHistoryResponse,
19
+ SlackConversationInfoResponse,
19
20
  SlackConversationMarkResponse,
20
21
  SlackConversationRepliesResponse,
21
22
  SlackConversationsListResponse,
@@ -26,250 +27,7 @@ import type {
26
27
  SlackUserInfoResponse,
27
28
  SlackUsersListResponse,
28
29
  } from "./types.js";
29
-
30
- const SLACK_API_BASE = "https://slack.com/api";
31
- const MAX_RATE_LIMIT_RETRIES = 3;
32
- const DEFAULT_RETRY_AFTER_S = 1;
33
-
34
- export class SlackApiError extends Error {
35
- constructor(
36
- public readonly status: number,
37
- public readonly slackError: string,
38
- message: string,
39
- ) {
40
- super(message);
41
- this.name = "SlackApiError";
42
- }
43
- }
44
-
45
- /**
46
- * Sleep helper that respects Slack's Retry-After header value.
47
- */
48
- function sleepMs(ms: number): Promise<void> {
49
- return new Promise((resolve) => setTimeout(resolve, ms));
50
- }
51
-
52
- /**
53
- * Check a Slack API response envelope for errors and map to SlackApiError.
54
- * Returns the data if ok, throws otherwise.
55
- */
56
- function checkSlackEnvelope<T extends SlackApiResponse>(data: T): T {
57
- if (!data.ok) {
58
- const slackError = data.error ?? "unknown_error";
59
- const status = [
60
- "invalid_auth",
61
- "token_expired",
62
- "token_revoked",
63
- "not_authed",
64
- ].includes(slackError)
65
- ? 401
66
- : 400;
67
- throw new SlackApiError(
68
- status,
69
- slackError,
70
- `Slack API error: ${slackError}`,
71
- );
72
- }
73
- return data;
74
- }
75
-
76
- /**
77
- * Build a Slack API request using a raw token (for retry via `connection.withToken`).
78
- */
79
- async function rawSlackRequest<T extends SlackApiResponse>(
80
- token: string,
81
- method: string,
82
- query: Record<string, string> | undefined,
83
- body: Record<string, unknown> | undefined,
84
- ): Promise<T> {
85
- let url = `${SLACK_API_BASE}/${method}`;
86
- const headers: Record<string, string> = {
87
- Authorization: `Bearer ${token}`,
88
- };
89
-
90
- let init: RequestInit;
91
- if (body) {
92
- headers["Content-Type"] = "application/json; charset=utf-8";
93
- init = { method: "POST", headers, body: JSON.stringify(body) };
94
- } else {
95
- if (query && Object.keys(query).length > 0) {
96
- url += `?${new URLSearchParams(query)}`;
97
- }
98
- init = { method: "GET", headers };
99
- }
100
-
101
- const resp = await fetch(url, init);
102
- if (resp.status === 429) {
103
- throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
104
- }
105
- if (!resp.ok) {
106
- throw new SlackApiError(
107
- resp.status,
108
- `http_${resp.status}`,
109
- `Slack API HTTP ${resp.status}`,
110
- );
111
- }
112
- return checkSlackEnvelope((await resp.json()) as T);
113
- }
114
-
115
- /**
116
- * Execute a Slack API request via OAuthConnection with rate-limit retry.
117
- *
118
- * Slack returns HTTP 200 with `{ ok: false, error: "invalid_auth" }` for
119
- * auth errors. Because `connection.request()` delegates to `withValidToken`
120
- * which only retries on HTTP-level 401s, we catch Slack envelope auth
121
- * errors (mapped to SlackApiError with status 401) and perform a single
122
- * retry via `connection.withToken()` which forces a token refresh before
123
- * giving us the new token.
124
- */
125
- async function requestViaConnection<T extends SlackApiResponse>(
126
- connection: OAuthConnection,
127
- method: string,
128
- params?: Record<string, string | undefined>,
129
- body?: Record<string, unknown>,
130
- ): Promise<T> {
131
- const query: Record<string, string> | undefined = params
132
- ? Object.fromEntries(
133
- Object.entries(params).filter(
134
- (entry): entry is [string, string] => entry[1] !== undefined,
135
- ),
136
- )
137
- : undefined;
138
-
139
- for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
140
- const resp = await connection.request({
141
- method: body ? "POST" : "GET",
142
- path: `/${method}`,
143
- query: query && Object.keys(query).length > 0 ? query : undefined,
144
- body,
145
- });
146
-
147
- // Handle 429 rate limits with Retry-After backoff
148
- if (resp.status === 429) {
149
- if (attempt >= MAX_RATE_LIMIT_RETRIES) {
150
- throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
151
- }
152
- const retryAfter =
153
- parseInt(
154
- resp.headers["retry-after"] ?? resp.headers["Retry-After"] ?? "",
155
- 10,
156
- ) || DEFAULT_RETRY_AFTER_S;
157
- await sleepMs(retryAfter * 1000);
158
- continue;
159
- }
160
-
161
- if (resp.status >= 400) {
162
- throw new SlackApiError(
163
- resp.status,
164
- `http_${resp.status}`,
165
- `Slack API HTTP ${resp.status}`,
166
- );
167
- }
168
-
169
- const data = resp.body as T;
170
-
171
- // Handle rate_limited error in response body (some Slack APIs return 200 with error)
172
- if (
173
- !data.ok &&
174
- data.error === "rate_limited" &&
175
- attempt < MAX_RATE_LIMIT_RETRIES
176
- ) {
177
- await sleepMs(DEFAULT_RETRY_AFTER_S * 1000);
178
- continue;
179
- }
180
-
181
- try {
182
- return checkSlackEnvelope(data);
183
- } catch (err) {
184
- // Slack envelope auth errors (invalid_auth, token_expired, etc.) come
185
- // back as HTTP 200, so they escape withValidToken's retry scope inside
186
- // connection.request(). Catch them here and retry once with a freshly-
187
- // refreshed token via connection.withToken().
188
- if (err instanceof SlackApiError && err.status === 401) {
189
- return connection.withToken((freshToken) =>
190
- rawSlackRequest<T>(freshToken, method, query, body),
191
- );
192
- }
193
- throw err;
194
- }
195
- }
196
-
197
- // Unreachable, but TypeScript needs this
198
- throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
199
- }
200
-
201
- /**
202
- * Execute a Slack API request via raw token with rate-limit retry.
203
- */
204
- async function requestViaToken<T extends SlackApiResponse>(
205
- token: string,
206
- method: string,
207
- params?: Record<string, string | undefined>,
208
- body?: Record<string, unknown>,
209
- ): Promise<T> {
210
- let url = `${SLACK_API_BASE}/${method}`;
211
- const headers: Record<string, string> = {
212
- Authorization: `Bearer ${token}`,
213
- };
214
-
215
- let init: RequestInit;
216
- if (body) {
217
- headers["Content-Type"] = "application/json; charset=utf-8";
218
- init = { method: "POST", headers, body: JSON.stringify(body) };
219
- } else {
220
- if (params) {
221
- const searchParams = new URLSearchParams();
222
- for (const [k, v] of Object.entries(params)) {
223
- if (v !== undefined) {
224
- searchParams.set(k, v);
225
- }
226
- }
227
- url += `?${searchParams}`;
228
- }
229
- init = { method: "GET", headers };
230
- }
231
-
232
- for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
233
- const resp = await fetch(url, init);
234
-
235
- // Handle 429 rate limits with Retry-After backoff
236
- if (resp.status === 429) {
237
- if (attempt >= MAX_RATE_LIMIT_RETRIES) {
238
- throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
239
- }
240
- const retryAfter =
241
- parseInt(resp.headers.get("Retry-After") ?? "", 10) ||
242
- DEFAULT_RETRY_AFTER_S;
243
- await sleepMs(retryAfter * 1000);
244
- continue;
245
- }
246
-
247
- if (!resp.ok) {
248
- throw new SlackApiError(
249
- resp.status,
250
- `http_${resp.status}`,
251
- `Slack API HTTP ${resp.status}`,
252
- );
253
- }
254
-
255
- const data = (await resp.json()) as T;
256
-
257
- // Handle rate_limited error in response body (some Slack APIs return 200 with error)
258
- if (
259
- !data.ok &&
260
- data.error === "rate_limited" &&
261
- attempt < MAX_RATE_LIMIT_RETRIES
262
- ) {
263
- await sleepMs(DEFAULT_RETRY_AFTER_S * 1000);
264
- continue;
265
- }
266
-
267
- return checkSlackEnvelope(data);
268
- }
269
-
270
- // Unreachable, but TypeScript needs this
271
- throw new SlackApiError(429, "rate_limited", "Slack API rate limited");
272
- }
30
+ import { slackRequest } from "./web-api-transport.js";
273
31
 
274
32
  async function request<T extends SlackApiResponse>(
275
33
  connectionOrToken: OAuthConnection | string,
@@ -277,10 +35,10 @@ async function request<T extends SlackApiResponse>(
277
35
  params?: Record<string, string | undefined>,
278
36
  body?: Record<string, unknown>,
279
37
  ): Promise<T> {
280
- if (typeof connectionOrToken === "string") {
281
- return requestViaToken<T>(connectionOrToken, method, params, body);
282
- }
283
- return requestViaConnection<T>(connectionOrToken, method, params, body);
38
+ return slackRequest<T>(connectionOrToken, method, {
39
+ ...(params ? { query: params } : {}),
40
+ ...(body ? { body } : {}),
41
+ });
284
42
  }
285
43
 
286
44
  export async function authTest(
@@ -397,6 +155,17 @@ export async function conversationsOpen(
397
155
  );
398
156
  }
399
157
 
158
+ export async function conversationInfo(
159
+ connectionOrToken: OAuthConnection | string,
160
+ channel: string,
161
+ ): Promise<SlackConversationInfoResponse> {
162
+ return request<SlackConversationInfoResponse>(
163
+ connectionOrToken,
164
+ "conversations.info",
165
+ { channel },
166
+ );
167
+ }
168
+
400
169
  export async function userInfo(
401
170
  connectionOrToken: OAuthConnection | string,
402
171
  userId: string,
@@ -15,14 +15,6 @@ mock.module("./api.js", () => ({
15
15
  callSlackApiMock(method, body),
16
16
  callSlackApiForm: async () => ({}),
17
17
  completeSlackUpload: async () => {},
18
- SlackApiError: class SlackApiError extends Error {
19
- readonly slackError: string | undefined;
20
-
21
- constructor(slackError: string | undefined) {
22
- super(slackError ?? "unknown");
23
- this.slackError = slackError;
24
- }
25
- },
26
18
  uploadToSlackUrl: async () => {},
27
19
  startSlackStream: (params: { markdownText?: string }) =>
28
20
  callSlackApiMock("chat.startStream", { ...params }),
@@ -32,7 +24,10 @@ mock.module("./api.js", () => ({
32
24
  callSlackApiMock("chat.stopStream", { ...params }),
33
25
  }));
34
26
 
35
- const { SlackApiError } = await import("./api.js");
27
+ // The real error class: send.ts branches on `instanceof SlackApiError` from
28
+ // the (unmocked) shared transport, so thrown test errors must be real
29
+ // instances or every branch under test would silently take the generic path.
30
+ const { SlackApiError } = await import("./web-api-transport.js");
36
31
  const { sendSlackAssistantThreadStatus, sendSlackReply } =
37
32
  await import("./send.js");
38
33
 
@@ -20,12 +20,12 @@ import {
20
20
  callSlackApi,
21
21
  callSlackApiForm,
22
22
  completeSlackUpload,
23
- SlackApiError,
24
23
  startSlackStream,
25
24
  stopSlackStream,
26
25
  uploadToSlackUrl,
27
26
  } from "./api.js";
28
27
  import { renderSlackBlocks } from "./render.js";
28
+ import { SlackApiError } from "./web-api-transport.js";
29
29
 
30
30
  const log = getLogger("slack-send");
31
31
 
@@ -27,6 +27,7 @@ export interface SlackBotsInfoResponse extends SlackApiResponse {
27
27
  export interface SlackConversation {
28
28
  id: string;
29
29
  name?: string;
30
+ name_normalized?: string;
30
31
  is_channel?: boolean;
31
32
  is_group?: boolean;
32
33
  is_im?: boolean;
@@ -45,6 +46,10 @@ export interface SlackConversation {
45
46
  is_user_deleted?: boolean;
46
47
  }
47
48
 
49
+ export interface SlackConversationInfoResponse extends SlackApiResponse {
50
+ channel: SlackConversation;
51
+ }
52
+
48
53
  export interface SlackConversationsListResponse extends SlackApiResponse {
49
54
  channels: SlackConversation[];
50
55
  response_metadata?: { next_cursor?: string };
@@ -0,0 +1,181 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import {
4
+ checkSlackEnvelope,
5
+ classifySlackError,
6
+ rawSlackRequest,
7
+ SlackApiError,
8
+ } from "./web-api-transport.js";
9
+
10
+ const originalFetch = globalThis.fetch;
11
+
12
+ afterEach(() => {
13
+ globalThis.fetch = originalFetch;
14
+ });
15
+
16
+ function jsonResponse(body: unknown, status = 200): Response {
17
+ return new Response(JSON.stringify(body), {
18
+ status,
19
+ headers: { "content-type": "application/json" },
20
+ });
21
+ }
22
+
23
+ /** Await a promise expected to reject with SlackApiError and return it. */
24
+ async function captureSlackApiError(
25
+ promise: Promise<unknown>,
26
+ ): Promise<SlackApiError> {
27
+ try {
28
+ await promise;
29
+ } catch (err) {
30
+ if (err instanceof SlackApiError) {
31
+ return err;
32
+ }
33
+ throw err;
34
+ }
35
+ throw new Error("expected the promise to reject with SlackApiError");
36
+ }
37
+
38
+ describe("SlackApiError", () => {
39
+ test("derives status from category when not explicit", () => {
40
+ expect(new SlackApiError("invalid_auth").status).toBe(401);
41
+ expect(new SlackApiError("token_expired").status).toBe(401);
42
+ expect(new SlackApiError("rate_limited").status).toBe(429);
43
+ expect(new SlackApiError("ratelimited").status).toBe(429);
44
+ expect(new SlackApiError("channel_not_found").status).toBe(400);
45
+ expect(new SlackApiError("invalid_blocks").status).toBe(400);
46
+ });
47
+
48
+ test("explicit status wins over the derived one", () => {
49
+ expect(new SlackApiError("http_503", { status: 503 }).status).toBe(503);
50
+ });
51
+
52
+ test("normalizes a missing error code to unknown_error", () => {
53
+ const err = new SlackApiError(undefined);
54
+ expect(err.slackError).toBe("unknown_error");
55
+ expect(err.category).toBe("unknown");
56
+ expect(err.message).toBe("Slack API error: unknown_error");
57
+ });
58
+
59
+ test("classifies known Slack error codes", () => {
60
+ expect(new SlackApiError("missing_scope").category).toBe("permission");
61
+ expect(new SlackApiError("is_archived").category).toBe("channel_not_found");
62
+ expect(new SlackApiError("message_not_found").category).toBe("not_found");
63
+ expect(classifySlackError("some_new_code")).toBe("unknown");
64
+ });
65
+ });
66
+
67
+ describe("checkSlackEnvelope", () => {
68
+ test("passes ok envelopes through", () => {
69
+ const data = { ok: true as const };
70
+ expect(checkSlackEnvelope(data)).toBe(data);
71
+ });
72
+
73
+ test("throws a classified SlackApiError on ok: false", () => {
74
+ expect(() =>
75
+ checkSlackEnvelope({ ok: false, error: "invalid_auth" }),
76
+ ).toThrow(SlackApiError);
77
+ try {
78
+ checkSlackEnvelope({ ok: false, error: "invalid_auth" });
79
+ } catch (err) {
80
+ expect((err as SlackApiError).status).toBe(401);
81
+ expect((err as SlackApiError).category).toBe("auth");
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("rawSlackRequest", () => {
87
+ test("retries HTTP 429 honoring Retry-After, then succeeds", async () => {
88
+ let calls = 0;
89
+ globalThis.fetch = mock(async () => {
90
+ calls++;
91
+ if (calls === 1) {
92
+ return new Response("", {
93
+ status: 429,
94
+ headers: { "Retry-After": "0" },
95
+ });
96
+ }
97
+ return jsonResponse({ ok: true });
98
+ }) as unknown as typeof fetch;
99
+
100
+ const data = await rawSlackRequest("xoxb-t", "conversations.info", {
101
+ query: { channel: "C1" },
102
+ });
103
+ expect(data.ok).toBe(true);
104
+ expect(calls).toBe(2);
105
+ });
106
+
107
+ test("retries 5xx and reports SlackApiError after exhausting retries", async () => {
108
+ let calls = 0;
109
+ globalThis.fetch = mock(async () => {
110
+ calls++;
111
+ return new Response("", { status: 503 });
112
+ }) as unknown as typeof fetch;
113
+
114
+ // The fixed 1s backoff between 5xx retries is real time, so this test
115
+ // runs ~3s (3 retries); the timeout below gives it headroom.
116
+ const promise = rawSlackRequest("xoxb-t", "chat.postMessage", {
117
+ body: { channel: "C1", text: "hi" },
118
+ });
119
+ const err = await captureSlackApiError(promise);
120
+ expect(err.status).toBe(503);
121
+ expect(err.slackError).toBe("http_503");
122
+ expect(calls).toBe(4);
123
+ }, 10_000);
124
+
125
+ test("throws classified SlackApiError for non-retryable envelope errors", async () => {
126
+ globalThis.fetch = mock(async () =>
127
+ jsonResponse({ ok: false, error: "channel_not_found" }),
128
+ ) as unknown as typeof fetch;
129
+
130
+ const promise = rawSlackRequest("xoxb-t", "conversations.info", {
131
+ query: { channel: "C_MISSING" },
132
+ });
133
+ const err = await captureSlackApiError(promise);
134
+ expect(err.category).toBe("channel_not_found");
135
+ });
136
+
137
+ test("throws http_<status> SlackApiError for non-JSON 4xx responses", async () => {
138
+ globalThis.fetch = mock(
139
+ async () => new Response("forbidden", { status: 403 }),
140
+ ) as unknown as typeof fetch;
141
+
142
+ const promise = rawSlackRequest("xoxb-t", "conversations.info", {});
143
+ const err = await captureSlackApiError(promise);
144
+ expect(err.status).toBe(403);
145
+ expect(err.slackError).toBe("http_403");
146
+ });
147
+
148
+ test("rejects form bodies dispatched over an OAuth connection", async () => {
149
+ const connection = {
150
+ request: async () => {
151
+ throw new Error("connection.request must not be reached for forms");
152
+ },
153
+ withToken: async () => {
154
+ throw new Error("withToken must not be reached for forms");
155
+ },
156
+ };
157
+ const { slackRequest } = await import("./web-api-transport.js");
158
+ const err = await captureSlackApiError(
159
+ slackRequest(connection as never, "files.getUploadURLExternal", {
160
+ form: new URLSearchParams({ filename: "a.png" }),
161
+ }),
162
+ );
163
+ expect(err.slackError).toBe("form_unsupported_over_oauth");
164
+ });
165
+
166
+ test("sends form bodies as x-www-form-urlencoded POSTs", async () => {
167
+ let capturedInit: RequestInit | undefined;
168
+ globalThis.fetch = mock(async (_input, init) => {
169
+ capturedInit = init;
170
+ return jsonResponse({ ok: true });
171
+ }) as unknown as typeof fetch;
172
+
173
+ await rawSlackRequest("xoxb-t", "files.getUploadURLExternal", {
174
+ form: new URLSearchParams({ filename: "a.png", length: "10" }),
175
+ });
176
+ expect(capturedInit?.method).toBe("POST");
177
+ expect(
178
+ (capturedInit?.headers as Record<string, string>)["Content-Type"],
179
+ ).toBe("application/x-www-form-urlencoded");
180
+ });
181
+ });