@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
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { slackMessageRawText } from "../message-content.js";
4
+
5
+ describe("slackMessageRawText", () => {
6
+ test("returns top-level text untouched when present", () => {
7
+ expect(
8
+ slackMessageRawText({
9
+ text: "hello <@U123>",
10
+ attachments: [{ fallback: "ignored" }],
11
+ }),
12
+ ).toBe("hello <@U123>");
13
+ });
14
+
15
+ test("whitespace-only text falls through to attachments", () => {
16
+ expect(
17
+ slackMessageRawText({
18
+ text: " \n ",
19
+ attachments: [{ fallback: "the fallback" }],
20
+ }),
21
+ ).toBe("the fallback");
22
+ });
23
+
24
+ test("prefers structured attachment fields over fallback", () => {
25
+ expect(
26
+ slackMessageRawText({
27
+ text: "",
28
+ attachments: [
29
+ {
30
+ fallback: "plain summary",
31
+ pretext: "build failed by <@U08E>",
32
+ author_name: "CI",
33
+ title: "CI Main Web",
34
+ title_link: "https://example.com/runs/1",
35
+ text: "3 tests failing",
36
+ fields: [
37
+ { title: "Branch", value: "main" },
38
+ { title: "", value: "value-only" },
39
+ ],
40
+ footer: "workflow #12",
41
+ },
42
+ ],
43
+ }),
44
+ ).toBe(
45
+ [
46
+ "build failed by <@U08E>",
47
+ "CI",
48
+ "CI Main Web (https://example.com/runs/1)",
49
+ "3 tests failing",
50
+ "Branch: main",
51
+ "value-only",
52
+ "workflow #12",
53
+ ].join("\n"),
54
+ );
55
+ });
56
+
57
+ test("uses fallback when the attachment has no structured text", () => {
58
+ expect(
59
+ slackMessageRawText({
60
+ text: "",
61
+ attachments: [{ fallback: "only the fallback" }],
62
+ }),
63
+ ).toBe("only the fallback");
64
+ });
65
+
66
+ test("joins multiple attachments with newlines and drops empty ones", () => {
67
+ expect(
68
+ slackMessageRawText({
69
+ text: "",
70
+ attachments: [{ text: "first" }, {}, { fallback: "second" }],
71
+ }),
72
+ ).toBe("first\nsecond");
73
+ });
74
+
75
+ test("extracts section, header, and context block text ahead of attachments", () => {
76
+ expect(
77
+ slackMessageRawText({
78
+ text: "",
79
+ blocks: [
80
+ {
81
+ type: "header",
82
+ text: { type: "plain_text", text: "Release v1.2" },
83
+ },
84
+ {
85
+ type: "section",
86
+ text: { type: "mrkdwn", text: "All green" },
87
+ fields: [
88
+ { type: "mrkdwn", text: "Env: prod" },
89
+ { type: "mrkdwn", text: "" },
90
+ ],
91
+ },
92
+ { type: "divider" },
93
+ {
94
+ type: "context",
95
+ elements: [
96
+ { type: "mrkdwn", text: "deployed by <@U42>" },
97
+ { type: "plain_text", text: "just now" },
98
+ ],
99
+ },
100
+ ],
101
+ attachments: [{ fallback: "attachment tail" }],
102
+ }),
103
+ ).toBe(
104
+ [
105
+ "Release v1.2",
106
+ "All green",
107
+ "Env: prod",
108
+ "deployed by <@U42> just now",
109
+ "attachment tail",
110
+ ].join("\n"),
111
+ );
112
+ });
113
+
114
+ test("extracts markdown block text", () => {
115
+ expect(
116
+ slackMessageRawText({
117
+ text: "",
118
+ blocks: [
119
+ { type: "markdown", text: "**Deploy failed** on `main`" },
120
+ { type: "divider" },
121
+ ],
122
+ }),
123
+ ).toBe("**Deploy failed** on `main`");
124
+ });
125
+
126
+ test("extracts blocks nested inside an attachment", () => {
127
+ expect(
128
+ slackMessageRawText({
129
+ text: "",
130
+ attachments: [
131
+ {
132
+ fallback: "plain summary",
133
+ blocks: [
134
+ { type: "section", text: { type: "mrkdwn", text: "Run #7 red" } },
135
+ { type: "markdown", text: "3 tests failing" },
136
+ ],
137
+ },
138
+ ],
139
+ }),
140
+ ).toBe("Run #7 red\n3 tests failing");
141
+ });
142
+
143
+ test("returns empty string when nothing carries text", () => {
144
+ expect(
145
+ slackMessageRawText({
146
+ text: "",
147
+ blocks: [{ type: "divider" }],
148
+ attachments: [{}],
149
+ }),
150
+ ).toBe("");
151
+ });
152
+ });
@@ -38,6 +38,7 @@ import {
38
38
  isPrivateConversation,
39
39
  slackUserDisplayName,
40
40
  } from "./conversation-utils.js";
