@vellumai/assistant 0.11.8 → 0.11.9-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Slack reaction intercept stage.
2
+ * Reaction intercept stage, on any channel.
3
3
  *
4
- * Reactions are passive channel signals — not messages, and not access
5
- * attempts. They are dispatched here *before* the message pipeline (ACL,
6
- * admission floor, disk-pressure block, conversation binding) so that:
4
+ * Reactions are not messages and not access attempts. They are dispatched
5
+ * here *before* the message pipeline (ACL, admission floor, disk-pressure
6
+ * block, conversation binding) so that:
7
7
  *
8
8
  * - a 👍 never triggers an ingress access challenge / verification handshake
9
9
  * or an access-request notification (LUM-2489),
@@ -11,50 +11,64 @@
11
11
  * row — it is dropped as channel noise,
12
12
  * - a known contact's reaction is recorded as an inline transcript signal in
13
13
  * the conversation of the message it was attached to, whether that message
14
- * arrived from the channel or the assistant posted it,
15
- * - a guardian's reaction on an approval card is routed through the guardian
16
- * decision pipeline (the same path as buttons and text replies).
14
+ * arrived from the channel or the assistant posted it.
17
15
  *
18
16
  * The reactor's trust is read solely from the gateway-stamped verdict on
19
17
  * `sourceMetadata`; a missing/failed/contradictory verdict fails closed to
20
- * `unknown` (drop). Reactions never drive an agent turn.
18
+ * `unknown` (drop).
19
+ *
20
+ * One reaction shape addresses the assistant rather than annotating the
21
+ * room: an admitted actor ADDING a reaction to a message the assistant
22
+ * itself authored. That one wakes a discretion turn through the ordinary
23
+ * channel-turn machinery (`buildReactionWakeTurn`); every other reaction
24
+ * stays a passive transcript row and never drives a turn.
21
25
  */
22
26
  import type { SourceMetadata } from "@vellumai/gateway-client";
23
27
  import {
24
28
  type InboundReactionPayload,
25
29
  resolveInboundEventKind,
26
30
  } from "@vellumai/gateway-client";
31
+ import { pickReactionEmojiFields } from "@vellumai/service-contracts/reactions";
27
32
 
28
33
  import type { ChannelId, InterfaceId } from "../../../channels/types.js";
34
+ import { createApprovalCopyGenerator } from "../../../daemon/approval-generators.js";
29
35
  import { findConversation } from "../../../daemon/conversation-registry.js";
30
36
  import { getDiskPressureStatus } from "../../../daemon/disk-pressure-guard.js";
31
37
  import { classifyDiskPressureTurnPolicy } from "../../../daemon/disk-pressure-policy.js";
38
+ import { processMessage } from "../../../daemon/process-message.js";
39
+ import { renderReactionHistoryText } from "../../../daemon/reaction-history-render.js";
32
40
  import type { TrustContext } from "../../../daemon/trust-context-types.js";
33
41
  import { writeSlackMetadata } from "../../../messaging/providers/slack/message-metadata.js";
34
42
  import {
35
43
  buildNeutralReactionMeta,
44
+ buildReactionRowEnvelope,
36
45
  buildSlackReactionMeta,
37
46
  } from "../../../messaging/reaction-envelopes.js";
47
+ import { readProviderMetadata } from "../../../messaging/read-provider-metadata.js";
48
+ import { isGuardianCardRow } from "../../../notifications/approval-card-data.js";
38
49
  import {
39
50
  addMessage,
51
+ getMessageById,
40
52
  provenanceFromTrustContext,
41
53
  } from "../../../persistence/conversation-crud.js";
42
54
  import {
43
- findConversationByProviderMessageId,
44
55
  findInboundEvent,
56
+ findMessageByProviderMessageId,
45
57
  findMessageBySourceId,
46
58
  linkMessage,
47
59
  recordInbound,
60
+ storePayload,
48
61
  } from "../../../persistence/delivery-crud.js";
