@vellumai/assistant 0.11.4 → 0.11.5-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) 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__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -168,6 +168,41 @@ Identifiers and plumbing notes:
168
168
  - Guardian request records live in the gateway-owned `guardian_requests` table (and their `guardian_request_deliveries`); the daemon reads/writes them through the `channels/gateway-guardian-requests.ts` client.
169
169
  - Legacy in-turn interception (`routes/guardian-approval-interception.ts` + the approval prompt watcher) still serves a guardian's own tool-approval prompts mid-turn, resolving via `conversation.handleConfirmationResponse(requestId, decision)`. It is a legacy rail — the reply router runs first; converge new work on the pipeline.
170
170
 
171
+ ### Message metadata vocabulary
172
+
173
+ Several differently-scoped things are called metadata on the channel path, and
174
+ they nest, so a name that fits two of them reads as one concept. Counterpart to
175
+ the gateway's Channel Identity Vocabulary, which covers the wire side.
176
+
177
+ - **`sourceMetadata`** describes an inbound event in flight on the gateway to
178
+ daemon wire (`SourceMetadataSchema`, `packages/gateway-client`): provider
179
+ ids, trust verdict, admission policy. It is also persisted verbatim on the
180
+ stored inbound payload, because the retry sweep replays that payload and has
181
+ to reconstruct the same turn (`channel-retry-sweep.ts`).
182
+ - **`messages.metadata`** is the stored envelope on a row. Everything below is
183
+ a key inside it. `messageMetadataSchema` (`persistence/conversation-crud.ts`)
184
+ describes its shared keys; it is parsed where a reader needs them, not
185
+ enforced on every write.
186
+ - **`buildChannelMetadata`** (`routes/channel-metadata.ts`) builds the turn's
187
+ portion of that envelope: provenance, `userMessageChannel`, interfaces,
188
+ attachments. Callers spread it and add channel-specific keys alongside, so it
189
+ is a producer of the envelope, never a key within it.
190
+ - **`slackMeta`** is the per-row key describing what a row is in its Slack
191
+ conversation: `channelTs` for the row's own id, `threadTs` for its thread,
192
+ `reaction.targetChannelTs` for the message a reaction was attached to, plus
193
+ Slack's own file markers and timezone labels. Every channel-path reader goes
194
+ through it today.
195
+ - **`providerMeta`** is the channel-neutral counterpart of that key
196
+ (`messaging/provider-message-metadata.ts`): `conversationExternalId`,
197
+ `messageId`, `threadId`, `actorExternalId`, `eventKind` and
198
+ `reaction.targetMessageId`, deliberately the same vocabulary the inbound
199
+ wire uses in `InboundEventBase`, so a channel that can describe itself at
200
+ ingress can describe its stored rows without a translation of its own.
201
+ Writable by any channel, including one this repo has no code for. `readProviderMetadata` reads it and maps `slackMeta` onto it.
202
+ **Nothing writes or reads it yet**; it is the home a new channel should use
203
+ rather than inventing a sixth key, and existing readers move onto it as
204
+ channels adopt it.
205
+
171
206
  ### Channel verification: gateway-owned
172
207
 
173
208
  Verification SESSION state (sessions, secrets, rate limits, validate+consume) AND the channel-verified OUTCOME (status / verifiedAt / verifiedVia) are both gateway-owned. The gateway holds the `channel_verification_sessions` + `channel_guardian_rate_limits` tables (`gateway/src/db/session-store.ts`) and mints all secrets in `gateway/src/verification/session-service.ts`; the daemon holds no session or rate-limit state (its legacy tables were dropped by gateway data migration m0014).