41
+ import { slackMessageRawText } from "./message-content.js";
41
42
  import type {
42
43
  SlackConversation,
43
44
  SlackMessage,
@@ -79,6 +80,15 @@ const userInfoCache = new Map<string, Promise<SlackUserInfoLookupResult>>();
79
80
  */
80
81
  const channelNameCache = new Map<string, Promise<string | undefined>>();
81
82
 
83
+ /**
84
+ * Cache resolved bot display names (`bots.info`) for bot-authored rows that
85
+ * carry only a `bot_id`, same lifetime and keying discipline as
86
+ * {@link userInfoCache}. Holds `undefined` for bots this auth provably
87
+ * cannot resolve so a wall of webhook history doesn't re-fire a doomed
88
+ * lookup per message batch.
89
+ */
90
+ const botNameCache = new Map<string, Promise<string | undefined>>();
91
+
82
92
  /**
83
93
  * Cached auth resolved during resolveConnection(), split by direction.
84
94
  *
@@ -331,42 +341,113 @@ function slackUserInfoCacheKey(
331
341
  }
332
342
 
333
343
  /**
334
- * Resolve a channel's display name for inline mention rendering, cached per
335
- * auth scope. Returns undefined when the channel has no name (DMs) or this
336
- * auth cannot see it; transient failures are not cached so a later batch
337
- * retries.
344
+ * Shared resolve-once discipline for the Slack directory name caches
345
+ * (channel and bot names): concurrent callers share the in-flight promise,
346
+ * successes and provably-permanent failures stay cached, and transient
347
+ * failures are evicted so a later batch retries.
338
348
  */
339
- async function resolveChannelName(
340
- auth: OAuthConnection | string,
341
- channelId: string,
349
+ async function resolveCachedSlackName(
350
+ cache: Map<string, Promise<string | undefined>>,
351
+ cacheKey: string,
352
+ lookup: () => Promise<string | undefined>,
353
+ isPermanentFailure: (err: unknown) => boolean,
342
354
  ): Promise<string | undefined> {
343
- if (!channelId) {
344
- return undefined;
345
- }
346
- const cacheKey = `${slackAuthCacheScope(auth)}:channel:${channelId}`;
347
- const cached = channelNameCache.get(cacheKey);
355
+ const cached = cache.get(cacheKey);
348
356
  if (cached) {
349
357
  return cached;
350
358
  }
351
359
 
352
- const resolved = slack.conversationInfo(auth, channelId).then(
353
- (resp) => trimNonEmpty(resp.channel.name),
360
+ const resolved = lookup().then(
361
+ (name) => name,
354
362
  (err: unknown) => {
355
363
  // Cache the definitive "this auth cannot resolve it" answers; drop
356
364
  // everything else so transient failures retry on the next batch.
357
- const permanent =
358
- err instanceof SlackApiError &&
359
- (err.category === "channel_not_found" || err.category === "permission");
360
- if (!permanent) {
361
- channelNameCache.delete(cacheKey);
365
+ if (!isPermanentFailure(err)) {
366
+ cache.delete(cacheKey);
362
367
  }
363
368
  return undefined;
364
369
  },
365
370
  );
366
- channelNameCache.set(cacheKey, resolved);
371
+ cache.set(cacheKey, resolved);
367
372
  return resolved;
368
373
  }
369
374
 
375
+ /**
376
+ * Resolve a channel's display name for inline mention rendering, cached per
377
+ * auth scope. Returns undefined when the channel has no name (DMs) or this
378
+ * auth cannot see it.
379
+ */
380
+ async function resolveChannelName(
381
+ auth: OAuthConnection | string,
382
+ channelId: string,
383
+ ): Promise<string | undefined> {
384
+ if (!channelId) {
385
+ return undefined;
386
+ }
387
+ return resolveCachedSlackName(
388
+ channelNameCache,
389
+ `${slackAuthCacheScope(auth)}:channel:${channelId}`,
390
+ async () =>
391
+ trimNonEmpty(
392
+ (await slack.conversationInfo(auth, channelId)).channel.name,
393
+ ),
394
+ (err) =>
395
+ err instanceof SlackApiError &&
396
+ (err.category === "channel_not_found" || err.category === "permission"),
397
+ );
398
+ }
399
+
400
+ /**
401
+ * Resolve a bot's display name via `bots.info`, cached per auth scope.
402
+ * Returns undefined when this auth cannot resolve it.
403
+ */
404
+ async function resolveBotName(
405
+ auth: OAuthConnection | string,
406
+ botId: string,
407
+ ): Promise<string | undefined> {
408
+ return resolveCachedSlackName(
409
+ botNameCache,
410
+ `${slackAuthCacheScope(auth)}:bot:${botId}`,
411
+ async () => trimNonEmpty((await slack.botsInfo(auth, botId)).bot.name),
412
+ (err) =>
413
+ err instanceof SlackApiError &&
414
+ (err.slackError === "bot_not_found" || err.category === "permission"),
415
+ );
416
+ }
417
+
418
+ /**
419
+ * Sender info for a bot-authored row (no `user` to look up). The message's
420
+ * own `username` (bot_message subtype) or `bot_profile.name` answers without
421
+ * I/O; `bots.info` covers rows carrying only a `bot_id`, with the id itself
422
+ * as the last resort so the sender is at least attributable.
423
+ *
424
+ * `batchBotNames` pins the first lookup attempt per bot id for the caller's
425
+ * batch: the module cache evicts transient failures (so a later batch
426
+ * retries), and without the pin a history page full of one webhook's rows
427
+ * would re-fire a doomed `bots.info` cycle per row during a Slack outage.
428
+ */
429
+ async function resolveBotSenderInfo(
430
+ auth: OAuthConnection | string,
431
+ msg: SlackMessage,
432
+ batchBotNames: Map<string, Promise<string | undefined>>,
433
+ ): Promise<NormalizedSlackUserInfo> {
434
+ const fromMessage =
435
+ trimNonEmpty(msg.username) ?? trimNonEmpty(msg.bot_profile?.name);
436
+ if (fromMessage) {
437
+ return { displayName: fromMessage };
438
+ }
439
+ const botId = msg.bot_id?.trim();
440
+ if (!botId) {
441
+ return { displayName: "unknown" };
442
+ }
443
+ let pending = batchBotNames.get(botId);
444
+ if (pending === undefined) {
445
+ pending = resolveBotName(auth, botId);
446
+ batchBotNames.set(botId, pending);
447
+ }
448
+ return { displayName: (await pending) ?? botId };
449
+ }
450
+
370
451
  function normalizeSlackUserInfo(
371
452
  user: SlackUser,
372
453
  contactDisplayName: string | undefined,
@@ -398,6 +479,7 @@ function trimNonEmpty(value: unknown): string | undefined {
398
479
  export function __resetSlackMentionCachesForTests(): void {
399
480
  userInfoCache.clear();
400
481
  channelNameCache.clear();
482
+ botNameCache.clear();
401
483
  }
402
484
 
403
485
  function slackUserInfoMetadata(
@@ -529,19 +611,22 @@ async function mapSlackMessages(
529
611
  channelId: string,
530
612
  slackMessages: SlackMessage[],
531
613
  ): Promise<Message[]> {
532
- const labels = await buildMentionLabels(
533
- auth,
534
- slackMessages.map((msg) => msg.text),
535
- );
614
+ // Raw text may come from attachments/blocks (bot and webhook posts), so
615
+ // mention labels are built from the same derived strings that get rendered.
616
+ const rawTexts = slackMessages.map((msg) => slackMessageRawText(msg));
617
+ const labels = await buildMentionLabels(auth, rawTexts);
618
+ const batchBotNames = new Map<string, Promise<string | undefined>>();
536
619
  const messages: Message[] = [];
537
- for (const msg of slackMessages) {
538
- const senderInfo = await resolveUserInfo(auth, msg.user ?? "");
620
+ for (const [i, msg] of slackMessages.entries()) {
621
+ const senderInfo = msg.user
622
+ ? await resolveUserInfo(auth, msg.user)
623
+ : await resolveBotSenderInfo(auth, msg, batchBotNames);
539
624
  messages.push(
540
625
  mapMessage(
541
626
  msg,
542
627
  channelId,
543
628
  senderInfo,
544
- renderSlackTextForModel(msg.text, labels),
629
+ renderSlackTextForModel(rawTexts[i], labels),
545
630
  ),
546
631
  );
547
632
  }
@@ -28,6 +28,16 @@ mock.module("../../../security/secure-keys.js", () => ({
28
28
  }),
29
29
  }));
30
30
 
31
+ // With `userTokenStored` false, `resolveSlackAuth` looks for a `slack` OAuth
32
+ // connection before falling back to the bot token. This suite builds no
33
+ // database, so the lookup has to be answered here; spread for the same reason
34
+ // the secure-keys mock above does.
35
+ const realOauthStore = await import("../../../oauth/oauth-store.js");
36
+ mock.module("../../../oauth/oauth-store.js", () => ({
37
+ ...realOauthStore,
38
+ getConnectionByProvider: () => undefined,
39
+ }));
40
+
31
41
  const { appendSlackStream, getSlackConversationInfo, startSlackStream } =
32
42
  await import("./api.js");
33
43
 
@@ -21,9 +21,11 @@
21
21
  * any caller read the installer's channels or post as them. Also used for
22
22
  * content the assistant posts as itself.
23
23
  *
24
- * - "user" — act as the assistant's owner, using the optional user token for
25
- * its wider reach (channels the owner is in but the bot isn't; and
26
- * `search.messages`, which only a user token can call). Scoped to the
24
+ * - "user": act as the assistant's owner, using whichever of the installer's
25
+ * own tokens is present for its wider reach (channels the owner is in but
26
+ * the bot isn't; and `search.messages`, which only a user token can call):
27
+ * the channel's pasted user token, else a `slack` OAuth connection, else
28
+ * the bot token. Scoped to the
27
29
  * in-conversation messaging adapter, where the assistant is acting for its
28
30
  * own owner within a trust-classified conversation — NOT the edge-reachable
29
31
  * control-plane routes above. Falls back to the bot token when no user
@@ -57,12 +59,36 @@ export async function resolveSlackAuth(
57
59
  );
58
60
  if (botToken) {
59
61
  if (identity === "user") {
60
- // Prefer the optional user token; fall back to the bot token when the
61
- // install never captured one.
62
+ // Three rungs, widest reach first. The pasted channel user token and the
63
+ // `slack` OAuth connection are the same thing obtained two ways: the
64
+ // installer's own token, carrying the channel and search scopes a bot
65
+ // token cannot. Falling straight to the bot from a missing paste would
66
+ // drop `search.messages`, which only a user token can call, while a
67
+ // usable one sat in the OAuth connection.
62
68
  const userToken = await getSecureKeyAsync(
63
69
  credentialKey("slack_channel", "user_token"),
64
70
  );
65
- return userToken ?? botToken;
71
+ if (userToken) {
72
+ return userToken;
73
+ }
74
+ // The connection holds the installer's user token: this provider always
75
+ // requests `user_scope`, and `exchangeCodeForTokens` stores the nested
76
+ // `authed_user` token whenever Slack returns one. An install that
77
+ // granted no user scopes leaves a bot token there instead, which is the
78
+ // same thing the next line falls back to, so the wrong guess costs
79
+ // nothing.
80
+ if (getConnectionByProvider("slack")) {
81
+ try {
82
+ return await resolveOAuthConnection("slack", {
83
+ account: opts.account,
84
+ });
85
+ } catch {
86
+ // Unlike the legacy branch below, this one has somewhere to go: a
87
+ // revoked or unrefreshable connection degrades to the bot token
88
+ // rather than failing a resolve the bot can still answer.
89
+ }
90
+ }
91
+ return botToken;
66
92
  }
67
93
  return botToken;
68
94
  }
@@ -22,7 +22,6 @@ import type {
22
22
  SlackConversationsListResponse,
23
23
  SlackConversationsOpenResponse,
24
24
  SlackPostMessageResponse,
25
- SlackReactionsAddResponse,
26
25
  SlackSearchMessagesResponse,
27
26
  SlackUserInfoResponse,
28
27
  SlackUsersListResponse,
@@ -222,20 +221,6 @@ export async function searchMessages(
222
221
  );
223
222
  }
224
223
 
225
- export async function addReaction(
226
- connectionOrToken: OAuthConnection | string,
227
- channel: string,
228
- timestamp: string,
229
- name: string,
230
- ): Promise<SlackReactionsAddResponse> {
231
- return request<SlackReactionsAddResponse>(
232
- connectionOrToken,
233
- "reactions.add",
234
- undefined,
235
- { channel, timestamp, name },
236
- );
237
- }
238
-
239
224
  export async function listUsers(
240
225
  connectionOrToken: OAuthConnection | string,
241
226
  limit = 200,
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Fallback text extraction for Slack messages whose content lives outside
3
+ * the top-level `text` field.
4
+ *
5
+ * Bot and incoming-webhook posts (CI alerts, app notifications) routinely
6
+ * ship an empty `text` with the visible content in legacy `attachments` or
7
+ * Block Kit `blocks`; Slack's own clients render those. A mapping that reads
8
+ * only `text` turns such a message into an empty row, which downstream
9
+ * surfaces (transcripts, channel references) present as missing content.
10
+ *
11
+ * Output is raw Slack mrkdwn: mention/link tokens inside attachments and
12
+ * blocks are preserved so callers can route the result through the same
13
+ * `renderSlackTextForModel` pass as ordinary message text.
14
+ */
15
+ import type {
16
+ AnyBlock,
17
+ ContextBlock,
18
+ HeaderBlock,
19
+ MarkdownBlock,
20
+ MessageAttachment,
21
+ SectionBlock,
22
+ } from "@slack/types";
23
+
24
+ import type { SlackMessage } from "./types.js";
25
+
26
+ /**
27
+ * The model-facing raw mrkdwn for a Slack message: `text` when present,
28
+ * otherwise content derived from `blocks` and `attachments` (in that order,
29
+ * matching how Slack renders a message body above its attachments).
30
+ */
31
+ export function slackMessageRawText(
32
+ msg: Pick<SlackMessage, "text" | "attachments" | "blocks">,
33
+ ): string {
34
+ if (msg.text?.trim()) {
35
+ return msg.text;
36
+ }
37
+ const parts = [
38
+ ...slackBlocksText(msg.blocks),
39
+ ...(msg.attachments ?? []).map(slackAttachmentText),
40
+ ].filter((part) => part.length > 0);
41
+ return parts.join("\n");
42
+ }
43
+
44
+ function slackAttachmentText(attachment: MessageAttachment): string {
45
+ // An attachment carrying Block Kit blocks renders them as its body, so
46
+ // they lead; the legacy fields still contribute when both are present.
47
+ const blockParts = slackBlocksText(attachment.blocks);
48
+ const title = trimmed(attachment.title);
49
+ const titleLink = trimmed(attachment.title_link);
50
+ const fieldLines = (attachment.fields ?? []).map((field) => {
51
+ const fieldTitle = trimmed(field.title);
52
+ const fieldValue = trimmed(field.value);
53
+ if (fieldTitle && fieldValue) {
54
+ return `${fieldTitle}: ${fieldValue}`;
55
+ }
56
+ return fieldTitle ?? fieldValue ?? "";
57
+ });
58
+ const structured = [
59
+ ...blockParts,
60
+ trimmed(attachment.pretext),
61
+ trimmed(attachment.author_name),
62
+ title && titleLink ? `${title} (${titleLink})` : title,
63
+ trimmed(attachment.text),
64
+ ...fieldLines,
65
+ trimmed(attachment.footer),
66
+ ].filter((part): part is string => !!part);
67
+ if (structured.length > 0) {
68
+ return structured.join("\n");
69
+ }
70
+ return trimmed(attachment.fallback) ?? "";
71
+ }
72
+
73
+ function isSectionBlock(block: AnyBlock): block is SectionBlock {
74
+ return block.type === "section";
75
+ }
76
+
77
+ function isHeaderBlock(block: AnyBlock): block is HeaderBlock {
78
+ return block.type === "header";
79
+ }
80
+
81
+ function isContextBlock(block: AnyBlock): block is ContextBlock {
82
+ return block.type === "context";
83
+ }
84
+
85
+ function isMarkdownBlock(block: AnyBlock): block is MarkdownBlock {
86
+ return block.type === "markdown";
87
+ }
88
+
89
+ /**
90
+ * Text carried by the block types that hold a message body's copy. Other
91
+ * block types contribute nothing: images/dividers/actions are non-textual,
92
+ * and `rich_text` (deeply nested; produced for user-typed messages, which
93
+ * carry the equivalent top-level `text`) is intentionally not walked.
94
+ */
95
+ function slackBlocksText(blocks: readonly AnyBlock[] | undefined): string[] {
96
+ const parts: string[] = [];
97
+ for (const block of blocks ?? []) {
98
+ if (isSectionBlock(block)) {
99
+ const text = trimmed(block.text?.text);
100
+ if (text) {
101
+ parts.push(text);
102
+ }
103
+ for (const field of block.fields ?? []) {
104
+ const fieldText = trimmed(field.text);
105
+ if (fieldText) {
106
+ parts.push(fieldText);
107
+ }
108
+ }
109
+ } else if (isHeaderBlock(block)) {
110
+ const text = trimmed(block.text.text);
111
+ if (text) {
112
+ parts.push(text);
113
+ }
114
+ } else if (isContextBlock(block)) {
115
+ const contextText = block.elements
116
+ .map((element) =>
117
+ "text" in element ? trimmed(element.text) : undefined,
118
+ )
119
+ .filter((text): text is string => !!text)
120
+ .join(" ");
121
+ if (contextText) {
122
+ parts.push(contextText);
123
+ }
124
+ } else if (isMarkdownBlock(block)) {
125
+ const text = trimmed(block.text);
126
+ if (text) {
127
+ parts.push(text);
128
+ }
129
+ }
130
+ }
131
+ return parts;
132
+ }
133
+
134
+ function trimmed(value: string | undefined): string | undefined {
135
+ const result = value?.trim();
136
+ return result ? result : undefined;
137
+ }
@@ -28,8 +28,12 @@ mock.module("./api.js", () => ({
28
28
  // the (unmocked) shared transport, so thrown test errors must be real
29
29
  // instances or every branch under test would silently take the generic path.
30
30
  const { SlackApiError } = await import("./web-api-transport.js");
31
- const { sendSlackAgentSessionStatus, sendSlackReply, updateSlackMessage } =
32
- await import("./send.js");
31
+ const {
32
+ sendSlackAgentSessionStatus,
33
+ sendSlackReaction,
34
+ sendSlackReply,
35
+ updateSlackMessage,
36
+ } = await import("./send.js");
33
37
 
34
38
  describe("sendSlackAgentSessionStatus", () => {
35
39
  const threadTs = "1700000000.000100";
@@ -137,6 +141,43 @@ describe("sendSlackAgentSessionStatus", () => {
137
141
  });
138
142
  });
139
143
 
144
+ describe("sendSlackReaction", () => {
145
+ test("adds with the bare emoji name, colons stripped", async () => {
146
+ const result = await sendSlackReaction("C1", ":tada:", "123.456", "add");
147
+ expect(result).toEqual({ ok: true });
148
+ expect(callSlackApiMock).toHaveBeenCalledWith("reactions.add", {
149
+ channel: "C1",
150
+ name: "tada",
151
+ timestamp: "123.456",
152
+ });
153
+ });
154
+
155
+ test("remove routes to reactions.remove", async () => {
156
+ await sendSlackReaction("C1", "tada", "123.456", "remove");
157
+ expect(callSlackApiMock).toHaveBeenCalledWith("reactions.remove", {
158
+ channel: "C1",
159
+ name: "tada",
160
+ timestamp: "123.456",
161
+ });
162
+ });
163
+
164
+ test("already_reacted reads as success: the end state holds", async () => {
165
+ callSlackApiMock.mockImplementationOnce(async () => {
166
+ throw new SlackApiError("already_reacted");
167
+ });
168
+ const result = await sendSlackReaction("C1", "tada", "123.456", "add");
169
+ expect(result).toEqual({ ok: true });
170
+ });
171
+
172
+ test("any other failure reports ok false without throwing", async () => {
173
+ callSlackApiMock.mockImplementationOnce(async () => {
174
+ throw new SlackApiError("channel_not_found");
175
+ });
176
+ const result = await sendSlackReaction("C1", "tada", "123.456", "add");
177
+ expect(result).toEqual({ ok: false });
178
+ });
179
+ });
180
+
140
181
  describe("updateSlackMessage", () => {
141
182
  const messageTs = "1700000000.000100";
142
183
  const blocks: KnownBlock[] = [