49
62
  import { markProcessed } from "../../../persistence/delivery-status.js";
63
+ import { extractTextFromStoredMessageContent } from "../../../persistence/message-content.js";
50
64
  import { getLogger } from "../../../util/logger.js";
51
65
  import { toTrustContext } from "../../actor-trust-resolver.js";
52
- import type { ApprovalConversationGenerator } from "../../http-types.js";
66
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../assistant-scope.js";
53
67
  import {
54
68
  actorTrustContextFromVerdict,
55
69
  verdictUsability,
56
70
  } from "../../trust-verdict-consumer.js";
57
- import { handleGuardianReplyIntercept } from "./guardian-reply-intercept.js";
71
+ import { processChannelMessageInBackground } from "./background-dispatch.js";
58
72
 
59
73
  const log = getLogger("runtime-http");
60
74
 
@@ -87,7 +101,6 @@ export interface ReactionInterceptParams {
87
101
  actorUsername: string | undefined;
88
102
  replyCallbackUrl: string | undefined;
89
103
  sourceMetadata: SourceMetadata | undefined;
90
- approvalConversationGenerator: ApprovalConversationGenerator | undefined;
91
104
  }
92
105
 
93
106
  /**
@@ -110,7 +123,6 @@ export async function handleReactionIntercept(
110
123
  actorUsername,
111
124
  replyCallbackUrl,
112
125
  sourceMetadata,
113
- approvalConversationGenerator,
114
126
  } = params;
115
127
 
116
128
  // Classify the reactor from the gateway-stamped verdict — the same source
@@ -142,10 +154,9 @@ export async function handleReactionIntercept(
142
154
 
143
155
  const reactedMessageTs = reaction.targetMessageId;
144
156
  // Respect disk-pressure cleanup so reactions don't bypass storage
145
- // protection. Guardians resolve to `allow-cleanup-mode` (not `block`), so a
146
- // guardian's approval-by-reaction still flows. Blocked silently and before
147
- // any write: a reaction is a passive signal, so the message pipeline's
148
- // "storage is low, try again" notice is meaningless for an emoji.
157
+ // protection. Blocked silently and before any write: a reaction is a
158
+ // passive signal, so the message pipeline's "storage is low, try again"
159
+ // notice is meaningless for an emoji.
149
160
  const diskPressure = classifyDiskPressureTurnPolicy(getDiskPressureStatus(), {
150
161
  sourceChannel,
151
162
  sourceInterface,
@@ -163,9 +174,9 @@ export async function handleReactionIntercept(
163
174
  };
164
175
  }
165
176
 
166
- // A redelivery is answered before anything acts on it. Recording the event
167
- // is what dedups a reaction, and that happens after the guardian rail, so
168
- // without this probe the same emoji could drive a decision twice.
177
+ // A redelivery is answered before the reacted message is resolved, so a
178
+ // repeat delivery costs no lookup. `recordInbound` below is the durable
179
+ // dedup; this is the early out in front of it.
169
180
  const alreadyRecorded = findInboundEvent(
170
181
  sourceChannel,
171
182
  conversationExternalId,
@@ -179,34 +190,6 @@ export async function handleReactionIntercept(
179
190
  };
180
191
  }
181
192
 
182
- // Guardian approval-by-reaction runs before the reacted message is resolved:
183
- // an approval card is addressed by its own message id and is not always a
184
- // stored message. Only `reaction:` (added) expresses intent;
185
- // `reaction_removed:` never does. `handleGuardianReplyIntercept` self-gates
186
- // on `trustClass === "guardian"`, so a contact's reaction returns no
187
- // response and falls through to persistence.
188
- if (reaction.op === "added" && replyCallbackUrl) {
189
- const reactionIntercept = await handleGuardianReplyIntercept({
190
- isDuplicate: false,
191
- trimmedContent: "",
192
- hasCallbackData: true,
193
- callbackData: undefined,
194
- reaction,
195
- reactedMessageTs,
196
- rawSenderId,
197
- canonicalSenderId,
198
- sourceChannel,
199
- conversationExternalId,
200
- replyCallbackUrl,
201
- trustClass: trustCtx.trustClass,
202
- guardianPrincipalId: trustCtx.guardianPrincipalId,
203
- approvalConversationGenerator,
204
- });
205
- if (reactionIntercept.response) {
206
- return reactionIntercept.response;
207
- }
208
- }
209
-
210
193
  // The reaction belongs to the conversation of the message it was attached
211
194
  // to. Slack sends no `thread_ts` on a reaction, so resolving a conversation
212
195
  // from the reaction's own address keys one on the reacted message instead of
@@ -217,19 +200,19 @@ export async function handleReactionIntercept(
217
200
  // them. The assistant's own posts open no inbound event, so a reaction on
218
201
  // one is resolved through the envelope those rows carry (`slackMeta` on
219
202
  // Slack, the neutral `providerMeta` everywhere else).
220
- const targetConversationId = reactedMessageTs
203
+ const target = reactedMessageTs
221
204
  ? (findMessageBySourceId(
222
205
  sourceChannel,
223
206
  conversationExternalId,
224
207
  reactedMessageTs,
225
- )?.conversationId ??
226
- findConversationByProviderMessageId(
208
+ ) ??
209
+ findMessageByProviderMessageId(
227
210
  sourceChannel,
228
211
  conversationExternalId,
229
212
  reactedMessageTs,
230
213
  ))
231
214
  : null;
232
- if (!targetConversationId || !reactedMessageTs) {
215
+ if (!target || !reactedMessageTs) {
233
216
  log.debug(
234
217
  { sourceChannel, conversationExternalId, reactedMessageTs },
235
218
  "Dropping reaction: reacted message is not stored",
@@ -237,6 +220,23 @@ export async function handleReactionIntercept(
237
220
  return { accepted: true, reaction: "dropped_unknown_target" };
238
221
  }
239
222
 
223
+ // A guardian card is a delivery projection, not conversation content.
224
+ // `isGuardianCardRow` is the predicate both history assemblers already use
225
+ // to keep these rows out of a turn's history; the wake path is the third
226
+ // reader of the same question. A reaction on a card has nothing to
227
+ // annotate, and the card is assistant-authored, so without this it would
228
+ // read as a signal addressed to the assistant and wake a turn. Channel
229
+ // deliveries persist no row today, but ones paired before the
230
+ // projection-only policy are still stored.
231
+ const targetRow = getMessageById(target.messageId, target.conversationId);
232
+ if (isGuardianCardRow(targetRow?.content)) {
233
+ log.debug(
234
+ { sourceChannel, conversationExternalId, reactedMessageTs },
235
+ "Dropping reaction: reacted message is a guardian card",
236
+ );
237
+ return { accepted: true, reaction: "dropped_guardian_card" };
238
+ }
239
+
240
240
  const result = recordInbound(
241
241
  sourceChannel,
242
242
  conversationExternalId,
@@ -245,11 +245,20 @@ export async function handleReactionIntercept(
245
245
  // own message, and a reaction is not one. Claiming the reacted message's
246
246
  // id here would put two linked rows on that id, leaving a later edit or
247
247
  // delete of it free to resolve to the reaction instead.
248
- { conversationId: targetConversationId },
248
+ { conversationId: target.conversationId },
249
249
  );
250
+ if (result.duplicate) {
251
+ return {
252
+ accepted: result.accepted,
253
+ duplicate: true,
254
+ eventId: result.eventId,
255
+ };
256
+ }
250
257
 
251
- // Record the reaction as an inline transcript signal.
252
- try {
258
+ const rowTrustContext = toTrustContext(trustCtx, conversationExternalId);
259
+ const actorExternalId = canonicalSenderId ?? rawSenderId ?? undefined;
260
+
261
+ const persistPassively = async (): Promise<void> => {
253
262
  await persistReactionAsMessage({
254
263
  conversationId: result.conversationId,
255
264
  conversationExternalId,
@@ -257,21 +266,70 @@ export async function handleReactionIntercept(
257
266
  sourceChannel,
258
267
  reaction,
259
268
  actorDisplayName,
260
- actorExternalId: canonicalSenderId ?? rawSenderId ?? undefined,
269
+ actorExternalId,
261
270
  reactedMessageTs,
262
- duplicate: result.duplicate,
263
- trustCtx: toTrustContext(trustCtx, conversationExternalId),
271
+ duplicate: false,
272
+ trustCtx: rowTrustContext,
264
273
  });
265
- if (!result.duplicate) {
266
- // Reactions never drive a turn, so a resident conversation would
267
- // otherwise carry the new row only after eviction. Marking it stale
268
- // makes the next turn's history reload pick the reaction up.
269
- findConversation(result.conversationId)?.markHistoryStale();
274
+ // A passive row lands in the store only, so a resident conversation
275
+ // would otherwise carry it only after eviction. Marking it stale makes
276
+ // the next turn's history reload pick the reaction up.
277
+ findConversation(result.conversationId)?.markHistoryStale();
278
+ };
279
+
280
+ // An admitted actor ADDING a reaction to the assistant's own message is a
281
+ // signal addressed to the assistant, like a thumbs-up sent to a person: it
282
+ // wakes a discretion turn. Everything else stays a passive transcript
283
+ // row: removals retract rather than address, and a reaction on another
284
+ // participant's message is between-humans signaling. Mechanics on
285
+ // `buildReactionWakeTurn`.
286
+ const wakeTurn =
287
+ reaction.op === "added" && replyCallbackUrl && sourceInterface
288
+ ? buildReactionWakeTurn({
289
+ target,
290
+ targetRow,
291
+ result,
292
+ reaction,
293
+ sourceChannel,
294
+ sourceInterface,
295
+ conversationExternalId,
296
+ replyCallbackUrl,
297
+ actorDisplayName,
298
+ actorExternalId,
299
+ reactedMessageTs,
300
+ trustCtx: rowTrustContext,
301
+ chatType: sourceMetadata?.chatType?.trim() || undefined,
302
+ persistPassively,
303
+ })
304
+ : null;
305
+ if (wakeTurn) {
306
+ try {
307
+ wakeTurn();
308
+ return {
309
+ accepted: result.accepted,
310
+ duplicate: false,
311
+ eventId: result.eventId,
312
+ reaction: "wake_dispatched",
313
+ };
314
+ } catch (err) {
315
+ // Dispatch startup failed before the background turn owns the event
316
+ // (its own failures degrade through `onTurnLostToBusy`). The dedup
317
+ // record already exists, so a gateway retry would short-circuit and
318
+ // leave the reaction unrecorded entirely; fall through to the passive
319
+ // row, which is what this reaction would have been without the wake.
320
+ log.error(
321
+ { err, conversationId: result.conversationId, eventId: result.eventId },
322
+ "Reaction wake dispatch failed to start; recording the passive row",
323
+ );
270
324
  }
325
+ }
326
+
327
+ try {
328
+ await persistPassively();
271
329
  } catch (err) {
272
330
  log.error(
273
331
  { err, conversationId: result.conversationId, eventId: result.eventId },
274
- "Failed to persist Slack reaction event",
332
+ "Failed to persist reaction event",
275
333
  );
276
334
  }
277
335
 
@@ -282,11 +340,163 @@ export async function handleReactionIntercept(
282
340
  };
283
341
  }
284
342
 
343
+ /**
344
+ * Build the dispatch thunk for a reaction-driven discretion turn, or null
345
+ * when the reaction does not qualify: only a reaction on a message the
346
+ * assistant itself authored addresses the assistant.
347
+ *
348
+ * The turn rides `processChannelMessageInBackground` unchanged, so it defers
349
+ * behind an in-flight turn, streams and delivers its reply through the
350
+ * channel rail, and a `<no_response/>` outcome is suppressed everywhere. Its
351
+ * persisted user row carries the same reaction envelope the passive path
352
+ * writes, so the row reads as a reaction to every envelope consumer, and its
353
+ * live content is the same line `renderReactionHistoryText` produces at
354
+ * reload, so the model sees one wording on both paths.
355
+ *
356
+ * Wake turns are at-most-once. The retry sweep's processing lane rebuilds a
357
+ * turn from its stored payload as a plain message, which would corrupt a
358
+ * reaction into a fabricated user message, so the event is marked processed
359
+ * up front and the payload it stores is delivery-only: it names where a
360
+ * generated reply goes and carries no content to rebuild a turn from. A
361
+ * crash mid-turn therefore loses the discretionary turn, never a row the
362
+ * store already held. The one recoverable loss, a non-channel turn stealing
363
+ * the lock after admission, degrades through `persistPassively` to exactly
364
+ * the row the passive path would have written.
365
+ */
366
+ function buildReactionWakeTurn(params: {
367
+ target: { messageId: string; conversationId: string };
368
+ targetRow: ReturnType<typeof getMessageById>;
369
+ result: { eventId: string; conversationId: string };
370
+ reaction: InboundReactionPayload;
371
+ sourceChannel: ChannelId;
372
+ sourceInterface: InterfaceId;
373
+ conversationExternalId: string;
374
+ replyCallbackUrl: string;
375
+ actorDisplayName: string | undefined;
376
+ actorExternalId: string | undefined;
377
+ reactedMessageTs: string;
378
+ trustCtx: TrustContext;
379
+ chatType: string | undefined;
380
+ persistPassively: () => Promise<void>;
381
+ }): (() => void) | null {
382
+ const targetRow = params.targetRow;
383
+ if (!targetRow || targetRow.role !== "assistant") {
384
+ return null;
385
+ }
386
+
387
+ const facts = reactionFacts(params);
388
+ const neutralMeta = buildNeutralReactionMeta(facts);
389
+ const targetText = extractTextFromStoredMessageContent(targetRow.content);
390
+ const content = renderReactionHistoryText(
391
+ neutralMeta,
392
+ () => targetText || undefined,
393
+ { selfAuthored: false },
394
+ );
395
+ if (!content) {
396
+ return null;
397
+ }
398
+ // The reaction envelope rides the same carrier lanes ordinary ingress
399
+ // uses: the neutral `channelInbound` for every non-Slack channel (its
400
+ // lane validates through the canonical schema and passes reaction-kind
401
+ // envelopes verbatim), and the transitional Slack-only field while Slack
402
+ // still writes its own envelope. The passive path keeps
403
+ // `buildReactionRowEnvelope`; the lanes here materialize the same
404
+ // builders' facts, so the two writers still cannot drift.
405
+ const envelopeCarrier =
406
+ params.sourceChannel === "slack"
407
+ ? {
408
+ slackReactionRowMeta: writeSlackMetadata(
409
+ buildSlackReactionMeta(facts),
410
+ ),
411
+ }
412
+ : { channelInbound: neutralMeta };
413
+
414
+ const replyCallbackUrl = resolveWakeReplyCallbackUrl(
415
+ params.sourceChannel,
416
+ params.replyCallbackUrl,
417
+ targetRow.metadata,
418
+ );
419
+
420
+ return () => {
421
+ // A delivery-only payload: the fields `channel-retry-sweep`'s delivery
422
+ // lane needs to re-post a generated reply whose first delivery failed
423
+ // transiently. It deliberately carries no `content` or `sourceChannel`,
424
+ // so it can never be replayed as a message turn even if this event
425
+ // somehow reached the processing lane; that replay is what the
426
+ // at-most-once design exists to prevent.
427
+ storePayload(params.result.eventId, {
428
+ replyCallbackUrl,
429
+ externalChatId: params.conversationExternalId,
430
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
431
+ });
432
+ markProcessed(params.result.eventId);
433
+ processChannelMessageInBackground({
434
+ processMessage,
435
+ conversationId: params.result.conversationId,
436
+ eventId: params.result.eventId,
437
+ content,
438
+ sourceChannel: params.sourceChannel,
439
+ sourceInterface: params.sourceInterface,
440
+ externalChatId: params.conversationExternalId,
441
+ trustCtx: params.trustCtx,
442
+ metadataHints: [],
443
+ replyCallbackUrl,
444
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
445
+ approvalCopyGenerator: createApprovalCopyGenerator(),
446
+ ...(params.chatType ? { chatType: params.chatType } : {}),
447
+ ...envelopeCarrier,
448
+ clientMessageId: `reaction:${params.result.eventId}`,
449
+ skipUserMessageIndexing: true,
450
+ onTurnLostToBusy: params.persistPassively,
451
+ });
452
+ };
453
+ }
454
+
455
+ /**
456
+ * Where a wake turn's reply goes, which is the conversation the reacted
457
+ * message lives in rather than the reaction's own address.
458
+ *
459
+ * A reaction event names no thread of its own, so Slack's normalizer fills
460
+ * the callback's `threadTs` with the REACTED MESSAGE's ts. Delivering
461
+ * through it would root the reply at that message: a thread of its own off
462
+ * a channel post, or an invalid parent when the target is itself a threaded
463
+ * reply. Slack rows record the thread they live in, so the target row's
464
+ * stored `threadId` is the authoritative destination, and its absence means
465
+ * the target sits at the channel root, where the reply belongs too.
466
+ *
467
+ * Every other channel is already correct: a Discord thread IS a channel, so
468
+ * a reaction inside one carries that thread as its address.
469
+ */
470
+ function resolveWakeReplyCallbackUrl(
471
+ sourceChannel: ChannelId,
472
+ reactionCallbackUrl: string,
473
+ targetMetadata: string | null,
474
+ ): string {
475
+ if (sourceChannel !== "slack") {
476
+ return reactionCallbackUrl;
477
+ }
478
+ let url: URL;
479
+ try {
480
+ url = new URL(reactionCallbackUrl);
481
+ } catch {
482
+ return reactionCallbackUrl;
483
+ }
484
+ const targetThreadTs = readProviderMetadata(targetMetadata, {
485
+ allowFlatLegacy: true,
486
+ })?.threadId;
487
+ if (targetThreadTs) {
488
+ url.searchParams.set("threadTs", targetThreadTs);
489
+ } else {
490
+ url.searchParams.delete("threadTs");
491
+ }
492
+ return url.toString();
493
+ }
494
+
285
495
  /**
286
496
  * Persist a Slack reaction event as a `messages` row with a `slackMeta`
287
497
  * envelope so the renderer can surface it inline in the chronological
288
- * transcript. Reactions do not trigger an agent response — the row is written
289
- * and the inbound event is linked, but the agent loop is not dispatched.
498
+ * transcript. The row is written and the inbound event is linked; the wake
499
+ * decision is the caller's (`buildReactionWakeTurn`).
290
500
  *
291
501
  * The caller is expected to have run `recordInbound` already so that
292
502
  * deduplication and conversation resolution have happened. Duplicate inbound
@@ -319,42 +529,10 @@ async function persistReactionAsMessage(params: {
319
529
  if (params.duplicate) {
320
530
  return;
321
531
  }
322
- const facts = {
323
- channel: params.sourceChannel,
324
- chatId: params.conversationExternalId,
325
- targetMessageId: params.reactedMessageTs,
326
- emoji: params.reaction.emoji,
327
- op: params.reaction.op,
328
- ...(params.actorExternalId
329
- ? { actorExternalId: params.actorExternalId }
330
- : {}),
331
- ...(params.actorDisplayName
332
- ? { actorDisplayName: params.actorDisplayName }
333
- : {}),
334
- };
532
+ const facts = reactionFacts(params);
335
533
 
336
- if (params.sourceChannel !== "slack") {
337
- const persisted = await addMessage(
338
- params.conversationId,
339
- "user",
340
- "[reaction]",
341
- {
342
- metadata: {
343
- ...provenanceFromTrustContext(params.trustCtx),
344
- providerMeta: JSON.stringify(buildNeutralReactionMeta(facts)),
345
- },
346
- skipIndexing: true,
347
- },
348
- );
349
- linkMessage(params.eventId, persisted.id);
350
- markProcessed(params.eventId);
351
- return;
352
- }
353
-
354
- const slackMeta = buildSlackReactionMeta(facts);
355
-
356
- // Sentinel content — Slack transcript renderers read `slackMeta` to format
357
- // the reaction line; the literal text is never displayed to the model.
534
+ // Sentinel content: transcript renderers read the envelope to format the
535
+ // reaction line; the literal text is never displayed to the model.
358
536
  const persisted = await addMessage(
359
537
  params.conversationId,
360
538
  "user",
@@ -362,7 +540,7 @@ async function persistReactionAsMessage(params: {
362
540
  {
363
541
  metadata: {
364
542
  ...provenanceFromTrustContext(params.trustCtx),
365
- slackMeta: writeSlackMetadata(slackMeta),
543
+ ...buildReactionRowEnvelope(facts),
366
544
  },
367
545
  skipIndexing: true,
368
546
  },
@@ -370,3 +548,32 @@ async function persistReactionAsMessage(params: {
370
548
  linkMessage(params.eventId, persisted.id);
371
549
  markProcessed(params.eventId);
372
550
  }
551
+
552
+ /**
553
+ * The envelope facts of one inbound reaction, from the intercept's own
554
+ * vocabulary. Shared by the passive row and the wake turn's row so the two
555
+ * writers describe the same reaction identically.
556
+ */
557
+ function reactionFacts(params: {
558
+ sourceChannel: ChannelId;
559
+ conversationExternalId: string;
560
+ reactedMessageTs: string;
561
+ reaction: InboundReactionPayload;
562
+ actorExternalId?: string;
563
+ actorDisplayName?: string;
564
+ }) {
565
+ return {
566
+ channel: params.sourceChannel,
567
+ chatId: params.conversationExternalId,
568
+ targetMessageId: params.reactedMessageTs,
569
+ emoji: params.reaction.emoji,
570
+ ...pickReactionEmojiFields(params.reaction),
571
+ op: params.reaction.op,
572
+ ...(params.actorExternalId
573
+ ? { actorExternalId: params.actorExternalId }
574
+ : {}),
575
+ ...(params.actorDisplayName
576
+ ? { actorDisplayName: params.actorDisplayName }
577
+ : {}),
578
+ };
579
+ }
@@ -139,6 +139,7 @@ import { ROUTES as SCHEDULE_WORKER_ROUTES } from "./schedule-worker-routes.js";
139
139
  import { ROUTES as SECRET_ROUTES } from "./secret-routes.js";
140
140
  import { ROUTES as SEQUENCE_ROUTES } from "./sequence-routes.js";
141
141
  import { ROUTES as SETTINGS_ROUTES } from "./settings-routes.js";
142
+ import { ROUTES as SIGHT_FRAME_ROUTES } from "./sight-frame-routes.js";
142
143
  import { ROUTES as SKILL_ROUTES } from "./skills-routes.js";
143
144
  import { ROUTES as SLACK_CHANNEL_RESOLVE_ROUTES } from "./slack-channel-routes.js";
144
145
  import { ROUTES as SOUNDS_CONFIG_ROUTES } from "./sounds-config-routes.js";
@@ -287,6 +288,7 @@ export const ROUTES: RouteDefinition[] = [
287
288
  ...SANITY_ROUTES,
288
289
  ...SECRET_ROUTES,
289
290
  ...SETTINGS_ROUTES,
291
+ ...SIGHT_FRAME_ROUTES,
290
292
  ...SOUNDS_CONFIG_ROUTES,
291
293
  ...SKILL_ROUTES,
292
294
  ...A2A_ROUTES,