@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
@@ -19,6 +19,7 @@ import {
19
19
  getContainerMemoryLimitBytes,
20
20
  getContainerMemoryUsageBytes,
21
21
  } from "../../util/cgroup-memory.js";
22
+ import { getCachedContainerCpuPercent } from "../../util/container-cpu-sampler.js";
22
23
  import { getDiskUsageInfo } from "../../util/disk-usage.js";
23
24
  import { getWorkspacePromptPath } from "../../util/platform.js";
24
25
  import { APP_VERSION } from "../../version.js";
@@ -93,128 +94,11 @@ interface CpuInfo {
93
94
  maxCores: number;
94
95
  }
95
96
 
96
- /**
97
- * Read the container's CPU usage from cgroup accounting files.
98
- *
99
- * Returns total CPU microseconds consumed by the container since boot.
100
- * We use the delta between two samples to compute percentage.
101
- */
102
- function getContainerCpuUsageUs(): number | null {
103
- // cgroups v2: cpu.stat has a "usage_usec" line.
104
- try {
105
- const stat = readFileSync("/sys/fs/cgroup/cpu.stat", "utf-8");
106
- for (const line of stat.split("\n")) {
107
- if (line.startsWith("usage_usec")) {
108
- const val = parseInt(line.split(/\s+/)[1], 10);
109
- if (!isNaN(val) && val > 0) {
110
- return val;
111
- }
112
- }
113
- }
114
- } catch {
115
- /* not available */
116
- }
117
-
118
- // cgroups v1: cpuacct.usage is in nanoseconds.
119
- try {
120
- const ns = parseInt(
121
- readFileSync("/sys/fs/cgroup/cpuacct/cpuacct.usage", "utf-8").trim(),
122
- 10,
123
- );
124
- if (!isNaN(ns) && ns > 0) {
125
- return ns / 1000;
126
- } // convert ns → µs
127
- } catch {
128
- /* not available */
129
- }
130
-
131
- return null;
132
- }
133
-
134
- // Track CPU usage over a rolling window so /v1/health reports near-real-time
135
- // utilization instead of a lifetime average (total CPU time / total uptime).
136
- const CPU_SAMPLE_INTERVAL_MS = 5_000;
137
-
138
- /**
139
- * Sample this process's cumulative CPU time, returning null when the
140
- * underlying syscall fails (mirroring {@link sampleProcessRssBytes}).
141
- */
142
- function sampleProcessCpuUsage(): NodeJS.CpuUsage | null {
143
- try {
144
- return process.cpuUsage();
145
- } catch {
146
- return null;
147
- }
148
- }
149
-
150
- let _lastProcessCpuUsage: NodeJS.CpuUsage | null = sampleProcessCpuUsage();
151
- let _lastCgroupCpuUs: number | null = getContainerCpuUsageUs();
152
- let _lastCpuTime: number = Date.now();
153
- let _cachedCpuPercent = 0;
154
-
155
- // Kick off the background sampler. unref() so it never prevents process exit.
156
- setInterval(() => {
157
- const now = Date.now();
158
- const elapsedMs = now - _lastCpuTime;
159
- if (elapsedMs <= 0) {
160
- return;
161
- }
162
-
163
- const numCores = getContainerCpuCores();
164
- if (numCores <= 0) {
165
- _lastCpuTime = now;
166
- return;
167
- }
168
-
169
- // Always sample process-level CPU so the baseline stays fresh. This
170
- // prevents a spike if the platform cgroup path later falls back to
171
- // process.cpuUsage() after cgroup stats were previously available.
172
- const newProcessUsage = sampleProcessCpuUsage();
173
- const processDeltaUs =
174
- newProcessUsage !== null && _lastProcessCpuUsage !== null
175
- ? newProcessUsage.user -
176
- _lastProcessCpuUsage.user +
177
- (newProcessUsage.system - _lastProcessCpuUsage.system)
178
- : null;
179
- if (newProcessUsage !== null) {
180
- _lastProcessCpuUsage = newProcessUsage;
181
- }
182
-
183
- if (getIsPlatform()) {
184
- // In platform mode, prefer cgroup-level CPU usage so we see the full
185
- // container footprint, not just this process.
186
- const cgroupUs = getContainerCpuUsageUs();
187
- if (cgroupUs !== null && _lastCgroupCpuUs !== null) {
188
- const deltaCpuUs = cgroupUs - _lastCgroupCpuUs;
189
- const deltaCpuMs = deltaCpuUs / 1000;
190
- _cachedCpuPercent =
191
- Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
192
- } else if (processDeltaUs !== null) {
193
- // cgroup CPU stats unavailable (e.g. gVisor) – fall back to process-level.
194
- const deltaCpuMs = processDeltaUs / 1000;
195
- _cachedCpuPercent =
196
- Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
197
- }
198
- _lastCgroupCpuUs = cgroupUs;
199
- } else if (processDeltaUs !== null) {
200
- // Non-platform: use process.cpuUsage() (accurate for single-process mode).
201
- const deltaCpuMs = processDeltaUs / 1000;
202
- _cachedCpuPercent =
203
- Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
204
- }
205
-
206
- _lastCpuTime = now;
207
- }, CPU_SAMPLE_INTERVAL_MS).unref();
208
-
209
97
  function getCpuInfo(): CpuInfo {
210
- try {
211
- return {
212
- currentPercent: _cachedCpuPercent,
213
- maxCores: Math.ceil(getContainerCpuCores()),
214
- };
215
- } catch {
216
- return { currentPercent: 0, maxCores: 0 };
217
- }
98
+ return {
99
+ currentPercent: getCachedContainerCpuPercent(),
100
+ maxCores: Math.ceil(getContainerCpuCores()),
101
+ };
218
102
  }
