@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
@@ -104,6 +104,58 @@ describe("handleListMessages system-card projection", () => {
104
104
  expect(silentRow?.noResponse).toBe(true);
105
105
  });
106
106
 
107
+ test("projects a reaction's typed emoji fields to the response", async () => {
108
+ const conv = createConversation();
109
+ const row = await addMessage(
110
+ conv.id,
111
+ "user",
112
+ JSON.stringify([{ type: "text", text: "[reaction]" }]),
113
+ {
114
+ metadata: {
115
+ messageKind: "reaction",
116
+ providerMeta: JSON.stringify({
117
+ source: "discord",
118
+ conversationExternalId: "chan-1",
119
+ eventKind: "reaction",
120
+ reaction: {
121
+ targetMessageId: "555.1",
122
+ emoji: "<:party_blob:111>",
123
+ emojiKind: "custom",
124
+ emojiName: "party_blob",
125
+ emojiId: "111",
126
+ emojiAnimated: true,
127
+ op: "added",
128
+ },
129
+ }),
130
+ },
131
+ },
132
+ );
133
+
134
+ const response = (await handleListMessages({
135
+ queryParams: { conversationId: conv.id },
136
+ })) as {
137
+ messages: Array<
138
+ ProjectedMessage & {
139
+ reaction?: {
140
+ emoji: string;
141
+ emojiKind?: string;
142
+ emojiName?: string;
143
+ emojiId?: string;
144
+ emojiAnimated?: boolean;
145
+ };
146
+ }
147
+ >;
148
+ };
149
+ const projected = response.messages.find((m) => m.id === row.id);
150
+ expect(projected?.reaction).toMatchObject({
151
+ emoji: "<:party_blob:111>",
152
+ emojiKind: "custom",
153
+ emojiName: "party_blob",
154
+ emojiId: "111",
155
+ emojiAnimated: true,
156
+ });
157
+ });
158
+
107
159
  test("projects the reaction fact from a reaction row's envelope", async () => {
108
160
  const conv = createConversation();
109
161
  const row = await addMessage(
@@ -144,6 +196,57 @@ describe("handleListMessages system-card projection", () => {
144
196
  expect(projected?.reaction?.selfAuthored).toBe(true);
145
197
  });
146
198
 
199
+ test("projects deletedAt from a row deleted on its channel", async () => {
200
+ // Mirror the delete propagation stamp: the row keeps its content and the
201
+ // envelope carries the marker, in either shape the daemon writes.
202
+ const conv = createConversation();
203
+ const discordRow = await addMessage(
204
+ conv.id,
205
+ "user",
206
+ JSON.stringify([{ type: "text", text: "A message later deleted" }]),
207
+ {
208
+ metadata: {
209
+ providerMeta: JSON.stringify({
210
+ source: "discord",
211
+ conversationExternalId: "chan-1",
212
+ messageId: "111",
213
+ eventKind: "message",
214
+ deletedAt: 1725100000000,
215
+ }),
216
+ },
217
+ },
218
+ );
219
+ const slackRow = await addMessage(
220
+ conv.id,
221
+ "assistant",
222
+ JSON.stringify([{ type: "text", text: "The assistant's deleted post" }]),
223
+ {
224
+ metadata: {
225
+ slackMeta: JSON.stringify({
226
+ source: "slack",
227
+ channelId: "C0123",
228
+ channelTs: "1725100000.000100",
229
+ eventKind: "message",
230
+ deletedAt: 1725100001000,
231
+ }),
232
+ },
233
+ },
234
+ );
235
+
236
+ const response = (await handleListMessages({
237
+ queryParams: { conversationId: conv.id },
238
+ })) as { messages: Array<ProjectedMessage & { deletedAt?: number }> };
239
+ for (const message of response.messages) {
240
+ expect(() => ConversationMessageSchema.parse(message)).not.toThrow();
241
+ }
242
+ expect(
243
+ response.messages.find((m) => m.id === discordRow.id)?.deletedAt,
244
+ ).toBe(1725100000000);
245
+ expect(response.messages.find((m) => m.id === slackRow.id)?.deletedAt).toBe(
246
+ 1725100001000,
247
+ );
248
+ });
249
+
147
250
  test("omits systemCard on ordinary user and assistant rows", async () => {
148
251
  const conv = createConversation();
149
252
  await addMessage(
@@ -414,3 +414,80 @@ describe("unsendable-image notice", () => {
414
414
  );
415
415
  });
416
416
  });
417
+
418
+ describe("resolveMediaReferences file workspace_ref stay-on-disk", () => {
419
+ beforeEach(resetTables);
420
+
421
+ test("does not inline over-cap text or extracted_text", async () => {
422
+ const conv = createConversation();
423
+ const stored = await createInlineAttachment(
424
+ conv.id,
425
+ conv.createdAt,
426
+ "oversized-content.txt",
427
+ "text/plain",
428
+ Buffer.from("hello-over-cap").toString("base64"),
429
+ );
430
+ const messages: Message[] = [
431
+ userMessage([
432
+ {
433
+ type: "file",
434
+ source: {
435
+ type: "workspace_ref",
436
+ media_type: "text/plain",
437
+ attachmentId: stored.id,
438
+ sizeBytes: 8_000_001,
439
+ filename: "oversized-content.txt",
440
+ },
441
+ extracted_text: "this dump must not be resent",
442
+ },
443
+ ]),
444
+ ];
445
+
446
+ const resolved = await resolveMediaReferences(messages);
447
+ const block = resolved[0]!.content[0]!;
448
+ expect(block.type).toBe("file");
449
+ if (block.type !== "file") {
450
+ throw new Error("expected a file block");
451
+ }
452
+ expect(block.source.type).toBe("workspace_ref");
453
+ expect(block.extracted_text).toBeUndefined();
454
+ expect(JSON.stringify(resolved)).not.toContain("this dump must not be resent");
455
+ expect(JSON.stringify(resolved)).not.toContain("hello-over-cap");
456
+ });
457
+
458
+ test("does not inline video bytes or extracted_text", async () => {
459
+ const conv = createConversation();
460
+ const stored = await createInlineAttachment(
461
+ conv.id,
462
+ conv.createdAt,
463
+ "clip.mp4",
464
+ "video/mp4",
465
+ Buffer.from("not-a-real-video").toString("base64"),
466
+ );
467
+ const messages: Message[] = [
468
+ userMessage([
469
+ {
470
+ type: "file",
471
+ source: {
472
+ type: "workspace_ref",
473
+ media_type: "video/mp4",
474
+ attachmentId: stored.id,
475
+ sizeBytes: stored.sizeBytes,
476
+ filename: "clip.mp4",
477
+ },
478
+ extracted_text: "dump of the video as text",
479
+ },
480
+ ]),
481
+ ];
482
+
483
+ const resolved = await resolveMediaReferences(messages);
484
+ const block = resolved[0]!.content[0]!;
485
+ expect(block.type).toBe("file");
486
+ if (block.type !== "file") {
487
+ throw new Error("expected a file block");
488
+ }
489
+ expect(block.source.type).toBe("workspace_ref");
490
+ expect(block.extracted_text).toBeUndefined();
491
+ expect(JSON.stringify(resolved)).not.toContain("dump of the video");
492
+ });
493
+ });
@@ -88,7 +88,7 @@ describe("notification decision fallback copy", () => {
88
88
  );
89
89
  });
90
90
 
91
- test("enforces free-text answer instructions for guardian.question when requestCode exists", async () => {
91
+ test("enforces free-text answer instructions into chat copy and keeps them out of vellum copy", async () => {
92
92
  const signal = makeSignal({
93
93
  contextPayload: {
94
94
  requestId: "req-pending-1",
@@ -101,13 +101,20 @@ describe("notification decision fallback copy", () => {
101
101
  });
102
102
  const decision = await evaluateSignal(signal, [
103
103
  "vellum",
104
+ "telegram",
104
105
  ] as NotificationChannel[]);
105
106
 
106
107
  expect(decision.fallbackUsed).toBe(true);
107
- expect(decision.renderedCopy.vellum?.body).toContain("A1B2C3");
108
- expect(decision.renderedCopy.vellum?.body).toContain("<your answer>");
109
- expect(decision.renderedCopy.vellum?.body).not.toContain("approve");
110
- expect(decision.renderedCopy.vellum?.body).not.toContain("reject");
108
+ expect(decision.renderedCopy.telegram?.body).toContain("A1B2C3");
109
+ expect(decision.renderedCopy.telegram?.body).toContain("<your answer>");
110
+ expect(decision.renderedCopy.telegram?.body).not.toContain("approve");
111
+ expect(decision.renderedCopy.telegram?.body).not.toContain("reject");
112
+ // The bell and the banner read the vellum copy and act through the
113
+ // card, so the question arrives without the chat channel's mechanics.
114
+ expect(decision.renderedCopy.vellum?.body).toBe("What is the gate code?");
115
+ expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
116
+ "What is the gate code?",
117
+ );
111
118
  });
112
119
 
113
120
  test("enforcement appends free-text answer instructions when LLM copy only mentions request code", async () => {
@@ -118,10 +125,10 @@ describe("notification decision fallback copy", () => {
118
125
  name: "record_notification_decision",
119
126
  input: {
120
127
  shouldNotify: true,
121
- selectedChannels: ["vellum"],
128
+ selectedChannels: ["telegram"],
122
129
  reasoningSummary: "LLM decision",
123
130
  renderedCopy: {
124
- vellum: {
131
+ telegram: {
125
132
  title: "Guardian Question",
126
133
  body: "Use reference code A1B2C3 for this request.",
127
134
  },
@@ -143,17 +150,19 @@ describe("notification decision fallback copy", () => {
143
150
  });
144
151
 
145
152
  const decision = await evaluateSignal(signal, [
146
- "vellum",
153
+ "telegram",
147
154
  ] as NotificationChannel[]);
148
155
 
149
156
  expect(decision.fallbackUsed).toBe(false);
150
- expect(decision.renderedCopy.vellum?.body).toContain(
157
+ expect(decision.renderedCopy.telegram?.body).toContain(
151
158
  '"A1B2C3 <your answer>"',
152
159
  );
153
- expect(decision.renderedCopy.vellum?.body).not.toContain(
160
+ expect(decision.renderedCopy.telegram?.body).not.toContain(
154
161
  '"A1B2C3 approve"',
155
162
  );
156
- expect(decision.renderedCopy.vellum?.body).not.toContain('"A1B2C3 reject"');
163
+ expect(decision.renderedCopy.telegram?.body).not.toContain(
164
+ '"A1B2C3 reject"',
165
+ );
157
166
  });
158
167
 
159
168
  test("enforcement appends answer instructions when LLM copy incorrectly uses approve/reject wording", async () => {
@@ -164,10 +173,10 @@ describe("notification decision fallback copy", () => {
164
173
  name: "record_notification_decision",
165
174
  input: {
166
175
  shouldNotify: true,
167
- selectedChannels: ["vellum"],
176
+ selectedChannels: ["telegram"],
168
177
  reasoningSummary: "LLM decision",
169
178
  renderedCopy: {
170
- vellum: {
179
+ telegram: {
171
180
  title: "Guardian Question",
172
181
  body: 'Reference code: A1B2C3. Reply "A1B2C3 approve" or "A1B2C3 reject".',
173
182
  },
@@ -189,11 +198,11 @@ describe("notification decision fallback copy", () => {
189
198
  });
190
199
 
191
200
  const decision = await evaluateSignal(signal, [
192
- "vellum",
201
+ "telegram",
193
202
  ] as NotificationChannel[]);
194
203
 
195
204
  expect(decision.fallbackUsed).toBe(false);
196
- expect(decision.renderedCopy.vellum?.body).toContain(
205
+ expect(decision.renderedCopy.telegram?.body).toContain(
197
206
  '"A1B2C3 <your answer>"',
198
207
  );
199
208
  });
@@ -206,10 +215,10 @@ describe("notification decision fallback copy", () => {
206
215
  name: "record_notification_decision",
207
216
  input: {
208
217
  shouldNotify: true,
209
- selectedChannels: ["vellum"],
218
+ selectedChannels: ["telegram"],
210
219
  reasoningSummary: "LLM decision",
211
220
  renderedCopy: {
212
- vellum: {
221
+ telegram: {
213
222
  title: "Guardian Question",
214
223
  body: "Use reference code A1B2C3 for this request.",
215
224
  },
@@ -230,12 +239,12 @@ describe("notification decision fallback copy", () => {
230
239
  });
231
240
 
232
241
  const decision = await evaluateSignal(signal, [
233
- "vellum",
242
+ "telegram",
234
243
  ] as NotificationChannel[]);
235
244
 
236
245
  expect(decision.fallbackUsed).toBe(false);
237
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 approve"');
238
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
246
+ expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 approve"');
247
+ expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 reject"');
239
248
  });
240
249
 
241
250
  test("approval-mode enforcement removes conflicting answer-mode phrasing", async () => {
@@ -246,10 +255,10 @@ describe("notification decision fallback copy", () => {
246
255
  name: "record_notification_decision",
247
256
  input: {
248
257
  shouldNotify: true,
249
- selectedChannels: ["vellum"],
258
+ selectedChannels: ["telegram"],
250
259
  reasoningSummary: "LLM decision",
251
260
  renderedCopy: {
252
- vellum: {
261
+ telegram: {
253
262
  title: "Guardian Question",
254
263
  body: 'Reference code: A1B2C3. Reply "A1B2C3 <your answer>".',
255
264
  },
@@ -270,13 +279,13 @@ describe("notification decision fallback copy", () => {
270
279
  });
271
280
 
272
281
  const decision = await evaluateSignal(signal, [
273
- "vellum",
282
+ "telegram",
274
283
  ] as NotificationChannel[]);
275
284
 
276
285
  expect(decision.fallbackUsed).toBe(false);
277
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 approve"');
278
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
279
- expect(decision.renderedCopy.vellum?.body).not.toContain("<your answer>");
286
+ expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 approve"');
287
+ expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 reject"');
288
+ expect(decision.renderedCopy.telegram?.body).not.toContain("<your answer>");
280
289
  });
281
290
 
282
291
  test("slack approval copy is stripped of request-code instructions instead of enforced", async () => {
@@ -292,12 +301,18 @@ describe("notification decision fallback copy", () => {
292
301
 
293
302
  const decision = await evaluateSignal(signal, [
294
303
  "vellum",
304
+ "telegram",
295
305
  "slack",
296
306
  ] as NotificationChannel[]);
297
307
 
298
308
  expect(decision.fallbackUsed).toBe(true);
299
- // Vellum keeps the code-reply directive.
300
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 approve"');
309
+ // Telegram answers by typed reply and keeps the code-reply directive.
310
+ expect(decision.renderedCopy.telegram?.body).toContain('"A1B2C3 approve"');
311
+ // Vellum acts through the card: the same text as Slack, code-free.
312
+ expect(decision.renderedCopy.vellum?.body).toBe(
313
+ decision.renderedCopy.slack?.body,
314
+ );
315
+ expect(decision.renderedCopy.vellum?.body).not.toContain("A1B2C3");
301
316
  // Slack renders Approve/Reject buttons — no code anywhere in its copy.
302
317
  expect(decision.renderedCopy.slack?.body).toBe(
303
318
  "Approve tool: bash — ls /tmp (requested by Alice)",
@@ -374,6 +389,110 @@ describe("notification decision fallback copy", () => {
374
389
  });
375
390
  });
376
391
 
392
+ // ── Surfaces that act through the card ───────────────────────────────────
393
+
394
+ /**
395
+ * The vellum copy is read by the bell detail, the OS banner, and the
396
+ * delivery audit row, and the platform push mirrors it. None of them takes
397
+ * a typed reply, so the chat channels' "Reference code / Reply CODE" copy
398
+ * is noise there and is kept out on every decision path.
399
+ */
400
+ describe("guardian copy for surfaces that act through the card", () => {
401
+ beforeEach(() => {
402
+ configuredProvider = null;
403
+ extractedToolUse = null;
404
+ });
405
+
406
+ test("a question with options reaches the bell as the question and its options, nothing else", async () => {
407
+ const signal = makeSignal({
408
+ sourceChannel: "telegram",
409
+ contextPayload: {
410
+ requestId: "req-ask-bell-1",
411
+ requestCode: "E96831",
412
+ requestKind: "pending_question",
413
+ questionText:
414
+ "When you say it feels too harsh, what is the feeling exactly?",
415
+ options: [
416
+ { id: "heavy", label: "Lines too heavy" },
417
+ { id: "showy", label: "Feels showy" },
418
+ { id: "both", label: "Both, honestly" },
419
+ ],
420
+ },
421
+ });
422
+ const decision = await evaluateSignal(signal, [
423
+ "vellum",
424
+ "telegram",
425
+ ] as NotificationChannel[]);
426
+
427
+ const expectedQuestion =
428
+ "When you say it feels too harsh, what is the feeling exactly?\n\n1. Lines too heavy\n2. Feels showy\n3. Both, honestly";
429
+ // The bell reads the seed; the banner reads the body. Line structure is
430
+ // preserved so the options render as lines rather than one run-on blob.
431
+ expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
432
+ expectedQuestion,
433
+ );
434
+ expect(decision.renderedCopy.vellum?.body).toBe(expectedQuestion);
435
+ expect(decision.renderedCopy.vellum?.deliveryText).toBe(expectedQuestion);
436
+ // The chat channel still carries the answer mechanics after the options.
437
+ expect(decision.renderedCopy.telegram?.deliveryText).toBe(
438
+ `${expectedQuestion}\n\nReference code: E96831. Reply "E96831 <your answer>".`,
439
+ );
440
+ });
441
+
442
+ test("model-authored code phrasing is stripped from vellum and platform copy", async () => {
443
+ configuredProvider = {
444
+ sendMessage: async () => ({ content: [] }),
445
+ };
446
+ extractedToolUse = {
447
+ name: "record_notification_decision",
448
+ input: {
449
+ shouldNotify: true,
450
+ selectedChannels: ["vellum", "platform"],
451
+ reasoningSummary: "LLM decision",
452
+ renderedCopy: {
453
+ vellum: {
454
+ title: "Tool Grant Request",
455
+ body: 'Alice is asking to run ls /tmp.\nApproval code: A1B2C3\nReply "A1B2C3 approve" or "A1B2C3 reject".',
456
+ conversationSeedMessage:
457
+ "**Alice** is asking to run `ls /tmp`.\n\nReference code: A1B2C3.",
458
+ },
459
+ platform: {
460
+ title: "Tool Grant Request",
461
+ body: 'Alice is asking to run ls /tmp. Reply "A1B2C3 approve" or "A1B2C3 reject".',
462
+ },
463
+ },
464
+ dedupeKey: "guardian-question-vellum-llm-test",
465
+ confidence: 0.9,
466
+ },
467
+ };
468
+
469
+ const signal = makeSignal({
470
+ contextPayload: {
471
+ requestId: "req-grant-vellum-1",
472
+ questionText: "Approve tool: bash (requested by Alice)",
473
+ requestCode: "A1B2C3",
474
+ requestKind: "tool_grant_request",
475
+ toolName: "bash",
476
+ },
477
+ });
478
+ const decision = await evaluateSignal(signal, [
479
+ "vellum",
480
+ "platform",
481
+ ] as NotificationChannel[]);
482
+
483
+ expect(decision.fallbackUsed).toBe(false);
484
+ expect(decision.renderedCopy.vellum?.body).toBe(
485
+ "Alice is asking to run ls /tmp.",
486
+ );
487
+ expect(decision.renderedCopy.vellum?.conversationSeedMessage).toBe(
488
+ "**Alice** is asking to run `ls /tmp`.",
489
+ );
490
+ expect(decision.renderedCopy.platform?.body).toBe(
491
+ "Alice is asking to run ls /tmp.",
492
+ );
493
+ });
494
+ });
495
+
377
496
  // ── Access-request instruction enforcement ──────────────────────────────
378
497
 
379
498
  describe("access-request instruction enforcement", () => {
@@ -406,22 +525,24 @@ describe("access-request instruction enforcement", () => {
406
525
  };
407
526
  }
408
527
 
409
- test("fallback copy includes access-request contract elements", async () => {
528
+ test("fallback copy carries the requester context and no reply mechanics", async () => {
410
529
  const signal = makeAccessRequestSignal();
411
530
  const decision = await evaluateSignal(signal, [
412
531
  "vellum",
532
+ "telegram",
413
533
  ] as NotificationChannel[]);
414
534
 
415
535
  expect(decision.fallbackUsed).toBe(true);
416
- // Directive form, not prose mentions: the guardian must be able to reply
417
- // with these exact code+verb tokens.
418
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 trust"');
419
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
420
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 block"');
421
- expect(decision.renderedCopy.vellum?.body).toContain("open invite flow");
536
+ for (const channel of ["vellum", "telegram"] as const) {
537
+ const body = decision.renderedCopy[channel]?.body ?? "";
538
+ expect(body).toContain("Alice");
539
+ expect(body).not.toContain("A1B2C3");
540
+ // Context, not mechanics: no surface offers an invite button.
541
+ expect(body).toContain('Reply "open invite flow"');
542
+ }
422
543
  });
423
544
 
424
- test("enforcement appends contract when LLM copy is missing request code", async () => {
545
+ test("model copy with no mechanics keeps its text and gains the invite sentence", async () => {
425
546
  configuredProvider = {
426
547
  sendMessage: async () => ({ content: [] }),
427
548
  };
@@ -448,13 +569,12 @@ describe("access-request instruction enforcement", () => {
448
569
  ] as NotificationChannel[]);
449
570
 
450
571
  expect(decision.fallbackUsed).toBe(false);
451
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 trust"');
452
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 reject"');
453
- expect(decision.renderedCopy.vellum?.body).toContain('"A1B2C3 block"');
454
- expect(decision.renderedCopy.vellum?.body).toContain("open invite flow");
572
+ expect(decision.renderedCopy.vellum?.body).toBe(
573
+ 'Someone wants access to your assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
574
+ );
455
575
  });
456
576
 
457
- test("enforcement appends contract when LLM copy has code but missing invite flow", async () => {
577
+ test("model-authored code directives are stripped, leaving the ask", async () => {
458
578
  configuredProvider = {
459
579
  sendMessage: async () => ({ content: [] }),
460
580
  };
@@ -481,10 +601,12 @@ describe("access-request instruction enforcement", () => {
481
601
  ] as NotificationChannel[]);
482
602
 
483
603
  expect(decision.fallbackUsed).toBe(false);
484
- expect(decision.renderedCopy.vellum?.body).toContain("open invite flow");
604
+ expect(decision.renderedCopy.vellum?.body).toBe(
605
+ 'Alice wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.',
606
+ );
485
607
  });
486
608
 
487
- test("enforcement does not duplicate when LLM copy already has all required elements", async () => {
609
+ test("the code directive is stripped from model copy and the invite directive stays", async () => {
488
610
  const fullBody =
489
611
  'Alice wants access.\nReply "A1B2C3 verify" to send them a verification code, "A1B2C3 trust" to trust them without one, "A1B2C3 reject" to leave them unverified, or "A1B2C3 block" to block them.\nReply "open invite flow" to start Trusted Contacts invite flow.';
490
612
  configuredProvider = {
@@ -513,11 +635,14 @@ describe("access-request instruction enforcement", () => {
513
635
  ] as NotificationChannel[]);
514
636
 
515
637
  expect(decision.fallbackUsed).toBe(false);
516
- // Body should remain unchanged when all required elements are present
517
- expect(decision.renderedCopy.vellum?.body).toBe(fullBody);
638
+ expect(decision.renderedCopy.vellum?.body).toBe(
639
+ 'Alice wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.',
640
+ );
518
641
  });
519
642
 
520
- test("enforcement also applies to deliveryText and conversationSeedMessage", async () => {
643
+ test("the strip covers deliveryText and conversationSeedMessage too", async () => {
644
+ const withMechanics =
645
+ 'Someone wants access.\nReply "A1B2C3 trust" to trust them, "A1B2C3 reject" to leave them unverified, or "A1B2C3 block" to block them.';
521
646
  configuredProvider = {
522
647
  sendMessage: async () => ({ content: [] }),
523
648
  };
@@ -530,9 +655,9 @@ describe("access-request instruction enforcement", () => {
530
655
  renderedCopy: {
531
656
  telegram: {
532
657
  title: "Access Request",
533
- body: "Someone wants access.",
534
- deliveryText: "Someone wants access.",
535
- conversationSeedMessage: "Someone wants access.",
658
+ body: withMechanics,
659
+ deliveryText: withMechanics,
660
+ conversationSeedMessage: withMechanics,
536
661
  },
537
662
  },
538
663
  dedupeKey: "access-req-multi-field",
@@ -545,19 +670,16 @@ describe("access-request instruction enforcement", () => {
545
670
  "telegram",
546
671
  ] as NotificationChannel[]);
547
672
 
548
- expect(decision.renderedCopy.telegram?.deliveryText).toContain("A1B2C3");
549
- expect(decision.renderedCopy.telegram?.deliveryText).toContain(
550
- "open invite flow",
551
- );
552
- expect(decision.renderedCopy.telegram?.conversationSeedMessage).toContain(
553
- "A1B2C3",
554
- );
555
- expect(decision.renderedCopy.telegram?.conversationSeedMessage).toContain(
556
- "open invite flow",
673
+ const expected =
674
+ 'Someone wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.';
675
+ expect(decision.renderedCopy.telegram?.body).toBe(expected);
676
+ expect(decision.renderedCopy.telegram?.deliveryText).toBe(expected);
677
+ expect(decision.renderedCopy.telegram?.conversationSeedMessage).toBe(
678
+ expected,
557
679
  );
558
680
  });
559
681
 
560
- test("enforcement appends contract when LLM copy contains conflicting instructions", async () => {
682
+ test("model copy that leaves the invite sentence out gets it appended", async () => {
561
683
  configuredProvider = {
562
684
  sendMessage: async () => ({ content: [] }),
563
685
  };
@@ -565,31 +687,33 @@ describe("access-request instruction enforcement", () => {
565
687
  name: "record_notification_decision",
566
688
  input: {
567
689
  shouldNotify: true,
568
- selectedChannels: ["vellum"],
690
+ selectedChannels: ["telegram"],
569
691
  reasoningSummary: "LLM decision",
570
692
  renderedCopy: {
571
- vellum: {
693
+ telegram: {
572
694
  title: "Access Request",
573
- body: 'Alice wants access. Just reply "yes" or "no" to decide.',
695
+ body: "Alice wants access.",
696
+ deliveryText: "Alice wants access.",
574
697
  },
575
698
  },
576
- dedupeKey: "access-req-conflicting",
699
+ dedupeKey: "access-req-invite-ensured",
577
700
  confidence: 0.9,
578
701
  },
579
702
  };
580
703
 
581
- const signal = makeAccessRequestSignal();
582
- const decision = await evaluateSignal(signal, [
583
- "vellum",
704
+ const decision = await evaluateSignal(makeAccessRequestSignal(), [
705
+ "telegram",
584
706
  ] as NotificationChannel[]);
585
707
 
586
- // Must contain the proper contract instructions despite conflicting LLM copy
587
- expect(decision.renderedCopy.vellum?.body).toContain("A1B2C3 verify");
588
- expect(decision.renderedCopy.vellum?.body).toContain("A1B2C3 reject");
589
- expect(decision.renderedCopy.vellum?.body).toContain("open invite flow");
708
+ expect(decision.fallbackUsed).toBe(false);
709
+ const expected =
710
+ 'Alice wants access.\nReply "open invite flow" to start Trusted Contacts invite flow.';
711
+ expect(decision.renderedCopy.telegram?.body).toBe(expected);
712
+ expect(decision.renderedCopy.telegram?.deliveryText).toBe(expected);
713
+ expect(decision.renderedCopy.telegram?.title).toBe("Access Request");
590
714
  });
591
715
 
592
- test("enforcement appends invite directive when requestCode is absent", async () => {
716
+ test("the invite directive stays even when the request has no code", async () => {
593
717
  configuredProvider = {
594
718
  sendMessage: async () => ({ content: [] }),
595
719
  };
@@ -602,7 +726,7 @@ describe("access-request instruction enforcement", () => {
602
726
  renderedCopy: {
603
727
  vellum: {
604
728
  title: "Access Request",
605
- body: "Someone wants access to your assistant.",
729
+ body: 'Someone wants access to your assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
606
730
  },
607
731
  },
608
732
  dedupeKey: "access-req-no-code-invite",
@@ -622,11 +746,9 @@ describe("access-request instruction enforcement", () => {
622
746
  ] as NotificationChannel[]);
623
747
 
624
748
  expect(decision.fallbackUsed).toBe(false);
625
- // Invite directive should still be enforced even without requestCode
626
- expect(decision.renderedCopy.vellum?.body).toContain("open invite flow");
627
- // Approve/reject should NOT be present since there is no requestCode
628
- expect(decision.renderedCopy.vellum?.body).not.toContain("approve");
629
- expect(decision.renderedCopy.vellum?.body).not.toContain("reject");
749
+ expect(decision.renderedCopy.vellum?.body).toBe(
750
+ 'Someone wants access to your assistant.\nReply "open invite flow" to start Trusted Contacts invite flow.',
751
+ );
630
752
  });
631
753
  });
632
754