@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
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
3
3
  import type { GuardianDelivery } from "@vellumai/gateway-client";
4
4
  import { hashVerificationSecret } from "@vellumai/gateway-client";
5
5
  import { MarkChannelRevokedIpcResponseSchema } from "@vellumai/gateway-client/gateway-ipc-contracts";
6
+ import { z } from "zod";
6
7
 
7
8
  import { startVerificationCall } from "../../calls/call-domain.js";
8
9
  import {
@@ -14,7 +15,7 @@ import {
14
15
  revokePendingSessions,
15
16
  updateSessionDelivery,
16
17
  } from "../../channels/gateway-verification-sessions.js";
17
- import type { ChannelId } from "../../channels/types.js";
18
+ import { CHANNEL_IDS, type ChannelId } from "../../channels/types.js";
18
19
  import {
19
20
  findContactChannel,
20
21
  getChannelById,
@@ -173,9 +174,38 @@ export async function createInboundChallenge(
173
174
  };
174
175
  }
175
176
 
177
+ /**
178
+ * Exactly what {@link getVerificationStatus} returns.
179
+ *
180
+ * Its own shape rather than the wide {@link ChannelVerificationSessionResult},
181
+ * which spans five different responses and so marks every field optional. A
182
+ * status caller always gets the binding verdict, and the route publishes this
183
+ * as its response body so clients read it from a generated type.
184
+ */
185
+ export const verificationStatusResponseSchema = z.object({
186
+ success: z.boolean(),
187
+ bound: z.boolean(),
188
+ channel: z.enum(CHANNEL_IDS),
189
+ assistantId: z.string(),
190
+ hasPendingChallenge: z.boolean(),
191
+ guardianExternalUserId: z.string().optional(),
192
+ guardianDeliveryChatId: z.string().optional(),
193
+ guardianUsername: z.string().optional(),
194
+ guardianDisplayName: z.string().optional(),
195
+ verificationSessionId: z.string().optional(),
196
+ expiresAt: z.number().optional(),
197
+ nextResendAt: z.number().nullable().optional(),
198
+ sendCount: z.number().optional(),
199
+ pendingBootstrap: z.boolean().optional(),
200
+ });
201
+
202
+ export type VerificationStatusResponse = z.infer<
203
+ typeof verificationStatusResponseSchema
204
+ >;
205
+
176
206
  export async function getVerificationStatus(
177
207
  channel?: ChannelId,
178
- ): Promise<ChannelVerificationSessionResult> {
208
+ ): Promise<VerificationStatusResponse> {
179
209
  const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
180
210
  const resolvedChannel = channel ?? "telegram";
181
211
 
@@ -215,7 +245,7 @@ export async function getVerificationStatus(
215
245
  findActiveSession(resolvedChannel, { verificationPurpose: "guardian" }),
216
246
  ]);
217
247
  const hasPendingChallenge = pendingSession != null;
218
- const outboundFields: Record<string, unknown> = {};
248
+ const outboundFields: Partial<VerificationStatusResponse> = {};
219
249
  if (activeOutboundSession) {
220
250
  outboundFields.verificationSessionId = activeOutboundSession.id;
221
251
  outboundFields.expiresAt = activeOutboundSession.expiresAt;
@@ -65,6 +65,7 @@ describe("projectProviderForWire", () => {
65
65
  const wire = projectProviderForWire(gemini!);
66
66
  const modelIds = wire.models.map((model) => model.id);
67
67
  const expectedGemini3ModelIds = [
68
+ "gemini-3.8-flash",
68
69
  "gemini-3.7-flash",
69
70
  "gemini-3.6-flash",
70
71
  "gemini-3.5-flash",
@@ -15,6 +15,7 @@ import { ConfirmationDecisionSchema } from "../../api/responses/conversation-mes
15
15
  import { getConfig } from "../../config/loader.js";
16
16
  import type { LLMCallSite, Speed } from "../../config/schemas/llm.js";
17
17
  import { ipcCall as gatewayIpcCall } from "../../ipc/gateway-client.js";
18
+ import type { ProviderMessageMetadata } from "../../messaging/provider-message-metadata.js";
18
19
  import type { SecretPromptResult } from "../../permissions/secret-prompt-types.js";
19
20
  import type { ConversationCreateType } from "../../persistence/conversation-types.js";
20
21
  import { resolveMediaSourceData } from "../../providers/media-resolve.js";
@@ -223,6 +224,14 @@ export interface ConversationCreateOptions {
223
224
  * chronological renderer to consume.
224
225
  */
225
226
  slackInbound?: SlackInboundMessageMetadata;
227
+ /**
228
+ * Neutral per-row channel envelope captured at the channel ingress
229
+ * boundary, the non-Slack counterpart of `slackInbound`. When present (and
230
+ * the turn channel matches its `source`), persistence writes it as the
231
+ * message's `providerMeta` metadata key so the stored row can say which
232
+ * external message it is, in which thread, from whom.
233
+ */
234
+ channelInbound?: ProviderMessageMetadata;
226
235
  /**
227
236
  * Conversation type for newly created conversations. When omitted,
228
237
  * defaults to `"standard"` (visible in the sidebar). Set to
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { createConversation } from "../persistence/conversation-crud.js";
4
+ import { getDb } from "../persistence/db-connection.js";
5
+ import { initializeDb } from "../persistence/db-init.js";
6
+ import {
7
+ assembleUserContentBlocks,
8
+ offloadLinkPlan,
9
+ offloadOversizedText,
10
+ OVERSIZED_CONTENT_FILENAME_PREFIX,
11
+ OVERSIZED_CONTENT_NOTE,
12
+ oversizedContentFilename,
13
+ } from "./port-oversized-content.js";
14
+
15
+ const OVERSIZED_FILENAME_RE = /^oversized-content-[0-9a-f-]{36}\.txt$/;
16
+
17
+ await initializeDb();
18
+
19
+ function resetTables() {
20
+ const db = getDb();
21
+ db.run("DELETE FROM message_attachments");
22
+ db.run("DELETE FROM attachments");
23
+ db.run("DELETE FROM messages");
24
+ db.run("DELETE FROM conversations");
25
+ }
26
+
27
+ describe("offloadOversizedText", () => {
28
+ test("leaves text under the cap unchanged", async () => {
29
+ resetTables();
30
+ const conv = createConversation();
31
+ const result = await offloadOversizedText("hello", {
32
+ conversationId: conv.id,
33
+ conversationCreatedAt: conv.createdAt,
34
+ });
35
+ expect(result).toEqual({ text: "hello" });
36
+ });
37
+
38
+ test("ports over-cap text into a workspace_ref attachment", async () => {
39
+ resetTables();
40
+ const conv = createConversation();
41
+ const original = "x".repeat(32);
42
+ const result = await offloadOversizedText(
43
+ original,
44
+ {
45
+ conversationId: conv.id,
46
+ conversationCreatedAt: conv.createdAt,
47
+ },
48
+ 16,
49
+ );
50
+
51
+ expect(result.text).toBe(OVERSIZED_CONTENT_NOTE);
52
+ expect(result.attachmentId).toBeDefined();
53
+ expect(result.filename).toMatch(OVERSIZED_FILENAME_RE);
54
+ expect(result.fileBlock).toMatchObject({
55
+ type: "file",
56
+ source: {
57
+ type: "workspace_ref",
58
+ media_type: "text/plain",
59
+ filename: result.filename,
60
+ attachmentId: result.attachmentId,
61
+ },
62
+ });
63
+ expect(JSON.stringify(result)).not.toContain(original);
64
+ if (result.fileBlock === undefined) {
65
+ throw new Error("expected an offloaded file block");
66
+ }
67
+
68
+ const blocks = assembleUserContentBlocks(result.text, [], result.fileBlock);
69
+ expect(JSON.stringify(blocks)).not.toContain(original);
70
+ expect(blocks[0]).toEqual({ type: "text", text: OVERSIZED_CONTENT_NOTE });
71
+ expect(blocks[1]).toEqual(result.fileBlock);
72
+ });
73
+
74
+ test("assigns a distinct generated filename per offload in one conversation", async () => {
75
+ resetTables();
76
+ const conv = createConversation();
77
+ const ctx = {
78
+ conversationId: conv.id,
79
+ conversationCreatedAt: conv.createdAt,
80
+ };
81
+ const first = await offloadOversizedText("a".repeat(32), ctx, 16);
82
+ const second = await offloadOversizedText("b".repeat(32), ctx, 16);
83
+
84
+ expect(first.filename).toMatch(OVERSIZED_FILENAME_RE);
85
+ expect(second.filename).toMatch(OVERSIZED_FILENAME_RE);
86
+ expect(first.filename).not.toBe(second.filename);
87
+ expect(first.filename).toContain(OVERSIZED_CONTENT_FILENAME_PREFIX);
88
+ });
89
+ });
90
+
91
+ describe("oversizedContentFilename", () => {
92
+ test("embeds the given id", () => {
93
+ expect(oversizedContentFilename("11111111-1111-1111-1111-111111111111")).toBe(
94
+ "oversized-content-11111111-1111-1111-1111-111111111111.txt",
95
+ );
96
+ });
97
+ });
98
+
99
+ describe("offloadLinkPlan", () => {
100
+ test("same persist and live id is linked and model-facing", () => {
101
+ const plan = offloadLinkPlan("att-1", "att-1");
102
+ expect(plan.linkIds).toEqual(["att-1"]);
103
+ expect([...plan.modelFacingIds]).toEqual(["att-1"]);
104
+ });
105
+
106
+ test("display-only persist offload is linked but not model-facing", () => {
107
+ const plan = offloadLinkPlan("att-display", undefined);
108
+ expect(plan.linkIds).toEqual(["att-display"]);
109
+ expect(plan.modelFacingIds.size).toBe(0);
110
+ });
111
+
112
+ test("distinct persist and live ids keep only live model-facing", () => {
113
+ const plan = offloadLinkPlan("att-display", "att-live");
114
+ expect(plan.linkIds).toEqual(["att-display", "att-live"]);
115
+ expect([...plan.modelFacingIds]).toEqual(["att-live"]);
116
+ });
117
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Move a user text payload that exceeds the provider string cap into the
3
+ * conversation attachment store, and leave a short note plus a workspace_ref
4
+ * in the message. The full text stays on disk, off the model prompt.
5
+ */
6
+
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ import { createInlineAttachment } from "../persistence/attachments-store.js";
10
+ import {
11
+ MAX_PROVIDER_STRING_BYTES,
12
+ utf8ByteLength,
13
+ } from "../providers/content-block-size.js";
14
+ import type { ContentBlock } from "../providers/types.js";
15
+
16
+ export const OVERSIZED_CONTENT_FILENAME_PREFIX = "oversized-content";
17
+
18
+ export const OVERSIZED_CONTENT_NOTE =
19
+ "Content exceeded the provider size limit and was saved as a workspace file.";
20
+
21
+ /** Unique per offload so two oversized payloads in one conversation do not share a name. */
22
+ export function oversizedContentFilename(id: string = randomUUID()): string {
23
+ return `${OVERSIZED_CONTENT_FILENAME_PREFIX}-${id}.txt`;
24
+ }
25
+
26
+ export interface PortOversizedContext {
27
+ conversationId: string;
28
+ conversationCreatedAt: number;
29
+ }
30
+
31
+ export interface OffloadOversizedTextResult {
32
+ text: string;
33
+ fileBlock?: Extract<ContentBlock, { type: "file" }>;
34
+ attachmentId?: string;
35
+ filename?: string;
36
+ }
37
+
38
+ export async function offloadOversizedText(
39
+ text: string,
40
+ ctx: PortOversizedContext,
41
+ maxBytes: number = MAX_PROVIDER_STRING_BYTES,
42
+ ): Promise<OffloadOversizedTextResult> {
43
+ if (utf8ByteLength(text) <= maxBytes) {
44
+ return { text };
45
+ }
46
+
47
+ const filename = oversizedContentFilename();
48
+ const stored = await createInlineAttachment(
49
+ ctx.conversationId,
50
+ ctx.conversationCreatedAt,
51
+ filename,
52
+ "text/plain",
53
+ Buffer.from(text, "utf8").toString("base64"),
54
+ { skipSizeLimit: true },
55
+ );
56
+
57
+ return {
58
+ text: OVERSIZED_CONTENT_NOTE,
59
+ fileBlock: {
60
+ type: "file",
61
+ source: {
62
+ type: "workspace_ref",
63
+ media_type: "text/plain",
64
+ attachmentId: stored.id,
65
+ sizeBytes: stored.sizeBytes,
66
+ filename,
67
+ },
68
+ },
69
+ attachmentId: stored.id,
70
+ filename,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Persist may offload a display-only copy (raw Slack, etc.) that must stay
76
+ * linked for GC but must not be named on the model-facing attachment list.
77
+ * The live offload is the only one `enrichMessageWithSourcePaths` may see.
78
+ */
79
+ export function offloadLinkPlan(
80
+ persistAttachmentId: string | undefined,
81
+ liveAttachmentId: string | undefined,
82
+ ): { linkIds: string[]; modelFacingIds: ReadonlySet<string> } {
83
+ const linkIds = [
84
+ ...new Set(
85
+ [persistAttachmentId, liveAttachmentId].filter(
86
+ (id): id is string => id !== undefined,
87
+ ),
88
+ ),
89
+ ];
90
+ return {
91
+ linkIds,
92
+ modelFacingIds: new Set(liveAttachmentId ? [liveAttachmentId] : []),
93
+ };
94
+ }
95
+
96
+ export function assembleUserContentBlocks(
97
+ text: string,
98
+ attachmentBlocks: ContentBlock[],
99
+ extraFileBlock?: ContentBlock,
100
+ ): ContentBlock[] {
101
+ const blocks: ContentBlock[] = [];
102
+ if (text.trim().length > 0) {
103
+ blocks.push({ type: "text", text });
104
+ }
105
+ blocks.push(...attachmentBlocks);
106
+ if (extraFileBlock) {
107
+ blocks.push(extraFileBlock);
108
+ }
109
+ return blocks;
110
+ }
@@ -38,6 +38,7 @@ import {
38
38
  import { getLogger } from "../util/logger.js";
39
39
  import type { Conversation } from "./conversation.js";
40
40
  import {
41
+ buildProviderMetaForPersistence,
41
42
  buildSlackMetaForPersistence,
42
43
  CONVERSATION_BUSY_MESSAGE,
43
44
  serializePersistedUserMessageContent,
@@ -124,6 +125,17 @@ type ProcessMessageOptions = ConversationCreateOptions & {
124
125
  * {@link deriveIngressIdempotencyKey}).
125
126
  */
126
127
  clientMessageId?: string;
128
+ /**
129
+ * Serialized `slackMeta` reaction envelope for a Slack reaction-driven
130
+ * turn, stamped onto the persisted user row so it reads as a reaction to
131
+ * every envelope consumer. Slack-only on purpose: every other channel's
132
+ * reaction turn rides `channelInbound`, whose lane declines Slack while
133
+ * Slack writes its own envelope.
134
+ *
135
+ * TRANSITIONAL: dies when Slack joins the neutral `providerMeta`
136
+ * envelope; do not extend it.
137
+ */
138
+ slackReactionRowMeta?: string;
127
139
  };
128
140
 
129
141
  /**
@@ -148,6 +160,58 @@ export function deriveIngressIdempotencyKey(options?: {
148
160
  return undefined;
149
161
  }
150
162
 
163
+ /**
164
+ * The transient carrier bag `persistUserMessage` strips back out of the
165
+ * metadata: the channel ingress envelopes threaded through options.
166
+ * Undefined when the turn carries neither, so non-channel turns persist no
167
+ * extra keys.
168
+ */
169
+ function buildIngressCarrierMetadata(
170
+ options?: ProcessMessageOptions,
171
+ ): Record<string, unknown> | undefined {
172
+ if (!options?.slackInbound && !options?.channelInbound) {
173
+ return undefined;
174
+ }
175
+ return {
176
+ ...(options.slackInbound ? { slackInbound: options.slackInbound } : {}),
177
+ ...(options.channelInbound
178
+ ? { channelInbound: options.channelInbound }
179
+ : {}),
180
+ };
181
+ }
182
+
183
+ /**
184
+ * The metadata a server-side ingress turn persists with its user row: the
185
+ * transient carrier bag above, plus the pre-built `slackMeta` a Slack
186
+ * reaction turn stamps directly (see `slackReactionRowMeta`).
187
+ */
188
+ function buildPersistMetadata(
189
+ options?: ProcessMessageOptions,
190
+ ): Record<string, unknown> | undefined {
191
+ const carrier = buildIngressCarrierMetadata(options);
192
+ if (!options?.slackReactionRowMeta) {
193
+ return carrier;
194
+ }
195
+ return { slackMeta: options.slackReactionRowMeta, ...carrier };
196
+ }
197
+
198
+ /**
199
+ * Layer the turn's persisted channel envelopes onto a metadata object:
200
+ * `slackMeta` for Slack turns, `providerMeta` for every other channel.
201
+ * At most one is non-null per turn.
202
+ */
203
+ function withChannelEnvelopes(
204
+ base: Record<string, unknown>,
205
+ slackMeta: string | null,
206
+ providerMeta: string | null,
207
+ ): Record<string, unknown> {
208
+ return {
209
+ ...base,
210
+ ...(slackMeta ? { slackMeta } : {}),
211
+ ...(providerMeta ? { providerMeta } : {}),
212
+ };
213
+ }
214
+
151
215
  function buildEventEmitter(
152
216
  observer?: (msg: AssistantEvent) => void,
153
217
  ): (msg: AssistantEvent) => void {
@@ -432,9 +496,14 @@ export async function processMessage(
432
496
  });
433
497
  const slashResult = await resolveSlash(content, slashContext);
434
498
 
499
+ const turnChannel = conversation.getTurnChannelContext()?.userMessageChannel;
435
500
  const slackMeta = buildSlackMetaForPersistence({
436
501
  slackInbound: options?.slackInbound,
437
- turnChannel: conversation.getTurnChannelContext()?.userMessageChannel,
502
+ turnChannel,
503
+ });
504
+ const providerMeta = buildProviderMetaForPersistence({
505
+ channelInbound: options?.channelInbound,
506
+ turnChannel,
438
507
  });
439
508
 
440
509
  if (slashResult.kind === "unknown") {
@@ -469,9 +538,11 @@ export async function processMessage(
469
538
  : {}),
470
539
  ...(Object.keys(imageSourcePaths).length > 0 ? { imageSourcePaths } : {}),
471
540
  };
472
- const userMetaWithSlack = slackMeta
473
- ? { ...serverChannelMeta, slackMeta }
474
- : serverChannelMeta;
541
+ const userMetaWithSlack = withChannelEnvelopes(
542
+ serverChannelMeta,
543
+ slackMeta,
544
+ providerMeta,
545
+ );
475
546
  const cleanMsg = await createUserMessage(content, attachments);
476
547
  const llmMsg = enrichMessageWithSourcePaths(cleanMsg, attachments);
477
548
  const persisted = await addMessage(
@@ -566,9 +637,11 @@ export async function processMessage(
566
637
  }
567
638
  : {}),
568
639
  };
569
- const compactUserMeta = slackMeta
570
- ? { ...compactChannelMeta, slackMeta }
571
- : compactChannelMeta;
640
+ const compactUserMeta = withChannelEnvelopes(
641
+ compactChannelMeta,
642
+ slackMeta,
643
+ providerMeta,
644
+ );
572
645
  const cleanMsg = await createUserMessage(content, attachments);
573
646
  const persisted = await addMessage(
574
647
  conversationId,
@@ -624,9 +697,11 @@ export async function processMessage(
624
697
  }
625
698
  : {}),
626
699
  };
627
- const cleanUserMeta = slackMeta
628
- ? { ...cleanChannelMeta, slackMeta }
629
- : cleanChannelMeta;
700
+ const cleanUserMeta = withChannelEnvelopes(
701
+ cleanChannelMeta,
702
+ slackMeta,
703
+ providerMeta,
704
+ );
630
705
  const cleanMsg = await createUserMessage(content, attachments);
631
706
  const persisted = await addMessage(
632
707
  conversationId,
@@ -660,9 +735,7 @@ export async function processMessage(
660
735
  const resolvedContent = slashResult.content;
661
736
 
662
737
  const requestId = uuidv7();
663
- const persistMetadata = options?.slackInbound
664
- ? { slackInbound: options.slackInbound }
665
- : undefined;
738
+ const persistMetadata = buildPersistMetadata(options);
666
739
  const ingressKey = deriveIngressIdempotencyKey(options);
667
740
  const { id: messageId, deduplicated } = await conversation.persistUserMessage(
668
741
  {
@@ -743,9 +816,7 @@ export async function processMessageInBackground(
743
816
  const emitEvent = buildEventEmitter(options?.onEvent);
744
817
 
745
818
  const requestId = uuidv7();
746
- const persistMetadata = options?.slackInbound
747
- ? { slackInbound: options.slackInbound }
748
- : undefined;
819
+ const persistMetadata = buildPersistMetadata(options);
749
820
  const ingressKey = deriveIngressIdempotencyKey(options);
750
821
  const { id: messageId, deduplicated } = await conversation.persistUserMessage(
751
822
  {
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { readSlackMetadataFromMessageMetadata } from "../messaging/providers/slack/message-metadata.js";
3
4
  import type { QueuedReactionRecord } from "./reaction-record.js";
4
5
 
5
6
  let persisted: Array<{
@@ -75,24 +76,35 @@ describe("persistReactionRecords", () => {
75
76
  expect(row.metadata?.slackMeta).toBeUndefined();
76
77
  });
77
78
 
78
- test("a Slack record writes the slackMeta envelope the Slack context reads", async () => {
79
+ test("a Slack record writes the neutral envelope, which the Slack context reads through its Slack view", async () => {
79
80
  await persistReactionRecords("conv-1", [
80
81
  record({ channel: "slack", chatId: "C1", messageId: "1700.1" }),
81
82
  ]);
82
83
  const row = persisted[0]!;
83
- expect(row.metadata?.providerMeta).toBeUndefined();
84
- const slackMeta = JSON.parse(String(row.metadata?.slackMeta)) as {
84
+ expect(row.metadata?.slackMeta).toBeUndefined();
85
+ const envelope = JSON.parse(String(row.metadata?.providerMeta)) as {
85
86
  source: string;
86
- channelId: string;
87
- channelTs: string;
87
+ conversationExternalId: string;
88
88
  eventKind: string;
89
- reaction: { emoji: string; targetChannelTs: string; op: string };
89
+ reaction: { targetMessageId: string; emoji: string; op: string };
90
90
  };
91
- expect(slackMeta.source).toBe("slack");
92
- expect(slackMeta.channelId).toBe("C1");
93
- expect(slackMeta.channelTs).toBe("1700.1");
94
- expect(slackMeta.eventKind).toBe("reaction");
95
- expect(slackMeta.reaction).toEqual({
91
+ expect(envelope.source).toBe("slack");
92
+ expect(envelope.conversationExternalId).toBe("C1");
93
+ expect(envelope.eventKind).toBe("reaction");
94
+ expect(envelope.reaction).toEqual({
95
+ targetMessageId: "1700.1",
96
+ emoji: "🎉",
97
+ op: "added",
98
+ });
99
+ // The Slack transcript's view of the same row: the reacted message's ts
100
+ // stands in as `channelTs`, as the Slack envelope stores it.
101
+ const view = readSlackMetadataFromMessageMetadata(
102
+ JSON.stringify(row.metadata),
103
+ );
104
+ expect(view?.channelId).toBe("C1");
105
+ expect(view?.channelTs).toBe("1700.1");
106
+ expect(view?.eventKind).toBe("reaction");
107
+ expect(view?.reaction).toEqual({
96
108
  emoji: "🎉",
97
109
  targetChannelTs: "1700.1",
98
110
  op: "added",
@@ -16,11 +16,7 @@
16
16
  * included.
17
17
  */
18
18
  import type { ChannelId } from "../channels/types.js";
19
- import { writeSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
20
- import {
21
- buildNeutralReactionMeta,
22
- buildSlackReactionMeta,
23
- } from "../messaging/reaction-envelopes.js";
19
+ import { buildNeutralReactionMeta } from "../messaging/reaction-envelopes.js";
24
20
  import {
25
21
  addMessage,
26
22
  REACTION_MESSAGE_KIND,
@@ -58,10 +54,11 @@ export async function persistReactionRecords(
58
54
  emoji: record.emoji,
59
55
  op: record.op,
60
56
  };
61
- const envelope =
62
- record.channel === "slack"
63
- ? { slackMeta: writeSlackMetadata(buildSlackReactionMeta(facts)) }
64
- : { providerMeta: JSON.stringify(buildNeutralReactionMeta(facts)) };
57
+ // The assistant's own rows carry the neutral envelope on every channel;
58
+ // the Slack renderers read it through the envelope's Slack view.
59
+ const envelope = {
60
+ providerMeta: JSON.stringify(buildNeutralReactionMeta(facts)),
61
+ };
65
62
  await addMessage(conversationId, "assistant", "[reaction]", {
66
63
  metadata: {
67
64
  messageKind: REACTION_MESSAGE_KIND,
@@ -470,10 +470,6 @@ export type ReplaceInDocumentResult =
470
470
  | { success: true; replacements_made: number; content_changed: boolean }
471
471
  | { success: false; error: string };
472
472
 
473
- function escapeRegExpChars(str: string): string {
474
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
475
- }
476
-
477
473
  /**
478
474
  * Find and replace text within a document — like sed.
479
475
  * Supports literal text and regex patterns with optional backreferences.
@@ -497,7 +493,7 @@ export function replaceInDocument(
497
493
  const flags = "g" + (options.caseSensitive === true ? "" : "i");
498
494
  const pattern = options.regex
499
495
  ? new RegExp(find, flags)
500
- : new RegExp(escapeRegExpChars(find), flags);
496
+ : new RegExp(RegExp.escape(find), flags);
501
497
 
502
498
  const totalMatches = [...row.content.matchAll(pattern)].length;
503
499
  if (
@@ -79,10 +79,6 @@ function mentionsCurrentUser(
79
79
  context: ConversationStarterValidationContext,
80
80
  ): boolean {
81
81
  return context.userReferences.some((reference) =>
82
- new RegExp(`\\b${escapeRegExp(reference)}\\b`, "i").test(text),
82
+ new RegExp(`\\b${RegExp.escape(reference)}\\b`, "i").test(text),
83
83
  );
84
84
  }
85
-
86
- function escapeRegExp(value: string): string {
87
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
- }