@@ -0,0 +1,131 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import { emitDaemonError } from "../../daemon/startup-error.js";
4
+ import {
5
+ startRuntimeHttpServer,
6
+ stopRuntimeHttpServer,
7
+ } from "../http-server.js";
8
+
9
+ /**
10
+ * The daemon binds two client-facing transports. An occupied runtime HTTP port
11
+ * must abort startup the same way an occupied IPC socket does, so the daemon
12
+ * can never answer IPC (reading healthy to `vellum ps` and platform status)
13
+ * while the gateway proxies /v1/* to a foreign listener.
14
+ */
15
+ describe("runtime HTTP port collision", () => {
16
+ const originalPort = process.env.RUNTIME_HTTP_PORT;
17
+
18
+ afterEach(async () => {
19
+ await stopRuntimeHttpServer();
20
+ if (originalPort === undefined) {
21
+ delete process.env.RUNTIME_HTTP_PORT;
22
+ } else {
23
+ process.env.RUNTIME_HTTP_PORT = originalPort;
24
+ }
25
+ });
26
+
27
+ test("aborts startup when a foreign process holds the runtime HTTP port", async () => {
28
+ /**
29
+ * Tests that a runtime HTTP bind collision fails daemon startup instead of
30
+ * silently degrading to IPC-only operation.
31
+ */
32
+
33
+ // GIVEN a foreign process holding a port
34
+ const foreign = Bun.serve({
35
+ port: 0,
36
+ hostname: "127.0.0.1",
37
+ fetch: () => new Response("foreign"),
38
+ });
39
+
40
+ // AND the daemon configured to serve HTTP on that same port
41
+ process.env.RUNTIME_HTTP_PORT = String(foreign.port);
42
+
43
+ try {
44
+ // WHEN the daemon starts its runtime HTTP server
45
+ const err = await startRuntimeHttpServer().then(
46
+ () => null,
47
+ (e: unknown) => e,
48
+ );
49
+
50
+ // THEN startup fails rather than continuing without HTTP
51
+ expect(err).toBeInstanceOf(Error);
52
+
53
+ // AND the failure carries the address-collision code the daemon's
54
+ // startup-error categorization branches on
55
+ expect((err as NodeJS.ErrnoException).code).toBe("EADDRINUSE");
56
+
57
+ // AND the message names the occupied port and the env var that moves it
58
+ expect((err as Error).message).toContain(String(foreign.port));
59
+ expect((err as Error).message).toContain("RUNTIME_HTTP_PORT");
60
+ } finally {
61
+ foreign.stop(true);
62
+ }
63
+ });
64
+
65
+ test("reports a runtime HTTP port collision as PORT_IN_USE on stderr", async () => {
66
+ /**
67
+ * Tests that the bind failure reaches consumers that parse the daemon's
68
+ * structured startup-error line (e.g. the macOS app) as PORT_IN_USE.
69
+ */
70
+
71
+ // GIVEN a foreign process holding the daemon's runtime HTTP port
72
+ const foreign = Bun.serve({
73
+ port: 0,
74
+ hostname: "127.0.0.1",
75
+ fetch: () => new Response("foreign"),
76
+ });
77
+ process.env.RUNTIME_HTTP_PORT = String(foreign.port);
78
+
79
+ // AND stderr captured so the structured startup-error line is observable
80
+ const written: string[] = [];
81
+ const originalWrite = process.stderr.write.bind(process.stderr);
82
+ process.stderr.write = ((chunk: string | Uint8Array) => {
83
+ written.push(chunk.toString());
84
+ return true;
85
+ }) as typeof process.stderr.write;
86
+
87
+ try {
88
+ // WHEN the daemon's startup error handler reports the bind failure
89
+ const err = await startRuntimeHttpServer().then(
90
+ () => null,
91
+ (e: unknown) => e,
92
+ );
93
+ emitDaemonError(err);
94
+
95
+ // THEN the structured line categorizes the failure as PORT_IN_USE
96
+ const line = written.find((entry) => entry.includes("DAEMON_ERROR:"));
97
+ expect(line).toBeDefined();
98
+ const structured = JSON.parse(
99
+ (line as string).replace("DAEMON_ERROR:", "").trim(),
100
+ ) as Record<string, unknown>;
101
+ expect(structured.error).toBe("PORT_IN_USE");
102
+ } finally {
103
+ process.stderr.write = originalWrite;
104
+ foreign.stop(true);
105
+ }
106
+ });
107
+
108
+ test("binds and leaves the singleton serving when the port is free", async () => {
109
+ /**
110
+ * Tests that the collision guard does not turn ordinary startup into a
111
+ * failure: a free port still yields a listening HTTP server.
112
+ */
113
+
114
+ // GIVEN a free port for the runtime HTTP server
115
+ const probe = Bun.serve({
116
+ port: 0,
117
+ hostname: "127.0.0.1",
118
+ fetch: () => new Response("probe"),
119
+ });
120
+ const freePort = probe.port;
121
+ probe.stop(true);
122
+ process.env.RUNTIME_HTTP_PORT = String(freePort);
123
+
124
+ // WHEN the daemon starts its runtime HTTP server
125
+ await startRuntimeHttpServer();
126
+
127
+ // THEN the server answers its liveness probe
128
+ const response = await fetch(`http://127.0.0.1:${freePort}/healthz`);
129
+ expect(response.status).toBe(200);
130
+ });
131
+ });
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Tests for the web presence policy.
3
+ *
4
+ * The invariant under test is fail-open: only a fresh report from a `web`
5
+ * client with `visible: true` and a matching `focusedConversationId` answers
6
+ * `true`. Every other shape (stale, hidden, mismatched conversation, no
7
+ * report, wrong interface, no clients, hub throw) answers `false` so the push
8
+ * is sent. Scoping the read to an actor principal narrows what counts as
9
+ * focus, so it can only ever turn a `true` into a `false`.
10
+ */
11
+
12
+ import { afterEach, describe, expect, spyOn, test } from "bun:test";
13
+
14
+ import {
15
+ clearHubClients,
16
+ registerHubClient,
17
+ type RegisterHubClientArgs,
18
+ } from "../../__tests__/helpers/hub-clients.js";
19
+ import { assistantEventHub } from "../assistant-event-hub.js";
20
+ import {
21
+ isWebConversationFocused,
22
+ WEB_PRESENCE_STALE_AFTER_MS,
23
+ } from "../web-presence.js";
24
+
25
+ const CONVERSATION_ID = "conv-1";
26
+
27
+ // ── Test helpers ──────────────────────────────────────────────────────────
28
+
29
+ /** A `now` far enough past every report to push all of them out of the window. */
30
+ function afterStaleness(): Date {
31
+ return new Date(Date.now() + WEB_PRESENCE_STALE_AFTER_MS + 1_000);
32
+ }
33
+
34
+ function registerClient(
35
+ args: Omit<RegisterHubClientArgs, "hub" | "presence">,
36
+ ): void {
37
+ registerHubClient({ ...args, hub: assistantEventHub, interfaceId: "web" });
38
+ }
39
+
40
+ function reportWebPresence(
41
+ clientId: string,
42
+ report: { visible: boolean; focusedConversationId: string | null },
43
+ ): void {
44
+ assistantEventHub.setClientWebPresence(clientId, report);
45
+ }
46
+
47
+ function clientEntry(clientId: string) {
48
+ const client = assistantEventHub
49
+ .listClientsByInterface("web")
50
+ .find((entry) => entry.clientId === clientId);
51
+ if (!client) {
52
+ throw new Error(`missing test client ${clientId}`);
53
+ }
54
+ return client;
55
+ }
56
+
57
+ afterEach(() => {
58
+ clearHubClients(assistantEventHub);
59
+ });
60
+
61
+ // ── Tests ─────────────────────────────────────────────────────────────────
62
+
63
+ describe("isWebConversationFocused", () => {
64
+ test("fresh visible report focused on the conversation is focused", () => {
65
+ registerClient({ clientId: "web-1" });
66
+ reportWebPresence("web-1", {
67
+ visible: true,
68
+ focusedConversationId: CONVERSATION_ID,
69
+ });
70
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(true);
71
+ });
72
+
73
+ test("report older than the staleness bound is not focused", () => {
74
+ registerClient({ clientId: "web-1" });
75
+ reportWebPresence("web-1", {
76
+ visible: true,
77
+ focusedConversationId: CONVERSATION_ID,
78
+ });
79
+ expect(
80
+ isWebConversationFocused(CONVERSATION_ID, { now: afterStaleness() }),
81
+ ).toBe(false);
82
+ });
83
+
84
+ test("semantic report expires even while SSE transport stays alive", () => {
85
+ registerClient({ clientId: "web-1" });
86
+ reportWebPresence("web-1", {
87
+ visible: true,
88
+ focusedConversationId: CONVERSATION_ID,
89
+ });
90
+ const now = new Date(Date.now() + WEB_PRESENCE_STALE_AFTER_MS + 1_000);
91
+ clientEntry("web-1").lastActiveAt = now;
92
+
93
+ expect(isWebConversationFocused(CONVERSATION_ID, { now })).toBe(false);
94
+ });
95
+
96
+ test("transport expiry is not focused even with a fresh semantic report", () => {
97
+ registerClient({ clientId: "web-1" });
98
+ reportWebPresence("web-1", {
99
+ visible: true,
100
+ focusedConversationId: CONVERSATION_ID,
101
+ });
102
+ const now = new Date();
103
+ clientEntry("web-1").lastActiveAt = new Date(
104
+ now.getTime() - WEB_PRESENCE_STALE_AFTER_MS - 1_000,
105
+ );
106
+ clientEntry("web-1").webPresence!.reportedAt = now;
107
+
108
+ expect(isWebConversationFocused(CONVERSATION_ID, { now })).toBe(false);
109
+ });
110
+
111
+ test("hidden tab is not focused even on the matching conversation", () => {
112
+ registerClient({ clientId: "web-1" });
113
+ reportWebPresence("web-1", {
114
+ visible: false,
115
+ focusedConversationId: CONVERSATION_ID,
116
+ });
117
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
118
+ });
119
+
120
+ test("visible tab focused on a different conversation is not focused", () => {
121
+ registerClient({ clientId: "web-1" });
122
+ reportWebPresence("web-1", {
123
+ visible: true,
124
+ focusedConversationId: "conv-other",
125
+ });
126
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
127
+ });
128
+
129
+ test("visible tab with no focused conversation is not focused", () => {
130
+ registerClient({ clientId: "web-1" });
131
+ reportWebPresence("web-1", { visible: true, focusedConversationId: null });
132
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
133
+ });
134
+
135
+ test("client that never reported presence is not focused", () => {
136
+ registerClient({ clientId: "web-1" });
137
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
138
+ });
139
+
140
+ test("a macos client's presence does not count as web focus", () => {
141
+ registerHubClient({
142
+ hub: assistantEventHub,
143
+ clientId: "mac-1",
144
+ interfaceId: "macos",
145
+ });
146
+ assistantEventHub.setClientWebPresence("mac-1", {
147
+ visible: true,
148
+ focusedConversationId: CONVERSATION_ID,
149
+ });
150
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
151
+ });
152
+
153
+ test("no connected clients is not focused", () => {
154
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
155
+ });
156
+
157
+ test("a hub read that throws is not focused", () => {
158
+ const spy = spyOn(
159
+ assistantEventHub,
160
+ "listClientsByInterface",
161
+ ).mockImplementation(() => {
162
+ throw new Error("hub exploded");
163
+ });
164
+ try {
165
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
166
+ } finally {
167
+ spy.mockRestore();
168
+ }
169
+ });
170
+
171
+ test("one focused web client among several is enough", () => {
172
+ registerClient({ clientId: "web-1" });
173
+ registerClient({ clientId: "web-2" });
174
+ reportWebPresence("web-1", {
175
+ visible: true,
176
+ focusedConversationId: "conv-other",
177
+ });
178
+ reportWebPresence("web-2", {
179
+ visible: true,
180
+ focusedConversationId: CONVERSATION_ID,
181
+ });
182
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(true);
183
+ });
184
+ });
185
+
186
+ describe("isWebConversationFocused scoped to an actor principal", () => {
187
+ test("matching actor principal is focused", () => {
188
+ registerClient({ clientId: "web-1", actorPrincipalId: "actor-a" });
189
+ reportWebPresence("web-1", {
190
+ visible: true,
191
+ focusedConversationId: CONVERSATION_ID,
192
+ });
193
+ expect(
194
+ isWebConversationFocused(CONVERSATION_ID, {
195
+ actorPrincipalId: "actor-a",
196
+ }),
197
+ ).toBe(true);
198
+ });
199
+
200
+ test("another actor's focused tab does not count", () => {
201
+ registerClient({ clientId: "web-1", actorPrincipalId: "actor-a" });
202
+ reportWebPresence("web-1", {
203
+ visible: true,
204
+ focusedConversationId: CONVERSATION_ID,
205
+ });
206
+ expect(
207
+ isWebConversationFocused(CONVERSATION_ID, {
208
+ actorPrincipalId: "actor-b",
209
+ }),
210
+ ).toBe(false);
211
+ });
212
+
213
+ test("client with no principal does not match a supplied principal", () => {
214
+ registerClient({ clientId: "web-1" });
215
+ reportWebPresence("web-1", {
216
+ visible: true,
217
+ focusedConversationId: CONVERSATION_ID,
218
+ });
219
+ expect(
220
+ isWebConversationFocused(CONVERSATION_ID, {
221
+ actorPrincipalId: "actor-a",
222
+ }),
223
+ ).toBe(false);
224
+ });
225
+
226
+ test("omitting the principal counts any focused web client", () => {
227
+ registerClient({ clientId: "web-1", actorPrincipalId: "actor-a" });
228
+ reportWebPresence("web-1", {
229
+ visible: true,
230
+ focusedConversationId: CONVERSATION_ID,
231
+ });
232
+ expect(isWebConversationFocused(CONVERSATION_ID)).toBe(true);
233
+ });
234
+ });
@@ -109,6 +109,24 @@ export interface ClientPresence {
109
109
  reportedAt: Date;
110
110
  }
