@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
@@ -111,7 +111,7 @@ export async function executeAcpSpawn(
111
111
  // This failure raises the inline Connect card. Record it so the
112
112
  // credential-prompt route only redirects a redundant secure-prompt when a
113
113
  // card actually exists (not for a proactive prompt before any failure).
114
- markAcpConnectCardRaised(context.conversationId);
114
+ markAcpConnectCardRaised(context.conversationId, agent);
115
115
  }
116
116
  return { content: msg, isError: true, ...(errorCode ? { errorCode } : {}) };
117
117
  }
@@ -174,7 +174,7 @@ export async function executeAcpSpawn(
174
174
  err instanceof Error
175
175
  ? err.message
176
176
  : String((err as { message?: unknown }).message ?? err);
177
- markAcpConnectCardRaised(context.conversationId);
177
+ markAcpConnectCardRaised(context.conversationId, agent);
178
178
  return {
179
179
  content: `${message}\n\n${ACP_AUTH_RECOVERY_GUIDANCE}`,
180
180
  isError: true,
@@ -5,6 +5,7 @@ import type { ToolContext } from "../../types.js";
5
5
  import {
6
6
  BROWSER_STATUS_INPUT_FIELD,
7
7
  BROWSER_STATUS_MODE,
8
+ CHROME_EXTENSION_INSTALL_HINT,
8
9
  } from "../browser-status-constants.js";
9
10
  import { CdpError } from "../cdp-client/errors.js";
10
11
 
@@ -214,6 +215,10 @@ describe("executeBrowserStatus", () => {
214
215
  expect(extension.summary).toContain("no Chrome Extension is connected");
215
216
  expect(extension.verified).toBe("preflight");
216
217
  expect(extension.details.transport).toBe("extension-ws");
218
+ expect(extension.userActions).toEqual([
219
+ CHROME_EXTENSION_INSTALL_HINT,
220
+ "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
221
+ ]);
217
222
  });
218
223
 
219
224
  test("probe failure diagnostics include remediation actions", async () => {
@@ -237,6 +242,10 @@ describe("executeBrowserStatus", () => {
237
242
  expect(extension).toBeDefined();
238
243
  expect(extension.available).toBe(false);
239
244
  expect(extension.summary).toContain("probe failed");
245
+ expect(extension.userActions).toEqual([
246
+ CHROME_EXTENSION_INSTALL_HINT,
247
+ "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
248
+ ]);
240
249
  });
241
250
 
242
251
  test("recommendation order follows auto candidate precedence with available extension", async () => {
@@ -39,7 +39,8 @@ import {
39
39
  BROWSER_STATUS_MODES,
40
40
  type BrowserStatusMode,
41
41
  CDP_INSPECT_STATUS_DISCOVERY_CODE,
42
- EXTENSION_STATUS_ERROR_MARKER,
42
+ CHROME_EXTENSION_INSTALL_HINT,
43
+ CHROME_WEB_STORE_INSTALL_URL,
43
44
  } from "./browser-status-constants.js";
44
45
  import {
45
46
  formatAxSnapshot,
@@ -296,6 +297,7 @@ export function parseBrowserMode(
296
297
  const REMEDIATION_HINTS: Record<string, string[]> = {
297
298
  // Extension backend
298
299
  "extension:transport_error": [
300
+ CHROME_EXTENSION_INSTALL_HINT,
299
301
  "Ensure the Vellum browser extension is installed and enabled, or that the macOS desktop client is running for host browser proxy mode.",
300
302
  "For extension mode: check that the extension WebSocket connection is active (extension popup → status).",
301
303
  "For macOS host browser proxy: verify the desktop client is running and has an active SSE connection to the assistant.",
@@ -335,11 +337,11 @@ const REMEDIATION_HINTS: Record<string, string[]> = {
335
337
  "Ensure Chrome on the user's machine is on version 146 or higher (chrome://settings/help).",
336
338
  'Ensure "Allow remote debugging for this browser instance" is toggled on at chrome://inspect/#remote-debugging.',
337
339
  "Verify the desktop client is running and has an active SSE connection to the assistant.",
338
- "Installing the Vellum Chrome extension is the preferred path and avoids the debug-port requirement.",
340
+ `Installing the Vellum Chrome extension is the preferred path and avoids the debug-port requirement: ${CHROME_WEB_STORE_INSTALL_URL}`,
339
341
  ],
340
342
  "host-bridge:transport_error": [
341
343
  "The desktop client could not reach Chrome's remote-debugging endpoint on the user's machine.",
342
- "Ensure Chrome is running with remote debugging enabled, or install the Vellum Chrome extension (preferred).",
344
+ `Ensure Chrome is running with remote debugging enabled, or install the Vellum Chrome extension (preferred): ${CHROME_WEB_STORE_INSTALL_URL}`,
343
345
  "Verify the desktop client is running and connected.",
344
346
  ],
345
347
  // Local/Playwright backend
@@ -2485,10 +2487,10 @@ function modeTradeoffs(mode: StatusCheckMode): string[] {
2485
2487
  return MODE_TRADEOFFS[mode];
2486
2488
  }
2487
2489
 
2488
- function extensionSetupActions(): string[] {
2490
+ function extensionConnectionActions(): string[] {
2489
2491
  return [
2490
- "Install the Vellum Assistant Chrome extension from the Chrome Web Store: https://chromewebstore.google.com/detail/vellum-assistant-browser/hphbdmpffeigpcdjkckleobjmhhokpne",
2491
- "Open the extension and pair with your assistant.",
2492
+ CHROME_EXTENSION_INSTALL_HINT,
2493
+ "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
2492
2494
  ];
2493
2495
  }
2494
2496
 
@@ -2517,49 +2519,14 @@ function extractDiscoveryCodes(error: CdpError): string[] {
2517
2519
  return dedupeStrings(codes);
2518
2520
  }
2519
2521
 
2520
- function containsTokenCaseInsensitive(text: string, token: string): boolean {
2521
- return text.toLowerCase().includes(token.toLowerCase());
2522
- }
2523
-
2524
2522
  function probeFailureActions(mode: StatusCheckMode, error: CdpError): string[] {
2525
2523
  const actions: string[] = [];
2526
- const message = error.message.toLowerCase();
2527
2524
  const discoveryCodes = extractDiscoveryCodes(error).map((c) =>
2528
2525
  c.toLowerCase(),
2529
2526
  );
2530
2527
 
2531
2528
  if (mode === BROWSER_STATUS_MODE.EXTENSION) {
2532
- actions.push(...extensionSetupActions());
2533
- if (
2534
- containsTokenCaseInsensitive(
2535
- message,
2536
- EXTENSION_STATUS_ERROR_MARKER.UNAUTHORIZED_ORIGIN,
2537
- )
2538
- ) {
2539
- actions.push(
2540
- "Ensure this extension ID is present in chrome-extension-allowlist.local.json in $GATEWAY_SECURITY_DIR and restart the assistant.",
2541
- );
2542
- }
2543
- if (
2544
- containsTokenCaseInsensitive(
2545
- message,
2546
- EXTENSION_STATUS_ERROR_MARKER.NATIVE_MESSAGING_HOST,
2547
- )
2548
- ) {
2549
- actions.push(
2550
- "Reinstall the native messaging host manifest and confirm it allows this extension ID.",
2551
- );
2552
- }
2553
- if (
2554
- containsTokenCaseInsensitive(
2555
- message,
2556
- EXTENSION_STATUS_ERROR_MARKER.HTTP_401,
2557
- )
2558
- ) {
2559
- actions.push(
2560
- "Re-pair the extension so it refreshes its local relay credential.",
2561
- );
2562
- }
2529
+ actions.push(...extensionConnectionActions());
2563
2530
  }
2564
2531
 
2565
2532
  if (mode === BROWSER_STATUS_MODE.CDP_INSPECT) {
@@ -2651,7 +2618,7 @@ async function checkExtensionModeStatus(
2651
2618
  autoCandidate,
2652
2619
  summary:
2653
2620
  "Extension mode is unavailable: no Chrome Extension is connected.",
2654
- userActions: extensionSetupActions(),
2621
+ userActions: extensionConnectionActions(),
2655
2622
  tradeoffs: modeTradeoffs(BROWSER_STATUS_MODE.EXTENSION),
2656
2623
  details: { transport: "extension-ws" },
2657
2624
  };
@@ -268,6 +268,7 @@ class BrowserManager {
268
268
  {
269
269
  stdout: "pipe",
270
270
  stderr: "pipe",
271
+ windowsHide: true,
271
272
  },
272
273
  );
273
274
  const timeoutMs = 300_000;
@@ -20,11 +20,12 @@ export const BROWSER_STATUS_INPUT_FIELD = {
20
20
  CHECK_LOCAL_LAUNCH: "check_local_launch",
21
21
  } as const;
22
22
 
23
- export const EXTENSION_STATUS_ERROR_MARKER = {
24
- UNAUTHORIZED_ORIGIN: "unauthorized_origin",
25
- NATIVE_MESSAGING_HOST: "native messaging host",
26
- HTTP_401: "401",
27
- } as const;
23
+ /** Chrome Web Store listing for the Vellum Assistant browser extension. */
24
+ export const CHROME_WEB_STORE_INSTALL_URL =
25
+ "https://chromewebstore.google.com/detail/vellum-assistant-browser/hphbdmpffeigpcdjkckleobjmhhokpne";
26
+
27
+ /** User-facing install step for status userActions and command-failure hints. */
28
+ export const CHROME_EXTENSION_INSTALL_HINT = `Install the Vellum Assistant Chrome extension from the Chrome Web Store: ${CHROME_WEB_STORE_INSTALL_URL}`;
28
29
 
29
30
  export const CDP_INSPECT_STATUS_DISCOVERY_CODE = {
30
31
  NO_TARGETS: DEVTOOLS_DISCOVERY_CODE.NO_TARGETS,
@@ -104,6 +104,7 @@ export async function importPlaywright(): Promise<typeof import("playwright")> {
104
104
  cwd: externalDir,
105
105
  stdout: "pipe",
106
106
  stderr: "pipe",
107
+ windowsHide: true,
107
108
  });
108
109
  const exitCode = await proc.exited;
109
110
  if (exitCode !== 0) {
@@ -168,6 +169,7 @@ export async function ensureChromiumHeadlessShell(
168
169
  stdout: "pipe",
169
170
  stderr: "pipe",
170
171
  cwd: externalPwExists ? externalDir : undefined,
172
+ windowsHide: true,
171
173
  },
172
174
  );
173
175
  const timeoutMs = 120_000;
@@ -0,0 +1,193 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { ReactionTarget } from "../../messaging/providers/channel-transport.js";
4
+ import type { ToolContext } from "../types.js";
5
+
6
+ let reactCalls: Array<{ channel: string; target: ReactionTarget }> = [];
7
+ let reactResultOk = true;
8
+ let channelSupported = true;
9
+
10
+ mock.module("../../messaging/providers/index.js", () => ({
11
+ supportsChannelReaction: () => channelSupported,
12
+ sendChannelReaction: async (channel: string, target: ReactionTarget) => {
13
+ reactCalls.push({ channel, target });
14
+ return { ok: reactResultOk };
15
+ },
16
+ }));
17
+
18
+ import type { QueuedReactionRecord } from "../../daemon/reaction-record.js";
19
+
20
+ let queued: Array<{ conversationId: string; record: QueuedReactionRecord }> =
21
+ [];
22
+ let conversationResident = true;
23
+ mock.module("../../daemon/conversation-registry.js", () => ({
24
+ findConversationOrSubagent: (id: string) =>
25
+ conversationResident
26
+ ? {
27
+ queueReactionRecord: (record: QueuedReactionRecord) =>
28
+ queued.push({ conversationId: id, record }),
29
+ }
30
+ : undefined,
31
+ }));
32
+
33
+ let directPersists: Array<{
34
+ conversationId: string;
35
+ records: readonly QueuedReactionRecord[];
36
+ }> = [];
37
+ mock.module("../../daemon/reaction-record.js", () => ({
38
+ persistReactionRecords: async (
39
+ conversationId: string,
40
+ records: readonly QueuedReactionRecord[],
41
+ ) => {
42
+ directPersists.push({ conversationId, records });
43
+ },
44
+ }));
45
+
46
+ const { reactToMessageTool } = await import("./react-to-message.js");
47
+
48
+ function channelContext(overrides: Partial<ToolContext> = {}): ToolContext {
49
+ return {
50
+ workingDir: "/tmp",
51
+ conversationId: "conv-1",
52
+ trustClass: "guardian",
53
+ executionChannel: "slack",
54
+ requesterChatId: "C123",
55
+ sourceMessageId: "1700000000.111111",
56
+ ...overrides,
57
+ } as ToolContext;
58
+ }
59
+
60
+ describe("react_to_message", () => {
61
+ beforeEach(() => {
62
+ reactCalls = [];
63
+ reactResultOk = true;
64
+ channelSupported = true;
65
+ queued = [];
66
+ directPersists = [];
67
+ conversationResident = true;
68
+ });
69
+
70
+ test("reacts to the triggering message by default", async () => {
71
+ const result = await reactToMessageTool.execute(
72
+ { emoji: "thumbsup" },
73
+ channelContext(),
74
+ );
75
+ expect(result.isError).toBe(false);
76
+ expect(reactCalls).toHaveLength(1);
77
+ expect(reactCalls[0]).toEqual({
78
+ channel: "slack",
79
+ target: {
80
+ chatId: "C123",
81
+ messageId: "1700000000.111111",
82
+ emoji: "thumbsup",
83
+ action: "add",
84
+ },
85
+ });
86
+ });
87
+
88
+ test("an explicit messageId overrides the default target", async () => {
89
+ await reactToMessageTool.execute(
90
+ { emoji: "tada", messageId: "1700000000.222222" },
91
+ channelContext(),
92
+ );
93
+ expect(reactCalls[0]?.target.messageId).toBe("1700000000.222222");
94
+ });
95
+
96
+ test("the turn's thread coordinate rides only the default target", async () => {
97
+ await reactToMessageTool.execute(
98
+ { emoji: "tada" },
99
+ channelContext({ sourceThreadId: "THREAD-1" }),
100
+ );
101
+ expect(reactCalls[0]?.target.threadId).toBe("THREAD-1");
102
+
103
+ await reactToMessageTool.execute(
104
+ { emoji: "tada", messageId: "other-msg" },
105
+ channelContext({ sourceThreadId: "THREAD-1" }),
106
+ );
107
+ expect(reactCalls[1]?.target.threadId).toBeUndefined();
108
+ });
109
+
110
+ test("remove action passes through and reports removal", async () => {
111
+ const result = await reactToMessageTool.execute(
112
+ { emoji: "tada", action: "remove" },
113
+ channelContext(),
114
+ );
115
+ expect(reactCalls[0]?.target.action).toBe("remove");
116
+ expect(result.content).toContain("Removed");
117
+ });
118
+
119
+ test("errors without channel message context", async () => {
120
+ const result = await reactToMessageTool.execute(
121
+ { emoji: "thumbsup" },
122
+ channelContext({ executionChannel: undefined }),
123
+ );
124
+ expect(result.isError).toBe(true);
125
+ expect(reactCalls).toHaveLength(0);
126
+ });
127
+
128
+ test("errors on a channel without the capability", async () => {
129
+ channelSupported = false;
130
+ const result = await reactToMessageTool.execute(
131
+ { emoji: "thumbsup" },
132
+ channelContext(),
133
+ );
134
+ expect(result.isError).toBe(true);
135
+ expect(result.content).toContain("does not support reactions");
136
+ expect(reactCalls).toHaveLength(0);
137
+ });
138
+
139
+ test("a channel rejection reaches the model as an error", async () => {
140
+ reactResultOk = false;
141
+ const result = await reactToMessageTool.execute(
142
+ { emoji: "thumbsup" },
143
+ channelContext(),
144
+ );
145
+ expect(result.isError).toBe(true);
146
+ });
147
+
148
+ test("a delivered reaction queues its durable record on the conversation", async () => {
149
+ await reactToMessageTool.execute({ emoji: "thumbsup" }, channelContext());
150
+ expect(queued).toHaveLength(1);
151
+ expect(queued[0]).toEqual({
152
+ conversationId: "conv-1",
153
+ record: {
154
+ channel: "slack",
155
+ chatId: "C123",
156
+ messageId: "1700000000.111111",
157
+ emoji: "thumbsup",
158
+ op: "added",
159
+ provenanceTrustClass: "guardian",
160
+ },
161
+ });
162
+ expect(directPersists).toHaveLength(0);
163
+ });
164
+
165
+ test("a removed reaction queues op removed", async () => {
166
+ await reactToMessageTool.execute(
167
+ { emoji: "thumbsup", action: "remove" },
168
+ channelContext(),
169
+ );
170
+ expect(queued[0]?.record.op).toBe("removed");
171
+ });
172
+
173
+ test("with no resident conversation the record persists directly", async () => {
174
+ conversationResident = false;
175
+ await reactToMessageTool.execute({ emoji: "thumbsup" }, channelContext());
176
+ expect(queued).toHaveLength(0);
177
+ expect(directPersists).toHaveLength(1);
178
+ expect(directPersists[0]?.records[0]?.emoji).toBe("thumbsup");
179
+ });
180
+
181
+ test("a rejected reaction records nothing", async () => {
182
+ reactResultOk = false;
183
+ await reactToMessageTool.execute({ emoji: "thumbsup" }, channelContext());
184
+ expect(queued).toHaveLength(0);
185
+ expect(directPersists).toHaveLength(0);
186
+ });
187
+
188
+ test("rejects input with no emoji", async () => {
189
+ const result = await reactToMessageTool.execute({}, channelContext());
190
+ expect(result.isError).toBe(true);
191
+ expect(reactCalls).toHaveLength(0);
192
+ });
193
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * `react_to_message`: the assistant reacts with an emoji on the current
3
+ * channel conversation.
4
+ *
5
+ * The producer half of the transport's `react` capability: the tool surface
6
+ * only offers this when the turn's channel transport implements `react`
7
+ * (`isToolActiveForContext` gates on `supportsChannelReaction`), so the
8
+ * model never sees the option on a channel that cannot honor it.
9
+ *
10
+ * The default target is the message that triggered the turn, which is the
11
+ * one id every channel turn carries (`ToolContext.sourceMessageId`); an
12
+ * explicit `messageId` in the channel's own id space overrides it for
13
+ * callers that hold one (a skill working from platform APIs).
14
+ */
15
+ import { z } from "zod";
16
+
17
+ import { parseChannelId } from "../../channels/types.js";
18
+ import { findConversationOrSubagent } from "../../daemon/conversation-registry.js";
19
+ import { persistReactionRecords } from "../../daemon/reaction-record.js";
20
+ import {
21
+ sendChannelReaction,
22
+ supportsChannelReaction,
23
+ } from "../../messaging/providers/index.js";
24
+ import { RiskLevel } from "../../permissions/types.js";
25
+ import {
26
+ invalidToolInputResult,
27
+ toToolInputSchema,
28
+ } from "../shared/zod-tool-schema.js";
29
+ import type {
30
+ ToolContext,
31
+ ToolDefinition,
32
+ ToolExecutionResult,
33
+ } from "../types.js";
34
+
35
+ export const reactToMessageInputSchema = z.looseObject({
36
+ emoji: z
37
+ .string()
38
+ .min(1)
39
+ .describe(
40
+ "The emoji, in this channel's own form: a Slack emoji name like " +
41
+ "thumbsup (colons optional), a unicode emoji on other channels.",
42
+ ),
43
+ action: z
44
+ .enum(["add", "remove"])
45
+ .describe("Add the reaction (default) or remove one you added earlier.")
46
+ .optional()
47
+ .catch(undefined),
48
+ messageId: z
49
+ .string()
50
+ .min(1)
51
+ .describe(
52
+ "Target message id in the channel's own id space. Omit to react to " +
53
+ "the message that started this turn, which is the usual case.",
54
+ )
55
+ .optional()
56
+ .catch(undefined),
57
+ });
58
+
59
+ const DESCRIPTION =
60
+ "React with an emoji on the current channel conversation, the way a " +
61
+ "person would tap a reaction instead of writing a reply. By default the " +
62
+ "reaction lands on the message that started this turn. Use it to " +
63
+ "acknowledge without adding a message; it does not replace answering " +
64
+ "when an answer is called for. When the reaction is your whole reply, " +
65
+ "output exactly <no_response/> afterward so no message is posted.";
66
+
67
+ export const reactToMessageTool = {
68
+ name: "react_to_message",
69
+ description: DESCRIPTION,
70
+ category: "interaction",
71
+ executionTarget: "sandbox",
72
+ defaultRiskLevel: RiskLevel.Low,
73
+ input_schema: toToolInputSchema(reactToMessageInputSchema),
74
+
75
+ async execute(
76
+ input: Record<string, unknown>,
77
+ context: ToolContext,
78
+ ): Promise<ToolExecutionResult> {
79
+ const parsed = reactToMessageInputSchema.safeParse(input);
80
+ if (!parsed.success) {
81
+ return invalidToolInputResult("react_to_message", parsed.error);
82
+ }
83
+
84
+ const channel = context.executionChannel;
85
+ const chatId = context.requesterChatId;
86
+ const messageId = parsed.data.messageId ?? context.sourceMessageId;
87
+ if (!channel || !chatId || !messageId) {
88
+ return {
89
+ content:
90
+ "Reactions are only available in a channel conversation with a " +
91
+ "message to react to; this turn has no channel message context.",
92
+ isError: true,
93
+ };
94
+ }
95
+ if (!supportsChannelReaction(channel)) {
96
+ return {
97
+ content: `The ${channel} channel does not support reactions.`,
98
+ isError: true,
99
+ };
100
+ }
101
+
102
+ const action = parsed.data.action ?? "add";
103
+ // The turn's thread coordinate belongs only to the turn's own message;
104
+ // an explicit messageId may live elsewhere, so it travels bare.
105
+ const threadId =
106
+ parsed.data.messageId === undefined ? context.sourceThreadId : undefined;
107
+ const result = await sendChannelReaction(channel, {
108
+ chatId,
109
+ messageId,
110
+ ...(threadId ? { threadId } : {}),
111
+ emoji: parsed.data.emoji,
112
+ action,
113
+ });
114
+ if (!result.ok) {
115
+ return {
116
+ content:
117
+ `Could not ${action} the ${parsed.data.emoji} reaction; the ` +
118
+ "channel rejected it.",
119
+ isError: true,
120
+ };
121
+ }
122
+
123
+ // The delivered reaction becomes a durable row, the same canonical
124
+ // fact inbound reactions store. Queued on the live conversation and
125
+ // drained by the agent loop at the turn boundary, never written here:
126
+ // an assistant row inserted between this call's tool_use and its
127
+ // tool_result would break the pairing history repair enforces, and a
128
+ // reload would read the reaction as having failed. The rare turn with
129
+ // no resident conversation persists directly, trading perfect row
130
+ // ordering for durability.
131
+ const sourceChannel = parseChannelId(channel);
132
+ if (sourceChannel) {
133
+ const record = {
134
+ channel: sourceChannel,
135
+ chatId,
136
+ messageId,
137
+ emoji: parsed.data.emoji,
138
+ op: action === "remove" ? ("removed" as const) : ("added" as const),
139
+ ...(context.trustClass
140
+ ? { provenanceTrustClass: context.trustClass }
141
+ : {}),
142
+ };
143
+ const conversation = findConversationOrSubagent(context.conversationId);
144
+ if (conversation) {
145
+ conversation.queueReactionRecord(record);
146
+ } else {
147
+ await persistReactionRecords(context.conversationId, [record]);
148
+ }
149
+ }
150
+
151
+ return {
152
+ content:
153
+ action === "remove"
154
+ ? `Removed the ${parsed.data.emoji} reaction.`
155
+ : `Reacted with ${parsed.data.emoji}.`,
156
+ isError: false,
157
+ };
158
+ },
159
+ } satisfies ToolDefinition;