@vellumai/assistant 0.11.7 → 0.11.8-staging.1

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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -5,6 +5,11 @@
5
5
  * attachments, by calling the Discord REST API directly via ./api.ts.
6
6
  */
7
7
 
8
+ import type {
9
+ ApprovalUIMetadata,
10
+ ChannelDeliveryResult,
11
+ } from "@vellumai/gateway-client";
12
+
8
13
  import { getAttachmentContent } from "../../../persistence/attachments-store.js";
9
14
  import type { RuntimeAttachmentMetadata } from "../../../runtime/http-types.js";
10
15
  import { getLogger } from "../../../util/logger.js";
@@ -42,6 +47,90 @@ const DISCORD_ALLOWED_MENTIONS = { parse: ["users"] } as const;
42
47
  */
43
48
  const DISCORD_MAX_ATTACHMENT_BYTES = 100 * 1024 * 1024;
44
49
 
50
+ // Component wire values, from Discord's message-components table.
51
+ const DISCORD_COMPONENT_ACTION_ROW = 1;
52
+ const DISCORD_COMPONENT_BUTTON = 2;
53
+ const DISCORD_BUTTON_STYLE_PRIMARY = 1;
54
+ const DISCORD_BUTTON_STYLE_SECONDARY = 2;
55
+ const DISCORD_BUTTON_STYLE_DANGER = 4;
56
+ /** Discord caps a component `custom_id` at 100 characters. */
57
+ const DISCORD_MAX_CUSTOM_ID_CHARS = 100;
58
+ /** Discord caps a button label at 80 characters. */
59
+ const DISCORD_MAX_BUTTON_LABEL_CHARS = 80;
60
+ /** Discord caps an action row at five buttons. */
61
+ const DISCORD_MAX_BUTTONS_PER_ROW = 5;
62
+
63
+ interface DiscordButtonComponent {
64
+ type: typeof DISCORD_COMPONENT_BUTTON;
65
+ style: number;
66
+ label: string;
67
+ custom_id: string;
68
+ }
69
+
70
+ interface DiscordActionRow {
71
+ type: typeof DISCORD_COMPONENT_ACTION_ROW;
72
+ components: DiscordButtonComponent[];
73
+ }
74
+
75
+ /** Translate a surface-agnostic emphasis into Discord's button style value. */
76
+ function discordStyleForEmphasis(
77
+ emphasis: "primary" | "secondary" | "destructive",
78
+ ): number {
79
+ switch (emphasis) {
80
+ case "primary":
81
+ return DISCORD_BUTTON_STYLE_PRIMARY;
82
+ case "destructive":
83
+ return DISCORD_BUTTON_STYLE_DANGER;
84
+ case "secondary":
85
+ return DISCORD_BUTTON_STYLE_SECONDARY;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Build the action rows for an approval card. Each button's `custom_id` is
91
+ * the shared `apr:<requestId>:<action>` callback convention, which the
92
+ * gateway forwards verbatim as the button event's `callbackData`. Styling
93
+ * mirrors the Slack card: an action's own `emphasis` wins, otherwise the
94
+ * first action is primary and `reject` is danger. Discord requires a style
95
+ * on every button, so the remainder are explicitly secondary.
96
+ *
97
+ * Throws when a `custom_id` would exceed Discord's cap: the caller falls
98
+ * back to the plain-text card rather than sending a button that could not
99
+ * round-trip its request id.
100
+ */
101
+ function buildDiscordApprovalComponents(
102
+ approval: ApprovalUIMetadata,
103
+ ): DiscordActionRow[] {
104
+ const buttons = approval.actions.map((action, index) => {
105
+ const customId = `apr:${approval.requestId}:${action.id}`;
106
+ if (customId.length > DISCORD_MAX_CUSTOM_ID_CHARS) {
107
+ throw new Error(
108
+ `custom_id for action "${action.id}" is ${customId.length} characters, exceeding Discord's ${DISCORD_MAX_CUSTOM_ID_CHARS}-character limit`,
109
+ );
110
+ }
111
+ return {
112
+ type: DISCORD_COMPONENT_BUTTON,
113
+ style: action.emphasis
114
+ ? discordStyleForEmphasis(action.emphasis)
115
+ : action.id === "reject"
116
+ ? DISCORD_BUTTON_STYLE_DANGER
117
+ : index === 0
118
+ ? DISCORD_BUTTON_STYLE_PRIMARY
119
+ : DISCORD_BUTTON_STYLE_SECONDARY,
120
+ label: action.label.slice(0, DISCORD_MAX_BUTTON_LABEL_CHARS),
121
+ custom_id: customId,
122
+ } satisfies DiscordButtonComponent;
123
+ });
124
+ const rows: DiscordActionRow[] = [];
125
+ for (let i = 0; i < buttons.length; i += DISCORD_MAX_BUTTONS_PER_ROW) {
126
+ rows.push({
127
+ type: DISCORD_COMPONENT_ACTION_ROW,
128
+ components: buttons.slice(i, i + DISCORD_MAX_BUTTONS_PER_ROW),
129
+ });
130
+ }
131
+ return rows;
132
+ }
133
+
45
134
  /** Send target for one Discord delivery. */
46
135
  export interface DiscordSendTarget {
47
136
  /**
@@ -56,6 +145,38 @@ function messagesRoute(target: DiscordSendTarget): string {
56
145
  return `/channels/${encodeURIComponent(target.channelId)}/messages`;
57
146
  }
58
147
 
148
+ /**
149
+ * A multi-chunk send that failed after at least one chunk was posted. The
150
+ * already-delivered chunks cannot be unsent, so a caller with a fallback
151
+ * must not replay the whole text; the undelivered remainder is what is
152
+ * still owed to the reader.
153
+ */
154
+ export class DiscordPartialSendError extends Error {
155
+ readonly chunksSent: number;
156
+ readonly remainingText: string;
157
+ readonly lastMessageId?: string;
158
+
159
+ constructor(
160
+ cause: unknown,
161
+ chunksSent: number,
162
+ remainingText: string,
163
+ lastMessageId?: string,
164
+ ) {
165
+ super(
166
+ `Discord send failed after ${chunksSent} chunk(s): ${
167
+ cause instanceof Error ? cause.message : String(cause)
168
+ }`,
169
+ { cause },
170
+ );
171
+ this.name = "DiscordPartialSendError";
172
+ this.chunksSent = chunksSent;
173
+ this.remainingText = remainingText;
174
+ if (lastMessageId !== undefined) {
175
+ this.lastMessageId = lastMessageId;
176
+ }
177
+ }
178
+ }
179
+
59
180
  /** Outcome of a Discord reply send. */
60
181
  export interface DiscordSendResult {
61
182
  /**
@@ -108,6 +229,11 @@ export async function editDiscordMessage(
108
229
  {
109
230
  content: options?.emphasis === "muted" ? mutedText(text) : text,
110
231
  allowed_mentions: DISCORD_ALLOWED_MENTIONS,
232
+ // A PATCH that omits `components` keeps whatever the message carries,
233
+ // and every edit through this path states a final text: a card that
234
+ // has been answered or withdrawn must not keep live buttons. Always
235
+ // stripping makes that an invariant rather than a caller's chore.
236
+ components: [],
111
237
  },
112
238
  );
113
239
  log.debug(
@@ -123,23 +249,46 @@ export async function editDiscordMessage(
123
249
  export async function sendDiscordReply(
124
250
  target: DiscordSendTarget,
125
251
  text: string,
252
+ approval?: ApprovalUIMetadata,
126
253
  ): Promise<DiscordSendResult> {
127
254
  const chunks = renderDiscordMessages(text);
128
255
  if (chunks.length === 0) {
129
256
  return {};
130
257
  }
131
258
 
259
+ // Buttons ride the final chunk: its id is the one recorded on the delivery
260
+ // row, so the message a press arrives on is the message the row can find.
261
+ const components = approval ? buildDiscordApprovalComponents(approval) : [];
262
+
132
263
  let lastMessageId: string | undefined;
133
- for (const chunk of chunks) {
134
- const sent = await callDiscordApi<DiscordMessage>(
135
- "POST",
136
- messagesRoute(target),
137
- {
138
- content: chunk,
139
- allowed_mentions: DISCORD_ALLOWED_MENTIONS,
140
- },
141
- );
142
- lastMessageId = typeof sent?.id === "string" ? sent.id : undefined;
264
+ for (const [index, chunk] of chunks.entries()) {
265
+ try {
266
+ const sent = await callDiscordApi<DiscordMessage>(
267
+ "POST",
268
+ messagesRoute(target),
269
+ {
270
+ content: chunk,
271
+ allowed_mentions: DISCORD_ALLOWED_MENTIONS,
272
+ ...(components.length > 0 && index === chunks.length - 1
273
+ ? { components }
274
+ : {}),
275
+ },
276
+ );
277
+ lastMessageId = typeof sent?.id === "string" ? sent.id : undefined;
278
+ } catch (err) {
279
+ // Nothing posted yet propagates plainly; a caller may retry or fall
280
+ // back with the full text. Past the first chunk the delivered prefix
281
+ // cannot be unsent, so the error names what is still owed.
282
+ if (index === 0) {
283
+ throw err;
284
+ }
285
+ throw new DiscordPartialSendError(
286
+ err,
287
+ index,
288
+ chunks.slice(index).join("\n"),
289
+ lastMessageId,
290
+ );
291
+ }
143
292
  }
144
293
 
145
294
  log.debug(
@@ -268,6 +417,44 @@ export async function sendDiscordAttachments(
268
417
  * Discord expires it after about ten seconds, so a caller that wants it to
269
418
  * persist re-sends on a timer rather than clearing it; there is no stop route.
270
419
  */
420
+ /**
421
+ * The REST path spelling of a reaction emoji: a custom emoji arrives in its
422
+ * mention form (`<:name:id>` / `<a:name:id>`, the shape inbound forwards)
423
+ * and the route wants the bare `name:id`; a unicode emoji rides URL-encoded
424
+ * as itself.
425
+ */
426
+ function discordReactionPathEmoji(emoji: string): string {
427
+ const custom = /^<a?:([^:>]+):(\d+)>$/.exec(emoji);
428
+ return encodeURIComponent(custom ? `${custom[1]}:${custom[2]}` : emoji);
429
+ }
430
+
431
+ /**
432
+ * Add or remove the bot's own emoji reaction on a message.
433
+ *
434
+ * `PUT`/`DELETE .../reactions/{emoji}/@me` are idempotent (204 either way
435
+ * when the end state already holds), so no already-reacted special case
436
+ * exists. Guild channels need the `Add Reactions` permission for `PUT`;
437
+ * a rejection reports `ok: false` rather than throwing.
438
+ */
439
+ export async function sendDiscordReaction(
440
+ chatId: string,
441
+ emoji: string,
442
+ messageId: string,
443
+ action: "add" | "remove",
444
+ ): Promise<ChannelDeliveryResult> {
445
+ const route = `/channels/${chatId}/messages/${messageId}/reactions/${discordReactionPathEmoji(emoji)}/@me`;
446
+ try {
447
+ await callDiscordApi(action === "add" ? "PUT" : "DELETE", route);
448
+ return { ok: true };
449
+ } catch (err) {
450
+ log.warn(
451
+ { err, chatId, messageId, action },
452
+ "Failed to deliver Discord reaction",
453
+ );
454
+ return { ok: false };
455
+ }
456
+ }
457
+
271
458
  export async function sendDiscordTypingIndicator(
272
459
  target: DiscordSendTarget,
273
460
  ): Promise<void> {
@@ -11,6 +11,7 @@ import type { DiscordSendTarget } from "./send.js";
11
11
  import {
12
12
  editDiscordMessage,
13
13
  sendDiscordAttachments,
14
+ sendDiscordReaction,
14
15
  sendDiscordReply,
15
16
  sendDiscordTypingIndicator,
16
17
  } from "./send.js";
@@ -76,6 +77,15 @@ export const discordTransport: ChannelTransport = {
76
77
  return { ok: true };
77
78
  },
78
79
 
80
+ async react(target) {
81
+ return sendDiscordReaction(
82
+ target.threadId ?? target.chatId,
83
+ target.emoji,
84
+ target.messageId,
85
+ target.action,
86
+ );
87
+ },
88
+
79
89
  async deliver(ctx, payload) {
80
90
  const { chatId, text, attachments, approval } = payload;
81
91
  const target = await sendTarget(ctx, chatId);
@@ -22,6 +22,7 @@ import type {
22
22
  CallbackContext,
23
23
  ChannelTransport,
24
24
  EditTarget,
25
+ ReactionTarget,
25
26
  } from "./channel-transport.js";
26
27
  import { discordTransport } from "./discord/transport.js";
27
28
  import { slackTransport } from "./slack/transport.js";
@@ -51,6 +52,47 @@ export function getTransportForCallback(
51
52
  return channel ? TRANSPORTS[channel] : undefined;
52
53
  }
53
54
 
55
+ /**
56
+ * Resolve a channel's transport by its id, or `undefined` for a channel with
57
+ * no direct transport. A plugin channel resolves to `undefined` until plugin
58
+ * outbound exists, so capability probes over this read as "not yet" rather
59
+ * than throwing on an unknown id.
60
+ */
61
+ export function getTransportForChannel(
62
+ channel: string | undefined,
63
+ ): ChannelTransport | undefined {
64
+ return channel && channel in TRANSPORTS
65
+ ? TRANSPORTS[channel as DirectDeliveryChannel]
66
+ : undefined;
67
+ }
68
+
69
+ /**
70
+ * Whether this channel can add or remove the assistant's own emoji
71
+ * reactions. Asks the transport rather than the channel id, so a channel
72
+ * that gains the method starts being offered without a caller being told.
73
+ */
74
+ export function supportsChannelReaction(channel: string | undefined): boolean {
75
+ return getTransportForChannel(channel)?.react !== undefined;
76
+ }
77
+
78
+ /**
79
+ * Add or remove one of the assistant's own emoji reactions on a message.
80
+ *
81
+ * Resolves to nothing when the channel cannot react; the tool surface gates
82
+ * on `supportsChannelReaction`, so reaching this without the capability is a
83
+ * caller that skipped the gate, not a user-visible failure.
84
+ */
85
+ export async function sendChannelReaction(
86
+ channel: string,
87
+ target: ReactionTarget,
88
+ ): Promise<ChannelDeliveryResult> {
89
+ const transport = getTransportForChannel(channel);
90
+ if (!transport?.react) {
91
+ return { ok: true };
92
+ }
93
+ return transport.react(target);
94
+ }
95
+
54
96
  /**
55
97
  * Whether the channel this callback addresses can show how busy the assistant
56
98
  * is.
@@ -17,7 +17,14 @@ mock.module("../../../../oauth/connection-resolver.js", () => ({
17
17
  throw new Error("OAuth fallback was not expected");
18
18
  },
19
19
  }));
20
+ // Spread rather than list: a partial factory silently drops whatever the
21
+ // module under test imports next, and `resolveSlackAuth` reaching
22
+ // `getConnectionByProvider` is what surfaced that here as a real query
23
+ // against a database this suite never built.
24
+ const actualOauthStore = await import("../../../../oauth/oauth-store.js");
20
25
  mock.module("../../../../oauth/oauth-store.js", () => ({
26
+ ...actualOauthStore,
27
+ getConnectionByProvider: () => undefined,
21
28
  isProviderConnected: async () => false,
22
29
  }));
23
30
 
@@ -38,6 +45,7 @@ import {
38
45
  const originalFetch = globalThis.fetch;
39
46
  let userInfoCalls: string[] = [];
40
47
  let channelInfoCalls: string[] = [];
48
+ let botsInfoCalls: string[] = [];
41
49
 
42
50
  function installFetchStub() {
43
51
  globalThis.fetch = (async (
@@ -158,6 +166,29 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
158
166
  };
159
167
  }
160
168
 
169
+ if (parsed.searchParams.get("channel") === "C_FLAKY_BOT_HISTORY") {
170
+ return {
171
+ ok: true,
172
+ has_more: false,
173
+ messages: [
174
+ {
175
+ type: "message",
176
+ subtype: "bot_message",
177
+ ts: "1700000004.000100",
178
+ bot_id: "B_FLAKY",
179
+ text: "first alert",
180
+ },
181
+ {
182
+ type: "message",
183
+ subtype: "bot_message",
184
+ ts: "1700000004.000200",
185
+ bot_id: "B_FLAKY",
186
+ text: "second alert",
187
+ },
188
+ ],
189
+ };
190
+ }
191
+
161
192
  if (parsed.searchParams.get("channel") === "C_BOT_HISTORY") {
162
193
  return {
163
194
  ok: true,
@@ -168,6 +199,39 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
168
199
  subtype: "bot_message",
169
200
  ts: "1700000003.000400",
170
201
  bot_id: "B_ASSISTANT",
202
+ text: "",
203
+ attachments: [
204
+ {
205
+ fallback: "notify-web failure",
206
+ pretext: "notify-web failure by <@ULEO>",
207
+ title: "CI Main Web",
208
+ title_link: "https://example.com/actions/runs/1",
209
+ },
210
+ ],
211
+ },
212
+ {
213
+ type: "message",
214
+ subtype: "bot_message",
215
+ ts: "1700000003.000500",
216
+ bot_id: "B_WEBHOOK",
217
+ username: "Deploy Webhook",
218
+ text: "",
219
+ blocks: [
220
+ {
221
+ type: "section",
222
+ text: { type: "mrkdwn", text: "Deploy finished" },
223
+ },
224
+ {
225
+ type: "context",
226
+ elements: [{ type: "mrkdwn", text: "took 42s" }],
227
+ },
228
+ ],
229
+ },
230
+ {
231
+ type: "message",
232
+ subtype: "bot_message",
233
+ ts: "1700000003.000600",
234
+ bot_id: "B_ASSISTANT",
171
235
  text: "Bot-authored history",
172
236
  },
173
237
  ],
@@ -236,6 +300,20 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
236
300
  return fakeUserInfoResponse(userId);
237
301
  }
238
302
 
303
+ if (method === "bots.info") {
304
+ const botId = parsed.searchParams.get("bot") ?? "";
305
+ botsInfoCalls.push(botId);
306
+ if (botId === "B_ASSISTANT") {
307
+ return { ok: true, bot: { id: "B_ASSISTANT", name: "CI Notifier" } };
308
+ }
309
+ // An unmapped transient failure: thrown immediately (no in-transport
310
+ // retry, unlike rate limits) and evicted from the cross-batch cache.
311
+ if (botId === "B_FLAKY") {
312
+ return { ok: false, error: "fatal_error" };
313
+ }
314
+ return { ok: false, error: "bot_not_found" };
315
+ }
316
+
239
317
  return { ok: true };
240
318
  }
241
319
 
@@ -364,6 +442,7 @@ describe("Slack adapter mention rendering", () => {
364
442
  __resetSlackMentionCachesForTests();
365
443
  userInfoCalls = [];
366
444
  channelInfoCalls = [];
445
+ botsInfoCalls = [];
367
446
  getSecureKeyAsyncMock.mockReset();
368
447
  getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
369
448
  if (key === credentialKey("slack_channel", "bot_token")) {
@@ -392,15 +471,62 @@ describe("Slack adapter mention rendering", () => {
392
471
  expect(messages[0].reactions).toEqual([{ name: "eyes", count: 1 }]);
393
472
  });
394
473
 
395
- test("getHistory preserves bot ids for bot-authored Slack messages", async () => {
474
+ test("getHistory resolves bot sender names and attachment/block content for bot-authored Slack messages", async () => {
396
475
  const messages = await slackProvider.getHistory(undefined, "C_BOT_HISTORY");
397
476
 
398
- expect(messages).toHaveLength(1);
399
- expect(messages[0].sender).toEqual({ id: "B_ASSISTANT", name: "unknown" });
477
+ expect(messages).toHaveLength(3);
478
+
479
+ // bot_id-only row: name via bots.info, content from the attachment
480
+ // (mention tokens inside it render like ordinary message text).
481
+ expect(messages[0].sender).toEqual({
482
+ id: "B_ASSISTANT",
483
+ name: "CI Notifier",
484
+ });
485
+ expect(messages[0].text).toBe(
486
+ "notify-web failure by @Leo\nCI Main Web (https://example.com/actions/runs/1)",
487
+ );
400
488
  expect(messages[0].metadata).toEqual({
401
489
  isBot: true,
402
490
  slackBotId: "B_ASSISTANT",
403
491
  });
492
+
493
+ // username-bearing row: named without a bots.info round trip, content
494
+ // from Block Kit blocks.
495
+ expect(messages[1].sender).toEqual({
496
+ id: "B_WEBHOOK",
497
+ name: "Deploy Webhook",
498
+ });
499
+ expect(messages[1].text).toBe("Deploy finished\ntook 42s");
500
+ expect(botsInfoCalls).not.toContain("B_WEBHOOK");
501
+
502
+ // Plain text row from the same bot: bots.info was resolved once, cached.
503
+ expect(messages[2].sender).toEqual({
504
+ id: "B_ASSISTANT",
505
+ name: "CI Notifier",
506
+ });
507
+ expect(messages[2].text).toBe("Bot-authored history");
508
+ expect(
509
+ botsInfoCalls.filter((botId) => botId === "B_ASSISTANT"),
510
+ ).toHaveLength(1);
511
+ });
512
+
513
+ test("a transient bots.info failure is attempted once per batch and falls back to the bot id", async () => {
514
+ const messages = await slackProvider.getHistory(
515
+ undefined,
516
+ "C_FLAKY_BOT_HISTORY",
517
+ );
518
+
519
+ expect(messages).toHaveLength(2);
520
+ expect(messages.map((message) => message.sender)).toEqual([
521
+ { id: "B_FLAKY", name: "B_FLAKY" },
522
+ { id: "B_FLAKY", name: "B_FLAKY" },
523
+ ]);
524
+ // The transient failure is evicted from the cross-batch cache (so a later
525
+ // page retries), but within one page the batch memo pins the first
526
+ // attempt: two rows from the same bot cost one bots.info call.
527
+ expect(botsInfoCalls.filter((botId) => botId === "B_FLAKY")).toHaveLength(
528
+ 1,
529
+ );
404
530
  });
405
531
 
406
532
  test("getHistory caches Slack user info and maps timezone metadata", async () => {
@@ -55,6 +55,41 @@ describe("resolveSlackAuth", () => {
55
55
  test("user intent falls back to the bot token without a user token", async () => {
56
56
  secureKeys.set(BOT_KEY, "xoxb-bot");
57
57
  expect(await resolveSlackAuth("user")).toBe("xoxb-bot");
58
+ // No connection row, so the OAuth rung is skipped rather than probed.
59
+ expect(resolveOAuthCalls).toEqual([]);
60
+ });
61
+
62
+ test("user intent takes the OAuth connection over the bot token", async () => {
63
+ // Both are the installer's own token. Degrading to the bot here would
64
+ // drop `search.messages`, which a bot token cannot call, while a usable
65
+ // user token sat in the connection.
66
+ secureKeys.set(BOT_KEY, "xoxb-bot");
67
+ connectionByProvider["slack"] = { id: "conn-1" };
68
+ oauthConnection = { accessToken: "xoxp-oauth" };
69
+
70
+ expect((await resolveSlackAuth("user")) as unknown).toBe(oauthConnection);
71
+ });
72
+
73
+ test("the pasted user token still wins over an OAuth connection", async () => {
74
+ secureKeys.set(BOT_KEY, "xoxb-bot");
75
+ secureKeys.set(USER_KEY, "xoxp-user");
76
+ connectionByProvider["slack"] = { id: "conn-1" };
77
+ oauthConnection = { accessToken: "xoxp-oauth" };
78
+
79
+ expect(await resolveSlackAuth("user")).toBe("xoxp-user");
80
+ // Resolved from the credential store, so the connection is never touched.
81
+ expect(resolveOAuthCalls).toEqual([]);
82
+ });
83
+
84
+ test("an unresolvable OAuth connection degrades to the bot token", async () => {
85
+ // A revoked or unrefreshable connection must not take messaging down: the
86
+ // bot token still serves every read it can.
87
+ secureKeys.set(BOT_KEY, "xoxb-bot");
88
+ connectionByProvider["slack"] = { id: "conn-1" };
89
+ oauthConnection = null; // the mock throws for this
90
+
91
+ expect(await resolveSlackAuth("user")).toBe("xoxb-bot");
92
+ expect(resolveOAuthCalls).toHaveLength(1);
58
93
  });
59
94
 
60
95
  test("resolves the refreshing OAuth connection for legacy installs", async () => {