111
111
 
112
+ /**
113
+ * Web tab visibility and conversation focus, as reported by a `web`-interface
114
+ * client (a browser tab or the Electron renderer). Distinct from
115
+ * {@link ClientPresence}: scoped to a single conversation rather than app-wide
116
+ * attendance, and reported by the shared web renderer rather than the macOS
117
+ * host-proxy bridge. See `runtime/web-presence.ts` for the read-side policy.
118
+ */
119
+ export interface WebPresenceReport {
120
+ visible: boolean;
121
+ /** The conversation currently focused (chat composer on screen), or `null`. */
122
+ focusedConversationId: string | null;
123
+ }
124
+
125
+ export interface ClientWebPresence extends WebPresenceReport {
126
+ /** When the daemon last heard from the client. Drives staleness. */
127
+ reportedAt: Date;
128
+ }
129
+
112
130
  interface ClientEntry extends BaseSubscriberEntry {
113
131
  type: "client";
114
132
  clientId: string;
@@ -130,6 +148,11 @@ interface ClientEntry extends BaseSubscriberEntry {
130
148
  * In-memory only, so consumers must fail open when it is absent.
131
149
  */
132
150
  presence?: ClientPresence;
151
+ /**
152
+ * Last web tab visibility/focus reported by this client, for clients that
153
+ * report it. In-memory only, so consumers must fail open when it is absent.
154
+ */
155
+ webPresence?: ClientWebPresence;
133
156
  }
134
157
 
135
158
  interface ProcessEntry extends BaseSubscriberEntry {
@@ -621,6 +644,21 @@ export class AssistantEventHub {
621
644
  return matched;
622
645
  }
623
646
 
647
+ /**
648
+ * Record the web tab visibility/focused-conversation reported by a client.
649
+ * `reportedAt` always advances. Returns true when at least one active
650
+ * client entry matched.
651
+ */
652
+ setClientWebPresence(clientId: string, report: WebPresenceReport): boolean {
653
+ const now = new Date();
654
+ let matched = false;
655
+ for (const entry of this.activeClientEntries(clientId)) {
656
+ entry.webPresence = { ...report, reportedAt: now };
657
+ matched = true;
658
+ }
659
+ return matched;
660
+ }
661
+
624
662
  /**
625
663
  * Force-disconnect a client by disposing all subscribers for the given
626
664
  * `clientId`. Returns the number of disposed entries.
@@ -1,6 +1,9 @@
1
+ import type { MessageAudience } from "@vellumai/gateway-client";
2
+
1
3
  import { stripVellumLinks } from "../daemon/assistant-attachments.js";
2
4
  import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
3
5
  import { renderHistoryContent } from "../daemon/handlers/shared.js";
6
+ import { editChannelMessage } from "../messaging/providers/index.js";
4
7
  import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
5
8
  import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
6
9
  import {
@@ -35,13 +38,11 @@ type DeliverRenderedReplyParams = {
35
38
  * 1-based count of segments delivered so far (including prior attempts). */
36
39
  onSegmentDelivered?: (deliveredCount: number) => void;
37
40
  /**
38
- * When true, deliver via ephemeral messaging so only the target `user`
39
- * sees the content. Ephemeral messages are fire-and-forget: they cannot
40
- * be edited or deleted after posting.
41
+ * Restricts the reply to one reader. Absent means the whole room sees it.
42
+ * A restricted reply is fire-and-forget: it cannot be edited or deleted
43
+ * after posting.
41
44
  */
42
- ephemeral?: boolean;
43
- /** Channel-specific user ID — required when `ephemeral` is true. */
44
- user?: string;
45
+ audience?: MessageAudience;
45
46
  /** When provided, the first segment will update the existing message
46
47
  * identified by this ts instead of posting a new one (Slack-specific). */
47
48
  messageTs?: string;
@@ -140,8 +141,7 @@ export async function deliverRenderedReplyViaCallback(
140
141
  interSegmentDelayMs = INTER_SEGMENT_DELAY_MS,
141
142
  startFromSegment = 0,
142
143
  onSegmentDelivered,
143
- ephemeral,
144
- user,
144
+ audience,
145
145
  messageTs,
146
146
  onMessageTs,
147
147
  } = params;
@@ -167,9 +167,7 @@ export async function deliverRenderedReplyViaCallback(
167
167
  chatId,
168
168
  attachments: replyAttachments,
169
169
  assistantId,
170
- ephemeral,
171
- user,
172
- messageTs,
170
+ audience,
173
171
  },
174
172
  );
175
173
  if (result.ts) {
@@ -187,9 +185,7 @@ export async function deliverRenderedReplyViaCallback(
187
185
  chatId,
188
186
  attachments: replyAttachments,
189
187
  assistantId,
190
- ephemeral,
191
- user,
192
- messageTs,
188
+ audience,
193
189
  },
194
190
  );
195
191
  const deliveredTs = result.ts ?? messageTs;
@@ -210,21 +206,51 @@ export async function deliverRenderedReplyViaCallback(
210
206
  const isLastSegment = i === deliverableSegments.length - 1;
211
207
  const isFirstSegment = i === startFromSegment;
212
208
  const segmentText = deliverableSegments[i];
213
- const result: ChannelDeliveryResult = await deliverChannelReply(
214
- callbackUrl,
215
- {
209
+ // Ask the channel to render richly; each channel's adapter decides how
210
+ // (Slack to Block Kit). Channels without rich rendering send plain text.
211
+ const segmentAttachments = isLastSegment ? replyAttachments : undefined;
212
+ const editTarget = isFirstSegment ? currentMessageTs : undefined;
213
+
214
+ let result: ChannelDeliveryResult;
215
+ if (editTarget) {
216
+ result = await editChannelMessage(callbackUrl, {
216
217
  chatId,
218
+ messageId: editTarget,
217
219
  text: segmentText,
218
- // Ask the channel to render richly; each channel's adapter decides how
219
- // (Slack → Block Kit). Channels without rich rendering send plain text.
220
220
  useBlocks: true,
221
- attachments: isLastSegment ? replyAttachments : undefined,
221
+ });
222
+ // An edit replaces the text of one message. Attachments are always new
223
+ // messages, so they still have to be posted alongside it.
224
+ //
225
+ // Failures here are not the reply failing. A transport rejects a total
226
+ // attachment failure only when there is no text to fall back on, and the
227
+ // edit above already delivered the text. Rethrowing would mark a reply
228
+ // undelivered that the reader can see, and have the sweep repost it.
229
+ if (segmentAttachments) {
230
+ try {
231
+ await deliverChannelReply(callbackUrl, {
232
+ chatId,
233
+ attachments: segmentAttachments,
234
+ assistantId,
235
+ audience,
236
+ });
237
+ } catch (err) {
238
+ log.warn(
239
+ { err, chatId },
240
+ "Attachments failed after an in-place edit; the edited text stands",
241
+ );
242
+ }
243
+ }
244
+ } else {
245
+ result = await deliverChannelReply(callbackUrl, {
246
+ chatId,
247
+ text: segmentText,
248
+ useBlocks: true,
249
+ attachments: segmentAttachments,
222
250
  assistantId,
223
- ephemeral,
224
- user,
225
- messageTs: isFirstSegment ? currentMessageTs : undefined,
226
- },
227
- );
251
+ audience,
252
+ });
253
+ }
228
254
 
229
255
  if (result.ts) {
230
256
  currentMessageTs = result.ts;
@@ -251,10 +277,8 @@ export type DeliverReplyOptions = {
251
277
  sinceMessageId?: string;
252
278
  startFromSegment?: number;
253
279
  onSegmentDelivered?: (deliveredCount: number) => void;
254
- /** Deliver as ephemeral (visible only to `user`). Fire-and-forget. */
255
- ephemeral?: boolean;
256
- /** Channel-specific user ID — required when `ephemeral` is true. */
257
- user?: string;
280
+ /** Restricts the reply to one reader. Absent means the whole room. */
281
+ audience?: MessageAudience;
258
282
  /** Update an existing message instead of posting a new one. */
259
283
  messageTs?: string;
260
284
  /** Called with the ts of the delivered/updated message. */
@@ -384,8 +408,7 @@ async function deliverPersistedAssistantMessageViaCallback(
384
408
  assistantId,
385
409
  startFromSegment: options?.startFromSegment,
386
410
  onSegmentDelivered: options?.onSegmentDelivered,
387
- ephemeral: options?.ephemeral,
388
- user: options?.user,
411
+ audience: options?.audience,
389
412
  messageTs: options?.messageTs,
390
413
  onMessageTs: composedOnMessageTs,
391
414
  });
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { AssistantEvent } from "../api/index.js";
6
+ import { audienceForReader } from "../channels/message-audience.js";
6
7
  import {
7
8
  type ChannelId,
8
9
  isChannelId,
@@ -333,10 +334,11 @@ export async function sweepFailedEvents(
333
334
  text: DISK_PRESSURE_REMOTE_BLOCK_REPLY,
334
335
  assistantId,
335
336
  };
336
- if (sourceChannel === "slack" && requesterExternalUserId) {
337
- replyPayload.ephemeral = true;
338
- replyPayload.user = requesterExternalUserId;
339
- }
337
+ replyPayload.audience = audienceForReader(
338
+ sourceChannel,
339
+ externalChatId,
340
+ requesterExternalUserId,
341
+ );
340
342
  try {
341
343
  await deliverChannelReply(replyCallbackUrl, replyPayload);
342
344
  } catch (err) {
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  canActOnPrivilegedDocuments,
5
+ canSeePersonalMemory,
5
6
  isArchiveBySenderAuthorized,
6
7
  } from "./effective-capabilities.js";
7
8
 
@@ -90,3 +91,21 @@ describe("isArchiveBySenderAuthorized", () => {
90
91
  ).toBe(false);
91
92
  });
92
93
  });
94
+
95
+ describe("canSeePersonalMemory", () => {
96
+ test("only the guardian class sees personal memory", () => {
97
+ expect(canSeePersonalMemory({ trustClass: "guardian" })).toBe(true);
98
+ for (const trustClass of [
99
+ "trusted_contact",
100
+ "unverified_contact",
101
+ "unknown",
102
+ ]) {
103
+ expect(canSeePersonalMemory({ trustClass })).toBe(false);
104
+ }
105
+ });
106
+
107
+ test("an unrecognized or absent class fails closed", () => {
108
+ expect(canSeePersonalMemory({ trustClass: "non_guardian" })).toBe(false);
109
+ expect(canSeePersonalMemory({ trustClass: undefined })).toBe(false);
110
+ });
111
+ });