@vellumai/assistant 0.11.4 → 0.11.5-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 (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -2,7 +2,10 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { FeedItem } from "../../home/feed-types.js";
4
4
  import type { NotificationSignal } from "../signal.js";
5
- import type { NotificationDecision } from "../types.js";
5
+ import type {
6
+ NotificationDecision,
7
+ NotificationDeliveryResult,
8
+ } from "../types.js";
6
9
 
7
10
  // ── Module mocks ───────────────────────────────────────────────────────
8
11
  //
@@ -13,8 +16,40 @@ import type { NotificationDecision } from "../types.js";
13
16
 
14
17
  const appendCalls: FeedItem[] = [];
15
18
  const conversationLookups: string[] = [];
19
+ const messageAppends: Array<{
20
+ conversationId: string;
21
+ role: string;
22
+ content: string;
23
+ options?: { skipIndexing?: boolean };
24
+ }> = [];
25
+ const messageRewrites: Array<{ messageId: string; content: string }> = [];
26
+ /** messageId -> the conversation it belongs to, for the scoped lookup. */
27
+ const messageOwners = new Map<string, string>();
16
28
  let conversationRow: { conversationType: string } | null = null;
17
29
  let conversationLookupShouldThrow = false;
30
+ let messageAppendShouldThrow = false;
31
+ let messageRewriteShouldThrow = false;
32
+ let messageLookupShouldThrow = false;
33
+
34
+ let feedItemSchemaShouldReject = false;
35
+ const messagesInvalidated: string[] = [];
36
+
37
+ mock.module("../../runtime/sync/resource-sync-events.js", () => ({
38
+ publishConversationMessagesChanged: (conversationId: string) => {
39
+ messagesInvalidated.push(conversationId);
40
+ },
41
+ }));
42
+
43
+ mock.module("../../home/feed-types.js", () => ({
44
+ feedItemSchema: {
45
+ parse: (item: unknown) => {
46
+ if (feedItemSchemaShouldReject) {
47
+ throw new Error("simulated schema rejection");
48
+ }
49
+ return item;
50
+ },
51
+ },
52
+ }));
18
53
 
19
54
  mock.module("../../home/feed-writer.js", () => ({
20
55
  appendFeedItem: async (item: FeedItem) => {
@@ -30,10 +65,38 @@ mock.module("../../persistence/conversation-crud.js", () => ({
30
65
  }
31
66
  return conversationRow;
32
67
  },
68
+ addMessage: async (
69
+ conversationId: string,
70
+ role: string,
71
+ content: string,
72
+ options?: { skipIndexing?: boolean },
73
+ ) => {
74
+ if (messageAppendShouldThrow) {
75
+ throw new Error("simulated message write failure");
76
+ }
77
+ messageAppends.push({ conversationId, role, content, options });
78
+ return { id: `msg-${messageAppends.length}` };
79
+ },
80
+ getMessageById: (messageId: string, conversationId?: string) => {
81
+ if (messageLookupShouldThrow) {
82
+ throw new Error("simulated message lookup failure");
83
+ }
84
+ const owner = messageOwners.get(messageId);
85
+ if (!owner || (conversationId && owner !== conversationId)) {
86
+ return null;
87
+ }
88
+ return { id: messageId, conversationId: owner };
89
+ },
90
+ updateMessageContent: (messageId: string, content: string) => {
91
+ if (messageRewriteShouldThrow) {
92
+ throw new Error("simulated message rewrite failure");
93
+ }
94
+ messageRewrites.push({ messageId, content });
95
+ },
33
96
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
34
97
  }));
35
98
 
36
- const { writeHomeFeedItemForSignal } =
99
+ const { updateFeedItemConversationMessage, writeHomeFeedItemForSignal } =
37
100
  await import("../home-feed-side-effect.js");
38
101
 
39
102
  // ── Test fixtures ──────────────────────────────────────────────────────
@@ -58,6 +121,19 @@ function makeSignal(
58
121
  };
59
122
  }
60
123
 
124
+ function makeVellumDelivery(
125
+ overrides: Partial<NotificationDeliveryResult> = {},
126
+ ): NotificationDeliveryResult {
127
+ return {
128
+ channel: "vellum",
129
+ destination: "vellum",
130
+ status: "sent",
131
+ conversationId: "conv-source-1",
132
+ messageId: "msg-paired",
133
+ ...overrides,
134
+ };
135
+ }
136
+
61
137
  function makeDecision(
62
138
  overrides: Partial<NotificationDecision> = {},
63
139
  ): NotificationDecision {
@@ -76,8 +152,16 @@ function makeDecision(
76
152
  beforeEach(() => {
77
153
  appendCalls.length = 0;
78
154
  conversationLookups.length = 0;
155
+ messageAppends.length = 0;
156
+ messageRewrites.length = 0;
157
+ messageOwners.clear();
79
158
  conversationRow = null;
80
159
  conversationLookupShouldThrow = false;
160
+ messageAppendShouldThrow = false;
161
+ messageRewriteShouldThrow = false;
162
+ messageLookupShouldThrow = false;
163
+ feedItemSchemaShouldReject = false;
164
+ messagesInvalidated.length = 0;
81
165
  });
82
166
 
83
167
  describe("writeHomeFeedItemForSignal", () => {
@@ -261,7 +345,7 @@ describe("writeHomeFeedItemForSignal", () => {
261
345
  const item = await writeHomeFeedItemForSignal(
262
346
  signal,
263
347
  decision,
264
- "paired-delivery-conv-id",
348
+ makeVellumDelivery({ conversationId: "paired-delivery-conv-id" }),
265
349
  );
266
350
 
267
351
  expect(item).not.toBeNull();
@@ -287,7 +371,7 @@ describe("writeHomeFeedItemForSignal", () => {
287
371
  const item = await writeHomeFeedItemForSignal(
288
372
  signal,
289
373
  decision,
290
- "paired-delivery-conv-id",
374
+ makeVellumDelivery({ conversationId: "paired-delivery-conv-id" }),
291
375
  );
292
376
 
293
377
  expect(item).not.toBeNull();
@@ -840,4 +924,452 @@ describe("writeHomeFeedItemForSignal", () => {
840
924
  expect(item?.noteworthy).toBe(true);
841
925
  expect(appendCalls[0]!.noteworthy).toBe(true);
842
926
  });
927
+
928
+ describe("body append into the feed item's conversation", () => {
929
+ test("appends the card summary when no vellum conversation was paired", async () => {
930
+ // The Slack/Telegram-only routing case: nothing paired a vellum
931
+ // conversation, so this seam is the only writer that can put the body
932
+ // where the card's button points.
933
+ conversationRow = { conversationType: "background" };
934
+ const signal = makeSignal({
935
+ contextPayload: { title: "Nightly briefing" },
936
+ });
937
+ const decision = makeDecision({
938
+ selectedChannels: ["telegram"],
939
+ renderedCopy: {
940
+ telegram: { title: "Nightly briefing", body: "Three things today." },
941
+ },
942
+ });
943
+
944
+ const item = await writeHomeFeedItemForSignal(signal, decision);
945
+
946
+ expect(item?.conversationId).toBe("conv-source-1");
947
+ expect(messageAppends).toHaveLength(1);
948
+ expect(messageAppends[0]).toMatchObject({
949
+ conversationId: "conv-source-1",
950
+ role: "assistant",
951
+ content: "Three things today.",
952
+ options: { skipIndexing: true },
953
+ });
954
+ expect(item?.metadata?.notificationConversationMessageId).toBe("msg-1");
955
+ // A client with the conversation open refetches only on the messages
956
+ // tag; `addMessage` publishes the metadata tag alone.
957
+ expect(messagesInvalidated).toEqual(["conv-source-1"]);
958
+ });
959
+
960
+ test("writes exactly the summary the card renders", async () => {
961
+ // The button's promise is that the conversation holds what the card
962
+ // shows, so the two read from the same resolved text.
963
+ conversationRow = { conversationType: "background" };
964
+ const signal = makeSignal();
965
+ const decision = makeDecision({
966
+ selectedChannels: ["slack"],
967
+ renderedCopy: {
968
+ slack: {
969
+ title: "Digest",
970
+ body: "Short popup line.",
971
+ conversationSeedMessage:
972
+ "**Digest**\n\n- First item\n- Second item",
973
+ },
974
+ },
975
+ });
976
+
977
+ const item = await writeHomeFeedItemForSignal(signal, decision);
978
+
979
+ expect(messageAppends[0]!.content).toBe(item!.summary);
980
+ expect(messageAppends[0]!.content).toBe(
981
+ "**Digest**\n\n- First item\n- Second item",
982
+ );
983
+ });
984
+
985
+ test("records the paired row when vellum already wrote the body", async () => {
986
+ // `pairDeliveryWithConversation` wrote the body, so nothing is written
987
+ // again here, but the card still records the row so an edit can reach it
988
+ // when the delivery walk does not.
989
+ conversationRow = { conversationType: "background" };
990
+ const signal = makeSignal();
991
+ const decision = makeDecision({
992
+ selectedChannels: ["vellum"],
993
+ renderedCopy: {
994
+ vellum: { title: "Nightly briefing", body: "Three things today." },
995
+ },
996
+ });
997
+
998
+ const item = await writeHomeFeedItemForSignal(
999
+ signal,
1000
+ decision,
1001
+ makeVellumDelivery(),
1002
+ );
1003
+
1004
+ expect(item?.conversationId).toBe("conv-source-1");
1005
+ expect(messageAppends).toHaveLength(0);
1006
+ expect(item?.metadata?.notificationConversationMessageId).toBe(
1007
+ "msg-paired",
1008
+ );
1009
+ });
1010
+
1011
+ test("skips the append when a sentinel source context leaves no target", async () => {
1012
+ // No resolvable conversation means the client hides the button, so
1013
+ // there is nothing to keep honest.
1014
+ conversationRow = null;
1015
+ const signal = makeSignal({
1016
+ sourceContextId: "job-1700000000",
1017
+ attentionHints: {
1018
+ requiresAction: false,
1019
+ urgency: "medium",
1020
+ isAsyncBackground: true,
1021
+ visibleInSourceNow: false,
1022
+ },
1023
+ });
1024
+ const decision = makeDecision({
1025
+ selectedChannels: ["telegram"],
1026
+ renderedCopy: {
1027
+ telegram: { title: "Retries exhausted", body: "Gave up after 3." },
1028
+ },
1029
+ });
1030
+
1031
+ const item = await writeHomeFeedItemForSignal(signal, decision);
1032
+
1033
+ expect(item).not.toBeNull();
1034
+ expect(item?.conversationId).toBeUndefined();
1035
+ expect(messageAppends).toHaveLength(0);
1036
+ });
1037
+
1038
+ test("writes no message when the card is rejected", async () => {
1039
+ // The body only belongs in a conversation because a card sends the user
1040
+ // there, so a rejected card must not leave one behind.
1041
+ conversationRow = { conversationType: "background" };
1042
+ feedItemSchemaShouldReject = true;
1043
+ const signal = makeSignal();
1044
+ const decision = makeDecision({
1045
+ selectedChannels: ["telegram"],
1046
+ renderedCopy: {
1047
+ telegram: { title: "Nightly briefing", body: "Three things today." },
1048
+ },
1049
+ });
1050
+
1051
+ const item = await writeHomeFeedItemForSignal(signal, decision);
1052
+
1053
+ expect(item).toBeNull();
1054
+ expect(appendCalls).toHaveLength(0);
1055
+ expect(messageAppends).toHaveLength(0);
1056
+ expect(messagesInvalidated).toHaveLength(0);
1057
+ });
1058
+
1059
+ test("a failed message write leaves the persisted feed item intact", async () => {
1060
+ conversationRow = { conversationType: "background" };
1061
+ messageAppendShouldThrow = true;
1062
+ const signal = makeSignal();
1063
+ const decision = makeDecision({
1064
+ selectedChannels: ["telegram"],
1065
+ renderedCopy: {
1066
+ telegram: { title: "Nightly briefing", body: "Three things today." },
1067
+ },
1068
+ });
1069
+
1070
+ const item = await writeHomeFeedItemForSignal(signal, decision);
1071
+
1072
+ expect(item).not.toBeNull();
1073
+ expect(appendCalls).toHaveLength(1);
1074
+ expect(messageAppends).toHaveLength(0);
1075
+ expect(item?.metadata?.notificationConversationMessageId).toBeUndefined();
1076
+ });
1077
+
1078
+ test("records the paired row when the vellum delivery failed", async () => {
1079
+ // The row exists whatever the delivery reported, and a failed delivery
1080
+ // is exactly the case the edit path's delivery walk skips.
1081
+ conversationRow = { conversationType: "background" };
1082
+ const signal = makeSignal();
1083
+ const decision = makeDecision({
1084
+ selectedChannels: ["vellum"],
1085
+ renderedCopy: {
1086
+ vellum: { title: "Nightly briefing", body: "Three things today." },
1087
+ },
1088
+ });
1089
+
1090
+ const item = await writeHomeFeedItemForSignal(
1091
+ signal,
1092
+ decision,
1093
+ makeVellumDelivery({ status: "failed", messageId: "msg-paired" }),
1094
+ );
1095
+
1096
+ expect(item?.metadata?.notificationConversationMessageId).toBe(
1097
+ "msg-paired",
1098
+ );
1099
+ // The row already exists, so nothing is written a second time.
1100
+ expect(messageAppends).toHaveLength(0);
1101
+ });
1102
+
1103
+ test("records nothing when the paired row sits outside the card's conversation", async () => {
1104
+ // Guardian producers pair a fresh conversation rather than the one the
1105
+ // card opens, so its row is not the card's to rewrite.
1106
+ conversationRow = { conversationType: "background" };
1107
+ const signal = makeSignal();
1108
+ const decision = makeDecision({
1109
+ selectedChannels: ["vellum"],
1110
+ renderedCopy: {
1111
+ vellum: { title: "Approval needed", body: "Allow this?" },
1112
+ },
1113
+ });
1114
+
1115
+ const item = await writeHomeFeedItemForSignal(
1116
+ signal,
1117
+ decision,
1118
+ makeVellumDelivery({
1119
+ conversationId: "conv-guardian-request",
1120
+ messageId: "msg-guardian",
1121
+ }),
1122
+ );
1123
+
1124
+ expect(item?.conversationId).toBe("conv-source-1");
1125
+ expect(item?.metadata?.notificationConversationMessageId).toBeUndefined();
1126
+ expect(messageAppends).toHaveLength(0);
1127
+ });
1128
+
1129
+ test("records the paired row when it backs a sentinel card", async () => {
1130
+ // A sentinel `sourceContextId` puts the paired conversation behind the
1131
+ // button, so its row is the one the card carries.
1132
+ conversationRow = null;
1133
+ const signal = makeSignal({
1134
+ sourceContextId: "job-1700000000",
1135
+ attentionHints: {
1136
+ requiresAction: false,
1137
+ urgency: "medium",
1138
+ isAsyncBackground: true,
1139
+ visibleInSourceNow: false,
1140
+ },
1141
+ });
1142
+ const decision = makeDecision({
1143
+ selectedChannels: ["vellum"],
1144
+ renderedCopy: {
1145
+ vellum: { title: "Retries exhausted", body: "Gave up after 3." },
1146
+ },
1147
+ });
1148
+
1149
+ const item = await writeHomeFeedItemForSignal(
1150
+ signal,
1151
+ decision,
1152
+ makeVellumDelivery({ conversationId: "conv-paired", messageId: "m-p" }),
1153
+ );
1154
+
1155
+ expect(item?.conversationId).toBe("conv-paired");
1156
+ expect(item?.metadata?.notificationConversationMessageId).toBe("m-p");
1157
+ });
1158
+
1159
+ test("strips a producer-supplied handle from the card metadata", async () => {
1160
+ // `contextPayload` reaches the card verbatim, and this key addresses a
1161
+ // row for rewriting, so a producer must not be able to set it.
1162
+ conversationRow = { conversationType: "background" };
1163
+ const signal = makeSignal({
1164
+ contextPayload: {
1165
+ title: "Nightly briefing",
1166
+ notificationConversationMessageId: "msg-somebody-elses",
1167
+ },
1168
+ });
1169
+ const decision = makeDecision({
1170
+ selectedChannels: ["vellum"],
1171
+ renderedCopy: {
1172
+ vellum: { title: "Nightly briefing", body: "Three things today." },
1173
+ },
1174
+ });
1175
+
1176
+ const item = await writeHomeFeedItemForSignal(
1177
+ signal,
1178
+ decision,
1179
+ makeVellumDelivery(),
1180
+ );
1181
+
1182
+ expect(item?.metadata?.notificationConversationMessageId).toBe(
1183
+ "msg-paired",
1184
+ );
1185
+ });
1186
+
1187
+ test("drops a producer-supplied handle when the card records none", async () => {
1188
+ conversationRow = { conversationType: "background" };
1189
+ const signal = makeSignal({
1190
+ contextPayload: {
1191
+ title: "Approval needed",
1192
+ notificationConversationMessageId: "msg-somebody-elses",
1193
+ },
1194
+ });
1195
+ const decision = makeDecision({
1196
+ selectedChannels: ["vellum"],
1197
+ renderedCopy: {
1198
+ vellum: { title: "Approval needed", body: "Allow this?" },
1199
+ },
1200
+ });
1201
+
1202
+ const item = await writeHomeFeedItemForSignal(
1203
+ signal,
1204
+ decision,
1205
+ makeVellumDelivery({ conversationId: "conv-guardian-request" }),
1206
+ );
1207
+
1208
+ expect(item?.metadata?.notificationConversationMessageId).toBeUndefined();
1209
+ });
1210
+
1211
+ test("a producer-supplied handle never displaces the real one", async () => {
1212
+ conversationRow = { conversationType: "background" };
1213
+ const signal = makeSignal({
1214
+ contextPayload: {
1215
+ title: "Nightly briefing",
1216
+ notificationConversationMessageId: "msg-somebody-elses",
1217
+ },
1218
+ });
1219
+ const decision = makeDecision({
1220
+ selectedChannels: ["telegram"],
1221
+ renderedCopy: {
1222
+ telegram: { title: "Nightly briefing", body: "Three things today." },
1223
+ },
1224
+ });
1225
+
1226
+ const item = await writeHomeFeedItemForSignal(signal, decision);
1227
+
1228
+ expect(item?.metadata?.notificationConversationMessageId).toBe("msg-1");
1229
+ });
1230
+
1231
+ test("preserves producer metadata alongside the message handle", async () => {
1232
+ conversationRow = { conversationType: "background" };
1233
+ const signal = makeSignal({
1234
+ contextPayload: { title: "Nightly briefing", scheduleId: "sched-7" },
1235
+ });
1236
+ const decision = makeDecision({
1237
+ selectedChannels: ["telegram"],
1238
+ renderedCopy: {
1239
+ telegram: { title: "Nightly briefing", body: "Three things today." },
1240
+ },
1241
+ });
1242
+
1243
+ const item = await writeHomeFeedItemForSignal(signal, decision);
1244
+
1245
+ expect(item?.metadata).toMatchObject({
1246
+ title: "Nightly briefing",
1247
+ scheduleId: "sched-7",
1248
+ notificationConversationMessageId: "msg-1",
1249
+ });
1250
+ });
1251
+ });
1252
+
1253
+ describe("updateFeedItemConversationMessage", () => {
1254
+ function makeItem(metadata?: Record<string, unknown>): FeedItem {
1255
+ return {
1256
+ id: "notif:sig-test-1",
1257
+ type: "notification",
1258
+ priority: 50,
1259
+ title: "Nightly briefing",
1260
+ summary: "Three things today.",
1261
+ timestamp: "2026-08-19T00:00:00.000Z",
1262
+ createdAt: "2026-08-19T00:00:00.000Z",
1263
+ status: "new",
1264
+ category: "scheduling",
1265
+ noteworthy: false,
1266
+ fromAssistant: false,
1267
+ conversationId: "conv-source-1",
1268
+ ...(metadata ? { metadata } : {}),
1269
+ };
1270
+ }
1271
+
1272
+ test("rewrites the message the card owns", () => {
1273
+ messageOwners.set("msg-9", "conv-source-1");
1274
+
1275
+ const rewritten = updateFeedItemConversationMessage(
1276
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1277
+ "Four things now.",
1278
+ );
1279
+
1280
+ expect(rewritten).toBe(true);
1281
+ expect(messageRewrites).toEqual([
1282
+ { messageId: "msg-9", content: "Four things now." },
1283
+ ]);
1284
+ expect(messagesInvalidated).toEqual(["conv-source-1"]);
1285
+ });
1286
+
1287
+ test("reports no rewrite when the card owns no message", () => {
1288
+ // The vellum-delivered case: the delivery row carries the handle and
1289
+ // the adapter rewrites it, so there is nothing to do here.
1290
+ const rewritten = updateFeedItemConversationMessage(
1291
+ makeItem({ scheduleId: "sched-7" }),
1292
+ "Four things now.",
1293
+ );
1294
+
1295
+ expect(rewritten).toBe(false);
1296
+ expect(messageRewrites).toHaveLength(0);
1297
+ });
1298
+
1299
+ test("refuses a handle addressing a row outside the card's conversation", () => {
1300
+ // Nothing should be able to point the rewrite at an unrelated message.
1301
+ messageOwners.set("msg-9", "conv-somebody-elses");
1302
+
1303
+ const rewritten = updateFeedItemConversationMessage(
1304
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1305
+ "Four things now.",
1306
+ );
1307
+
1308
+ expect(rewritten).toBe(false);
1309
+ expect(messageRewrites).toHaveLength(0);
1310
+ });
1311
+
1312
+ test("refuses a handle whose row no longer exists", () => {
1313
+ const rewritten = updateFeedItemConversationMessage(
1314
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1315
+ "Four things now.",
1316
+ );
1317
+
1318
+ expect(rewritten).toBe(false);
1319
+ expect(messageRewrites).toHaveLength(0);
1320
+ });
1321
+
1322
+ test("stands down when the delivery walk already rewrote the row", () => {
1323
+ messageOwners.set("msg-9", "conv-source-1");
1324
+
1325
+ const rewritten = updateFeedItemConversationMessage(
1326
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1327
+ "Four things now.",
1328
+ new Set(["msg-9"]),
1329
+ );
1330
+
1331
+ expect(rewritten).toBe(false);
1332
+ expect(messageRewrites).toHaveLength(0);
1333
+ });
1334
+
1335
+ test("rewrites when the walk touched some other row", () => {
1336
+ messageOwners.set("msg-9", "conv-source-1");
1337
+
1338
+ const rewritten = updateFeedItemConversationMessage(
1339
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1340
+ "Four things now.",
1341
+ new Set(["1700000000.0001"]),
1342
+ );
1343
+
1344
+ expect(rewritten).toBe(true);
1345
+ expect(messageRewrites).toHaveLength(1);
1346
+ });
1347
+
1348
+ test("reports no rewrite when the ownership lookup throws", () => {
1349
+ // The edit has already patched the feed and run the channel updates by
1350
+ // the time this runs, so a store that cannot answer must not abort it.
1351
+ messageOwners.set("msg-9", "conv-source-1");
1352
+ messageLookupShouldThrow = true;
1353
+
1354
+ const rewritten = updateFeedItemConversationMessage(
1355
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1356
+ "Four things now.",
1357
+ );
1358
+
1359
+ expect(rewritten).toBe(false);
1360
+ expect(messageRewrites).toHaveLength(0);
1361
+ });
1362
+
1363
+ test("reports no rewrite when the store write throws", () => {
1364
+ messageOwners.set("msg-9", "conv-source-1");
1365
+ messageRewriteShouldThrow = true;
1366
+
1367
+ const rewritten = updateFeedItemConversationMessage(
1368
+ makeItem({ notificationConversationMessageId: "msg-9" }),
1369
+ "Four things now.",
1370
+ );
1371
+
1372
+ expect(rewritten).toBe(false);
1373
+ });
1374
+ });
843
1375
  });
@@ -20,11 +20,15 @@
20
20
 
21
21
  import type { AssistantEvent } from "../../api/index.js";
22
22
  import type { InterfaceId } from "../../channels/types.js";
23
+ import { updateMessageContent } from "../../persistence/conversation-crud.js";
24
+ import { publishConversationMessagesChanged } from "../../runtime/sync/resource-sync-events.js";
23
25
  import { getLogger } from "../../util/logger.js";
24
26
  import type {
25
27
  ChannelAdapter,
26
28
  ChannelDeliveryPayload,
27
29
  ChannelDestination,
30
+ ChannelUpdateContext,
31
+ ChannelUpdatePayload,
28
32
  DeliveryResult,
29
33
  NotificationChannel,
30
34
  } from "../types.js";
@@ -132,4 +136,55 @@ export class VellumAdapter implements ChannelAdapter {
132
136
  return { success: false, error: message };
133
137
  }
134
138
  }
139
+
140
+ /**
141
+ * Rewrite the conversation message this delivery persisted.
142
+ *
143
+ * A vellum delivery has no channel-native message to patch, but it does
144
+ * carry the id of the row written into the conversation the notification
145
+ * card links to. Editing a notification patches the feed item, so without
146
+ * this the card would show the new body while "Go to Conversation" opened
147
+ * the old one.
148
+ *
149
+ * That row holds the body, and the feed rewrites its summary only when the
150
+ * patch carries one, so a title-only patch leaves the row alone to keep the
151
+ * two in step. A delivery carrying no message id has no row to rewrite.
152
+ */
153
+ async update(
154
+ delivery: ChannelUpdateContext,
155
+ patch: ChannelUpdatePayload,
156
+ ): Promise<DeliveryResult> {
157
+ if (!delivery.messageId) {
158
+ return {
159
+ success: false,
160
+ error:
161
+ "missing_message_id: this delivery persisted no conversation message",
162
+ };
163
+ }
164
+ if (patch.body === undefined) {
165
+ log.info(
166
+ { deliveryId: delivery.deliveryId, messageId: delivery.messageId },
167
+ "Vellum notification edit carried no body, conversation message left as is",
168
+ );
169
+ return { success: true, messageId: delivery.messageId };
170
+ }
171
+ try {
172
+ updateMessageContent(delivery.messageId, patch.body);
173
+ if (delivery.conversationId) {
174
+ publishConversationMessagesChanged(delivery.conversationId);
175
+ }
176
+ log.info(
177
+ { deliveryId: delivery.deliveryId, messageId: delivery.messageId },
178
+ "Vellum notification conversation message updated",
179
+ );
180
+ return { success: true, messageId: delivery.messageId };
181
+ } catch (err) {
182
+ const message = err instanceof Error ? err.message : String(err);
183
+ log.error(
184
+ { err, deliveryId: delivery.deliveryId, messageId: delivery.messageId },
185
+ "Failed to update Vellum notification conversation message",
186
+ );
187
+ return { success: false, error: message };
188
+ }
189
+ }
135
190
  }
@@ -14,7 +14,10 @@
14
14
 
15
15
  import type { Button, CardBlock, ContextBlock, KnownBlock } from "@slack/types";
16
16
 
17
- import { sendSlackReply } from "../../messaging/providers/slack/send.js";
17
+ import {
18
+ sendSlackReply,
19
+ updateSlackMessage,
20
+ } from "../../messaging/providers/slack/send.js";
18
21
  import { APPROVAL_INSTRUCTION_BLOCK_ID_PREFIX } from "../../messaging/providers/slack/withdraw.js";
19
22
  import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
20
23
  import { getLogger } from "../../util/logger.js";
@@ -476,10 +479,12 @@ export class SlackAdapter implements ChannelAdapter {
476
479
  return { success: false, error: "no body or title supplied for update" };
477
480
  }
478
481
  try {
479
- const result = await sendSlackReply(delivery.destination, text, {
480
- messageTs: delivery.messageId,
481
- useBlocks: true,
482
- });
482
+ const result = await updateSlackMessage(
483
+ delivery.destination,
484
+ delivery.messageId,
485
+ text,
486
+ { useBlocks: true },
487
+ );
483
488
  log.info(
484
489
  { chatId: delivery.destination, messageTs: delivery.messageId },
485
490
  "Slack notification updated",