219
103
 
220
104
  /**
@@ -17,6 +17,7 @@ import {
17
17
  attachmentsToContentBlocks,
18
18
  type MessageAttachmentInput,
19
19
  } from "../../agent/attachments.js";
20
+ import { audienceForReader } from "../../channels/message-audience.js";
20
21
  import {
21
22
  CHANNEL_IDS,
22
23
  INTERFACE_IDS,
@@ -42,6 +43,7 @@ import { mapChatTypeToConversationType } from "../../daemon/trust-context.js";
42
43
  import type { TrustContext } from "../../daemon/trust-context-types.js";
43
44
  import { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
44
45
  import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
46
+ import { editChannelMessage } from "../../messaging/providers/index.js";
45
47
  import {
46
48
  resolveSlackBotUserId,
47
49
  withSlackBotToken,
@@ -77,7 +79,7 @@ import {
77
79
  addMessage,
78
80
  getMessageById,
79
81
  getMessages,
80
- selectSlackMetaCandidateMetadata,
82
+ selectProviderMetaCandidateMetadata,
81
83
  updateMessageContent,
82
84
  updateMessageMetadata,
83
85
  } from "../../persistence/conversation-crud.js";
@@ -90,6 +92,7 @@ import {
90
92
  import { markProcessed } from "../../persistence/delivery-status.js";
91
93
  import { upsertBinding } from "../../persistence/external-conversation-store.js";
92
94
  import type { ContentBlock } from "../../providers/types.js";
95
+ import { checkIngressForSecrets } from "../../security/secret-ingress.js";
93
96
  import { canonicalizeInboundIdentity } from "../../util/canonicalize-identity.js";
94
97
  import { getLogger } from "../../util/logger.js";
95
98
  import { truncate } from "../../util/truncate.js";
@@ -417,9 +420,10 @@ export async function handleChannelInbound({
417
420
  // 👍 never triggers a verification handshake or an access-request
418
421
  // notification, and a stranger's reaction creates no conversation/binding.
419
422
  // The interceptor drops strangers, records known contacts' reactions as
420
- // transcript signals, and routes a guardian's reaction on an approval card
421
- // through the guardian decision pipeline. Reactions never drive an
422
- // agent turn.
423
+ // transcript signals in the conversation of the reacted message, and routes
424
+ // a guardian's reaction on an approval card through the guardian decision
425
+ // pipeline. Reactions never mint a conversation and never drive an agent
426
+ // turn.
423
427
  if (isSlackReactionEvent(body)) {
424
428
  return handleSlackReactionIntercept({
425
429
  callbackData: body.callbackData!,
@@ -433,7 +437,6 @@ export async function handleChannelInbound({
433
437
  actorUsername: body.actorUsername,
434
438
  replyCallbackUrl: body.replyCallbackUrl,
435
439
  sourceMetadata: body.sourceMetadata,
436
- slackChannelName,
437
440
  approvalConversationGenerator,
438
441
  });
439
442
  }
@@ -907,10 +910,11 @@ export async function handleChannelInbound({
907
910
  text: replyText,
908
911
  assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
909
912
  };
910
- if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
911
- replyPayload.ephemeral = true;
912
- replyPayload.user = (canonicalSenderId ?? rawSenderId)!;
913
- }
913
+ replyPayload.audience = audienceForReader(
914
+ sourceChannel,
915
+ conversationExternalId,
916
+ canonicalSenderId ?? rawSenderId,
917
+ );
914
918
  try {
915
919
  await deliverChannelReply(replyCallbackUrl, replyPayload);
916
920
  replyDelivered = true;
@@ -969,10 +973,11 @@ export async function handleChannelInbound({
969
973
  text: DISK_PRESSURE_REMOTE_BLOCK_REPLY,
970
974
  assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
971
975
  };
972
- if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
973
- replyPayload.ephemeral = true;
974
- replyPayload.user = (canonicalSenderId ?? rawSenderId)!;
975
- }
976
+ replyPayload.audience = audienceForReader(
977
+ sourceChannel,
978
+ conversationExternalId,
979
+ canonicalSenderId ?? rawSenderId,
980
+ );
976
981
  try {
977
982
  await deliverChannelReply(replyCallbackUrl, replyPayload);
978
983
  } catch (err) {
@@ -1189,15 +1194,16 @@ export async function handleChannelInbound({
1189
1194
  }
1190
1195
  }
1191
1196
 
1192
- // On Slack, edit the original approval message to remove stale buttons
1193
- // and deliver an ephemeral error so the user gets visible feedback
1197
+ // Edit the original approval message to remove stale buttons
1198
+ // and reply so the user gets visible feedback
1194
1199
  // instead of a silent no-op (JARVIS-299).
1195
- if (sourceChannel === "slack" && replyCallbackUrl && approvalMessageTs) {
1196
- deliverChannelReply(replyCallbackUrl, {
1200
+ // No channel check: a transport without `edit` declines the call, so a
1201
+ // channel gains this the moment it can revise a sent message.
1202
+ if (replyCallbackUrl && approvalMessageTs) {
1203
+ editChannelMessage(replyCallbackUrl, {
1197
1204
  chatId: conversationExternalId,
1205
+ messageId: approvalMessageTs,
1198
1206
  text: "This approval request has been resolved.",
1199
- messageTs: approvalMessageTs,
1200
- assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
1201
1207
  }).catch((err) => {
1202
1208
  log.error(
1203
1209
  { err, conversationId: result.conversationId },
@@ -1537,7 +1543,7 @@ function countSlackMetaMessages(conversationId: string): number {
1537
1543
  while (offset < SLACK_DM_CANDIDATE_MAX_SCAN) {
1538
1544
  const remaining = SLACK_DM_CANDIDATE_MAX_SCAN - offset;
1539
1545
  const batchLimit = Math.min(SLACK_DM_CANDIDATE_BATCH_SIZE, remaining);
1540
- const candidates = selectSlackMetaCandidateMetadata(
1546
+ const candidates = selectProviderMetaCandidateMetadata(
1541
1547
  conversationId,
1542
1548
  batchLimit,
1543
1549
  offset,
@@ -1676,6 +1682,10 @@ function readStoredSlackThreadState(
1676
1682
  * content.
1677
1683
  * Caller is responsible for dedup checks before invoking; this helper
1678
1684
  * performs no idempotency check itself.
1685
+ *
1686
+ * Returns false when the row was refused rather than written. Callers mark
1687
+ * the channel ts seen either way, so a refused body is not reconsidered on
1688
+ * the next pass.
1679
1689
  */
1680
1690
  async function persistBackfilledSlackMessage(params: {
1681
1691
  conversationId: string;
@@ -1683,8 +1693,27 @@ async function persistBackfilledSlackMessage(params: {
1683
1693
  message: ProviderMessage;
1684
1694
  account?: string;
1685
1695
  guardianExternalUserId?: string;
1686
- }): Promise<void> {
1696
+ }): Promise<boolean> {
1687
1697
  const { message } = params;
1698
+
1699
+ // Backfill writes externally-authored bodies into a conversation, so it
1700
+ // carries the same secret gate as live channel ingress
1701
+ // (`runSecretIngressCheck`). Runs before file hydration, so a refused row
1702
+ // costs no downloads.
1703
+ const secretScan = checkIngressForSecrets(message.text ?? "");
1704
+ if (secretScan.blocked) {
1705
+ log.warn(
1706
+ {
1707
+ conversationId: params.conversationId,
1708
+ channelId: params.channelId,
1709
+ channelTs: message.id,
1710
+ detectedTypes: secretScan.detectedTypes,
1711
+ },
1712
+ "Skipping backfilled Slack message: secret detected",
1713
+ );
1714
+ return false;
1715
+ }
1716
+
1688
1717
  const slackFilesWithUrls = readSlackFilesWithUrlsFromProviderMetadata(
1689
1718
  message.metadata,
1690
1719
  );
@@ -1736,9 +1765,19 @@ async function persistBackfilledSlackMessage(params: {
1736
1765
 
1737
1766
  const rawText = message.text ?? "";
1738
1767
 
1768
+ // `sentAt` is when the message was sent; the row's `createdAt` is when the
1769
+ // import wrote it. History serialization prefers `sentAt` for the display
1770
+ // timestamp, so setting it is what keeps weeks-old history from reading as
1771
+ // having just arrived. The Slack adapter already derives this from the
1772
+ // message ts, so no parsing happens here.
1773
+ const sentAt = Number.isFinite(message.timestamp)
1774
+ ? message.timestamp
1775
+ : undefined;
1776
+
1739
1777
  const persisted = await addMessage(params.conversationId, role, rawText, {
1740
1778
  metadata: {
1741
1779
  slackMeta: writeSlackMetadata(slackMeta),
1780
+ ...(sentAt !== undefined ? { sentAt } : {}),
1742
1781
  provenanceTrustClass: isGuardian ? "guardian" : "unknown",
1743
1782
  provenanceSourceChannel: "slack",
1744
1783
  ...(params.guardianExternalUserId
@@ -1761,7 +1800,7 @@ async function persistBackfilledSlackMessage(params: {
1761
1800
  f.mimetype.startsWith("image/"),
1762
1801
  );
1763
1802
  if (imageFiles.length === 0) {
1764
- return;
1803
+ return true;
1765
1804
  }
1766
1805
 
1767
1806
  const hydratedAttachments = await withSlackBotToken(
@@ -1832,7 +1871,7 @@ async function persistBackfilledSlackMessage(params: {
1832
1871
  { conversationId: params.conversationId, channelTs: message.id },
1833
1872
  "No Slack token available for backfill image hydration; skipping",
1834
1873
  );
1835
- return;
1874
+ return true;
1836
1875
  }
1837
1876
 
1838
1877
  if (hydratedAttachments.length > 0) {
@@ -1843,6 +1882,7 @@ async function persistBackfilledSlackMessage(params: {
1843
1882
  ),
1844
1883
  );
1845
1884
  }
1885
+ return true;
1846
1886
  }
1847
1887
 
1848
1888
  async function buildBackfilledSlackContentBlocks(
@@ -2079,7 +2119,7 @@ async function runBackfillSlackDmIfCold(params: {
2079
2119
  continue;
2080
2120
  }
2081
2121
  try {
2082
- await persistBackfilledSlackMessage({
2122
+ const stored = await persistBackfilledSlackMessage({
2083
2123
  conversationId: params.conversationId,
2084
2124
  channelId: params.channelId,
2085
2125
  message,
@@ -2089,7 +2129,9 @@ async function runBackfillSlackDmIfCold(params: {
2089
2129
  : {}),
2090
2130
  });
2091
2131
  seen.add(message.id);
2092
- written++;
2132
+ if (stored) {
2133
+ written++;
2134
+ }
2093
2135
  } catch (perRowErr) {
2094
2136
  log.warn(
2095
2137
  {
@@ -2677,7 +2719,7 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
2677
2719
  continue;
2678
2720
  }
2679
2721
  try {
2680
- await persistBackfilledSlackMessage({
2722
+ const stored = await persistBackfilledSlackMessage({
2681
2723
  conversationId,
2682
2724
  channelId,
2683
2725
  message,
@@ -2685,7 +2727,9 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
2685
2727
  ...(guardianExternalUserId ? { guardianExternalUserId } : {}),
2686
2728
  });
2687
2729
  threadState.storedChannelTs.add(message.id);
2688
- persisted++;
2730
+ if (stored) {
2731
+ persisted++;
2732
+ }
2689
2733
  } catch (err) {
2690
2734
  log.warn(
2691
2735
  { err, conversationId, channelId, threadTs, channelTs: message.id },
@@ -19,6 +19,7 @@ import {
19
19
  getPendingSession,
20
20
  resolveBootstrapToken,
21
21
  } from "../../../channels/gateway-verification-sessions.js";
22
+ import { audienceForReader } from "../../../channels/message-audience.js";
22
23
  import type { ChannelId } from "../../../channels/types.js";
23
24
  import {
24
25
  channelStatusToMemberStatus,
@@ -595,11 +596,11 @@ export async function enforceIngressAcl(
595
596
  text: replyText,
596
597
  assistantId,
597
598
  };
598
- // On Slack, send as ephemeral so only the requester sees the rejection
599
- if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
600
- replyPayload.ephemeral = true;
601
- replyPayload.user = (canonicalSenderId ?? rawSenderId)!;
602
- }
599
+ replyPayload.audience = audienceForReader(
600
+ sourceChannel,
601
+ conversationExternalId,
602
+ canonicalSenderId ?? rawSenderId,
603
+ );
603
604
  try {
604
605
  await deliverChannelReply(replyCallbackUrl, replyPayload);
605
606
  replyDelivered = true;
@@ -847,14 +848,11 @@ export async function enforceIngressAcl(
847
848
  text: inactiveReplyText,
848
849
  assistantId,
849
850
  };
850
- // On Slack, send as ephemeral so only the requester sees the rejection
851
- if (
852
- sourceChannel === "slack" &&
853
- (canonicalSenderId ?? rawSenderId)
854
- ) {
855
- inactiveReplyPayload.ephemeral = true;
856
- inactiveReplyPayload.user = (canonicalSenderId ?? rawSenderId)!;
857
- }
851
+ inactiveReplyPayload.audience = audienceForReader(
852
+ sourceChannel,
853
+ conversationExternalId,
854
+ canonicalSenderId ?? rawSenderId,
855
+ );
858
856
  try {
859
857
  await deliverChannelReply(replyCallbackUrl, inactiveReplyPayload);
860
858
  inactiveReplyDelivered = true;
@@ -891,10 +889,11 @@ export async function enforceIngressAcl(
891
889
  text: denyReplyText,
892
890
  assistantId,
893
891
  };
894
- if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
895
- denyPayload.ephemeral = true;
896
- denyPayload.user = (canonicalSenderId ?? rawSenderId)!;
897
- }
892
+ denyPayload.audience = audienceForReader(
893
+ sourceChannel,
894
+ conversationExternalId,
895
+ canonicalSenderId ?? rawSenderId,
896
+ );
898
897
  try {
899
898
  await deliverChannelReply(replyCallbackUrl, denyPayload);
900
899
  denyReplyDelivered = true;
@@ -88,6 +88,42 @@ mock.module("../../gateway-client.js", () => ({
88
88
  },
89
89
  }));
90
90
 
91
+ const sentReactions: Array<{
92
+ callbackUrl: string;
93
+ target: Record<string, unknown>;
94
+ }> = [];
95
+ const sentStreamOps: Array<Record<string, unknown>> = [];
96
+ let sendChannelStreamOpImpl: (
97
+ op: Record<string, unknown>,
98
+ ) => Promise<{ ok: boolean; ts?: string }> = async () => ({ ok: true });
99
+ const sentThreadStatuses: Array<Record<string, unknown>> = [];
100
+ mock.module("../../../messaging/providers/index.js", () => ({
101
+ sendChannelTyping: async () => ({ ok: true }),
102
+ supportsChannelTyping: () => false,
103
+ sendChannelStreamOp: async (
104
+ _callbackUrl: string,
105
+ _chatId: string,
106
+ op: Record<string, unknown>,
107
+ ) => {
108
+ sentStreamOps.push(op);
109
+ return sendChannelStreamOpImpl(op);
110
+ },
111
+ setChannelThreadStatus: async (
112
+ _callbackUrl: string,
113
+ status: Record<string, unknown>,
114
+ ) => {
115
+ sentThreadStatuses.push(status);
116
+ return { ok: true };
117
+ },
118
+ sendChannelReaction: async (
119
+ callbackUrl: string,
120
+ target: Record<string, unknown>,
121
+ ) => {
122
+ sentReactions.push({ callbackUrl, target });
123
+ return { ok: true };
124
+ },
125
+ }));
126
+
91
127
  mock.module("../../channel-reply-delivery.js", () => ({
92
128
  deliverReplyViaCallback: async (...args: unknown[]) => {
93
129
  const options = args[4] as
@@ -127,6 +163,10 @@ beforeEach(() => {
127
163
  __resetChannelTurnAdmissionForTests();
128
164
  clearConversations();
129
165
  deliveredChannelReplies.length = 0;
166
+ sentReactions.length = 0;
167
+ sentStreamOps.length = 0;
168
+ sendChannelStreamOpImpl = async () => ({ ok: true });
169
+ sentThreadStatuses.length = 0;
130
170
  markedProcessedEvents.length = 0;
131
171
  processingFailureEvents.length = 0;
132
172
  retryableFailureEvents.length = 0;
@@ -144,10 +184,7 @@ beforeEach(() => {
144
184
  deliverReplyViaCallbackImpl = async () => {};
145
185
  });
146
186
 
147
- const slackStreamOps = (): Array<Record<string, unknown>> =>
148
- deliveredChannelReplies
149
- .map((entry) => entry.payload.slackStream as Record<string, unknown>)
150
- .filter(Boolean);
187
+ const slackStreamOps = (): Array<Record<string, unknown>> => sentStreamOps;
151
188
 
152
189
  describe("isBoundGuardianActor", () => {
153
190
  test("returns true only when requester matches bound guardian", () => {
@@ -492,7 +529,7 @@ describe("processChannelMessageInBackground — reply delivery", () => {
492
529
  const channelId = "D-STREAMED";
493
530
  const threadTs = "1700000000.000044";
494
531
  const streamTs = "1700000000.000033";
495
- deliverChannelReplyImpl = async () => ({ ok: true, ts: streamTs });
532
+ sendChannelStreamOpImpl = async () => ({ ok: true, ts: streamTs });
496
533
 
497
534
  const processMessage: MessageProcessor = async (
498
535
  _conversationId,
@@ -671,7 +708,7 @@ describe("processChannelMessageInBackground — reply delivery", () => {
671
708
  const channelId = "D-STREAM-PROCESSING-FAILURE";
672
709
  const threadTs = "1700000000.000066";
673
710
  const streamTs = "1700000000.000077";
674
- deliverChannelReplyImpl = async () => ({ ok: true, ts: streamTs });
711
+ sendChannelStreamOpImpl = async () => ({ ok: true, ts: streamTs });
675
712
 
676
713
  const processMessage: MessageProcessor = async (
677
714
  _conversationId,
@@ -925,11 +962,12 @@ describe("Slack thinking status timing", () => {
925
962
  const messageTs = "1700000000.000010";
926
963
 
927
964
  const processMessage: MessageProcessor = async () => {
928
- expect(deliveredChannelReplies).toHaveLength(1);
929
- expect(deliveredChannelReplies[0]!.payload.reaction).toEqual({
965
+ expect(sentReactions).toHaveLength(1);
966
+ expect(sentReactions[0]!.target).toEqual({
967
+ chatId: channelId,
968
+ messageId: messageTs,
969
+ emoji: "eyes",
930
970
  action: "add",
931
- name: "eyes",
932
- messageTs,
933
971
  });
934
972
  return { messageId: "user-msg-dm-immediate" };
935
973
  };
@@ -950,12 +988,14 @@ describe("Slack thinking status timing", () => {
950
988
 
951
989
  await flush();
952
990
 
953
- const reactions = deliveredChannelReplies.map(
954
- (entry) => entry.payload.reaction,
955
- );
956
- expect(reactions).toEqual([
957
- { action: "add", name: "eyes", messageTs },
958
- { action: "remove", name: "eyes", messageTs },
991
+ expect(sentReactions.map((entry) => entry.target)).toEqual([
992
+ { chatId: channelId, messageId: messageTs, emoji: "eyes", action: "add" },
993
+ {
994
+ chatId: channelId,
995
+ messageId: messageTs,
996
+ emoji: "eyes",
997
+ action: "remove",
998
+ },
959
999
  ]);
960
1000
  });
961
1001
 
@@ -965,17 +1005,14 @@ describe("Slack thinking status timing", () => {
965
1005
  const threadTs = "1700000000.000011";
966
1006
 
967
1007
  const processMessage: MessageProcessor = async () => {
968
- expect(deliveredChannelReplies).toHaveLength(1);
969
- expect(deliveredChannelReplies[0]!.payload.assistantThreadStatus).toEqual(
970
- {
971
- channel: channelId,
972
- threadTs,
973
- status: expect.any(String),
974
- loadingMessages: ["Thinking\u2026"],
975
- },
976
- );
977
- const threadStatus = deliveredChannelReplies[0]!.payload
978
- .assistantThreadStatus as { status: string };
1008
+ expect(sentThreadStatuses).toHaveLength(1);
1009
+ expect(sentThreadStatuses[0]).toEqual({
1010
+ chatId: channelId,
1011
+ threadTs,
1012
+ status: expect.any(String),
1013
+ loadingMessages: ["Thinking\u2026"],
1014
+ });
1015
+ const threadStatus = sentThreadStatuses[0] as { status: string };
979
1016
  expect(slackStatusLabels).toContain(threadStatus.status);
980
1017
  return { messageId: "user-msg-mention-immediate" };
981
1018
  };
@@ -996,12 +1033,9 @@ describe("Slack thinking status timing", () => {
996
1033
 
997
1034
  await flush();
998
1035
 
999
- const statuses = deliveredChannelReplies.map((entry) => {
1000
- const status = entry.payload.assistantThreadStatus as
1001
- | { status?: string }
1002
- | undefined;
1003
- return status?.status;
1004
- });
1036
+ const statuses = sentThreadStatuses.map(
1037
+ (entry) => (entry as { status?: string }).status,
1038
+ );
1005
1039
  expect(slackStatusLabels).toContain(statuses[0]!);
1006
1040
  expect(statuses[1]).toBe("");
1007
1041
  });
@@ -1082,12 +1116,9 @@ describe("Slack thinking status timing", () => {
1082
1116
 
1083
1117
  await flush();
1084
1118
 
1085
- const statuses = deliveredChannelReplies.map((entry) => {
1086
- const status = entry.payload.assistantThreadStatus as
1087
- | { status?: string }
1088
- | undefined;
1089
- return status?.status;
1090
- });
1119
+ const statuses = sentThreadStatuses.map(
1120
+ (entry) => (entry as { status?: string }).status,
1121
+ );
1091
1122
  expect(slackStatusLabels).toContain(statuses[0]!);
1092
1123
  expect(statuses[1]).toBe("");
1093
1124
  });
@@ -1144,18 +1175,16 @@ describe("Slack thinking status timing", () => {
1144
1175
 
1145
1176
  await flush();
1146
1177
 
1147
- const statuses = deliveredChannelReplies.map(
1148
- (entry) => entry.payload.assistantThreadStatus,
1149
- );
1178
+ const statuses = sentThreadStatuses;
1150
1179
  expect(statuses).toEqual([
1151
1180
  {
1152
- channel: channelId,
1181
+ chatId: channelId,
1153
1182
  threadTs,
1154
1183
  status: expect.any(String),
1155
1184
  loadingMessages: ["In progress (1/2): Search docs"],
1156
1185
  },
1157
1186
  {
1158
- channel: channelId,
1187
+ chatId: channelId,
1159
1188
  threadTs,
1160
1189
  status: "",
1161
1190
  },
@@ -1278,24 +1307,22 @@ describe("Slack thinking status timing", () => {
1278
1307
 
1279
1308
  await flush();
1280
1309
 
1281
- const statuses = deliveredChannelReplies.map(
1282
- (entry) => entry.payload.assistantThreadStatus,
1283
- );
1310
+ const statuses = sentThreadStatuses;
1284
1311
  expect(statuses).toEqual([
1285
1312
  {
1286
- channel: channelId,
1313
+ chatId: channelId,
1287
1314
  threadTs,
1288
1315
  status: expect.any(String),
1289
1316
  loadingMessages: ["In progress (1/2): Read request"],
1290
1317
  },
1291
1318
  {
1292
- channel: channelId,
1319
+ chatId: channelId,
1293
1320
  threadTs,
1294
1321
  status: expect.any(String),
1295
1322
  loadingMessages: ["In progress (2/2): Write answer"],
1296
1323
  },
1297
1324
  {
1298
- channel: channelId,
1325
+ chatId: channelId,
1299
1326
  threadTs,
1300
1327
  status: "",
1301
1328
  },