@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
@@ -37,7 +37,6 @@ import {
37
37
  import {
38
38
  check,
39
39
  classifyRisk,
40
- generateAllowlistOptions,
41
40
  generateScopeOptions,
42
41
  } from "../../permissions/checker.js";
43
42
  import { resolveGuardianPersonaPath } from "../../prompts/persona-resolver.js";
@@ -710,19 +709,22 @@ async function handleToolPermissionSimulate({ body = {} }: RouteHandlerArgs) {
710
709
  isInteractive === false ? "headless" : "conversation";
711
710
  const policyContext = { executionTarget, executionContext } as const;
712
711
 
713
- const { level: riskLevel } = await classifyRisk(
712
+ const classification = await classifyRisk(
714
713
  toolName,
715
714
  input,
716
715
  workingDir,
717
716
  undefined,
718
717
  manifestOverride,
719
718
  );
719
+ const riskLevel = classification.level;
720
720
  const result = await check(
721
721
  toolName,
722
722
  input,
723
723
  workingDir,
724
724
  policyContext,
725
725
  manifestOverride,
726
+ undefined,
727
+ classification,
726
728
  );
727
729
 
728
730
  if (isInteractive === false && result.decision === "prompt") {
@@ -743,11 +745,9 @@ async function handleToolPermissionSimulate({ body = {} }: RouteHandlerArgs) {
743
745
  | undefined;
744
746
 
745
747
  if (result.decision === "prompt") {
746
- const allowlistOptions = await generateAllowlistOptions(toolName, input);
747
- const scopeOptions = generateScopeOptions(workingDir, toolName);
748
748
  promptPayload = {
749
- allowlistOptions,
750
- scopeOptions,
749
+ allowlistOptions: classification.allowlistOptions ?? [],
750
+ scopeOptions: generateScopeOptions(workingDir, toolName),
751
751
  persistentDecisionsAllowed: true,
752
752
  };
753
753
  }
@@ -37,26 +37,25 @@ const log = getLogger("surface-action-routes");
37
37
 
38
38
  /**
39
39
  * Resolve trust context for the actor principal from the gateway guardian
40
- * binding and set it on the conversation. A vellum principal is the guardian or
41
- * nobody, so the mapper yields guardian or unknown. Resolution (including the
42
- * dev-bypass principal translation and the mutating reset-drift repair) lives
43
- * in the shared {@link resolveVellumActorTrustContext} so the surface content
44
- * route's read-only requester scoping cannot drift from it.
40
+ * binding, and return it for the caller to attribute this action with. A vellum
41
+ * principal is the guardian or nobody, so the mapper yields guardian or
42
+ * unknown. Resolution (including the dev-bypass principal translation and the
43
+ * mutating reset-drift repair) lives in the shared
44
+ * {@link resolveVellumActorTrustContext} so the surface content route's
45
+ * read-only requester scoping cannot drift from it.
46
+ *
47
+ * Resolution only: the requester travels to `handleSurfaceAction` as an
48
+ * argument, and the conversation's slot is stamped there, at the point the
49
+ * click is committed to starting a turn. Stamping here instead would repoint
50
+ * the actor of a turn already in flight, or of a turn that starts between this
51
+ * call and the dispatch.
45
52
  */
46
- async function applyTrustContext(
47
- conversation: {
48
- setTrustContext?(ctx: TrustContext): void;
49
- },
53
+ async function resolveRequesterTrustContext(
50
54
  actorPrincipalId: string | undefined,
51
- ): Promise<void> {
52
- if (!conversation.setTrustContext) {
53
- return;
54
- }
55
- conversation.setTrustContext(
56
- await resolveVellumActorTrustContext(actorPrincipalId, {
57
- healResetDrift: true,
58
- }),
59
- );
55
+ ): Promise<TrustContext> {
56
+ return resolveVellumActorTrustContext(actorPrincipalId, {
57
+ healResetDrift: true,
58
+ });
60
59
  }
61
60
 
62
61
  // ---------------------------------------------------------------------------
@@ -154,7 +153,8 @@ async function handleSurfaceAction({
154
153
  }
155
154
 
156
155
  const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
157
- await applyTrustContext(conversation, actorPrincipalId);
156
+ const requesterTrustContext =
157
+ await resolveRequesterTrustContext(actorPrincipalId);
158
158
 
159
159
  // Translate dev-bypass → real guardian so the surface turn's principal matches
160
160
  // the SSE host-proxy client's registered principal; otherwise CU/app-control
@@ -170,6 +170,7 @@ async function handleSurfaceAction({
170
170
  actionId,
171
171
  data,
172
172
  resolvedActorPrincipalId,
173
+ requesterTrustContext,
173
174
  );
174
175
  const result =
175
176
  raw && typeof raw === "object" && "accepted" in raw
@@ -37,6 +37,7 @@
37
37
 
38
38
  import { statSync } from "node:fs";
39
39
 
40
+ import { runInPluginContext } from "../../plugins/plugin-execution-context.js";
40
41
  import { isRouteHostEnabled } from "../../routes/control.js";
41
42
  import {
42
43
  RouteHostClient,
@@ -55,6 +56,7 @@ import {
55
56
  routeSourceRoot,
56
57
  } from "./user-route-import.js";
57
58
  import {
59
+ pluginNameForRoutePath,
58
60
  resolveHandlerFile,
59
61
  resolveRouteLocation,
60
62
  } from "./user-route-resolution.js";
@@ -150,11 +152,36 @@ export class UserRouteDispatcher {
150
152
  );
151
153
  }
152
154
 
155
+ // A plugin's own routes execute as that plugin, so the host APIs a handler
156
+ // reaches scope to it exactly as they do inside its hooks and tools. Both
157
+ // execution paths carry it: in-thread here, and the route host through the
158
+ // invoke params it marks its own execution with.
159
+ const pluginName = pluginNameForRoutePath(routePath);
160
+
153
161
  if (isRouteHostEnabled()) {
154
- return this.dispatchViaHost(filePath, routePath, request);
162
+ return this.dispatchViaHost(filePath, routePath, request, pluginName);
155
163
  }
156
164
 
157
- const mod = await this.loadModule(filePath, location.routesDir);
165
+ // The context covers module evaluation, not just the handler call. A route
166
+ // module can reach a scoped host API at import time (a top-level `await`),
167
+ // and that import happens once per mtime, so a load left outside the
168
+ // context would take the unscoped branch and never be re-run scoped.
169
+ const serve = () =>
170
+ this.serveInThread(filePath, location.routesDir, routePath, request);
171
+ return pluginName ? runInPluginContext(pluginName, serve) : serve();
172
+ }
173
+
174
+ /**
175
+ * Load the handler module and run the method's handler in this thread.
176
+ * Called inside the owning plugin's execution context, when the route has one.
177
+ */
178
+ private async serveInThread(
179
+ filePath: string,
180
+ routesDir: string,
181
+ routePath: string,
182
+ request: Request,
183
+ ): Promise<Response> {
184
+ const mod = await this.loadModule(filePath, routesDir);
158
185
  const method = request.method as HttpMethod;
159
186
  const handler = mod.handlers[method];
160
187
 
@@ -180,6 +207,7 @@ export class UserRouteDispatcher {
180
207
  filePath: string,
181
208
  routePath: string,
182
209
  request: Request,
210
+ pluginName: string | undefined,
183
211
  ): Promise<Response> {
184
212
  const mtimeMs = statSync(filePath).mtimeMs;
185
213
 
@@ -201,6 +229,7 @@ export class UserRouteDispatcher {
201
229
  method: request.method,
202
230
  url: request.url,
203
231
  headers,
232
+ pluginName,
204
233
  },
205
234
  body,
206
235
  );
@@ -66,6 +66,23 @@ export interface RouteLocation {
66
66
  subPath: string;
67
67
  }
68
68
 
69
+ /**
70
+ * The manifest name of the plugin whose namespace an `/x/` route path falls in,
71
+ * or undefined for a workspace route.
72
+ *
73
+ * The dispatcher marks that plugin as in context for the handler's execution,
74
+ * so host APIs a route handler reaches (`resolveCredential`,
75
+ * `indexDocument`) scope to the owning plugin exactly as they do inside its
76
+ * hooks and tools.
77
+ */
78
+ export function pluginNameForRoutePath(routePath: string): string | undefined {
79
+ const segments = routePath.split("/");
80
+ if (segments[0] !== PLUGIN_ROUTE_SEGMENT) {
81
+ return undefined;
82
+ }
83
+ return segments[1] || undefined;
84
+ }
85
+
69
86
  /**
70
87
  * Resolve an `/x/` route path to the base directory + sub-path the handler file
71
88
  * is looked up under.
@@ -87,7 +104,7 @@ export interface RouteLocation {
87
104
  export function resolveRouteLocation(routePath: string): RouteLocation | null {
88
105
  const segments = routePath.split("/");
89
106
  if (segments[0] === PLUGIN_ROUTE_SEGMENT) {
90
- const pluginName = segments[1];
107
+ const pluginName = pluginNameForRoutePath(routePath);
91
108
  if (!pluginName) {
92
109
  return null;
93
110
  }
@@ -14,6 +14,22 @@ let deliverImpl: (
14
14
  ts: "stream-ts-1",
15
15
  });
16
16
 
17
+ const sentStreamOps: Array<Record<string, unknown>> = [];
18
+ let streamOpImpl: (op: Record<string, unknown>) => Promise<{
19
+ ok: boolean;
20
+ ts?: string;
21
+ }> = async () => ({ ok: true, ts: "stream-ts-1" });
22
+ mock.module("../messaging/providers/index.js", () => ({
23
+ sendChannelStreamOp: async (
24
+ _callbackUrl: string,
25
+ _chatId: string,
26
+ op: Record<string, unknown>,
27
+ ) => {
28
+ sentStreamOps.push(op);
29
+ return streamOpImpl(op);
30
+ },
31
+ }));
32
+
17
33
  mock.module("./gateway-client.js", () => ({
18
34
  deliverChannelReply: async (
19
35
  callbackUrl: string,
@@ -96,10 +112,7 @@ const taskProgressUpdate = (
96
112
  const tick = (ms: number): Promise<void> =>
97
113
  new Promise((resolve) => setTimeout(resolve, ms));
98
114
 
99
- const slackStreamOps = (): Array<Record<string, unknown>> =>
100
- deliverCalls
101
- .map((call) => call.payload.slackStream as Record<string, unknown>)
102
- .filter(Boolean);
115
+ const slackStreamOps = (): Array<Record<string, unknown>> => sentStreamOps;
103
116
 
104
117
  const streamedMarkdown = (): string =>
105
118
  slackStreamOps()
@@ -108,6 +121,8 @@ const streamedMarkdown = (): string =>
108
121
 
109
122
  beforeEach(() => {
110
123
  deliverCalls.length = 0;
124
+ sentStreamOps.length = 0;
125
+ streamOpImpl = async () => ({ ok: true, ts: "stream-ts-1" });
111
126
  deliverImpl = async () => ({ ok: true, ts: "stream-ts-1" });
112
127
  });
113
128
 
@@ -314,7 +329,7 @@ describe("createSlackReplySession", () => {
314
329
  });
315
330
 
316
331
  test("falls back when startStream returns no stream ts", async () => {
317
- deliverImpl = async () => ({ ok: false });
332
+ streamOpImpl = async () => ({ ok: false });
318
333
  const session = createSlackReplySession({
319
334
  sourceChannel: "slack",
320
335
  chatType: "im",
@@ -331,7 +346,7 @@ describe("createSlackReplySession", () => {
331
346
  });
332
347
 
333
348
  test("falls back when startStream throws", async () => {
334
- deliverImpl = async () => {
349
+ streamOpImpl = async () => {
335
350
  throw new Error("rate limited");
336
351
  };
337
352
  const session = createSlackReplySession({
@@ -349,8 +364,7 @@ describe("createSlackReplySession", () => {
349
364
  });
350
365
 
351
366
  test("falls back when stopStream throws after streaming text", async () => {
352
- deliverImpl = async (_url, payload) => {
353
- const op = payload.slackStream as { action: string };
367
+ streamOpImpl = async (op) => {
354
368
  if (op.action === "stop") {
355
369
  throw new Error("stop failed");
356
370
  }
@@ -482,11 +496,7 @@ describe("createSlackReplySession", () => {
482
496
  });
483
497
 
484
498
  test("leaves progress to stop when the task-only append fails", async () => {
485
- deliverImpl = async (_url, payload) => {
486
- const op = payload.slackStream as {
487
- action: string;
488
- markdownText?: string;
489
- };
499
+ streamOpImpl = async (op) => {
490
500
  if (op.action === "append" && op.markdownText === undefined) {
491
501
  throw new Error("chunks-only append rejected");
492
502
  }
@@ -1,4 +1,4 @@
1
- import type { SlackStreamTask } from "@vellumai/gateway-client";
1
+ import type { SlackStreamOp, SlackStreamTask } from "@vellumai/gateway-client";
2
2
 
3
3
  import type { AssistantEvent } from "../api/index.js";
4
4
  import {
@@ -10,11 +10,11 @@ import {
10
10
  incompleteVellumLinkSuffixLength,
11
11
  stripVellumLinks,
12
12
  } from "../daemon/assistant-attachments.js";
13
+ import { sendChannelStreamOp } from "../messaging/providers/index.js";
13
14
  import { SLACK_STREAM_MARKDOWN_LIMIT } from "../messaging/providers/slack/api.js";
14
15
  import { renderSlackBlocks } from "../messaging/providers/slack/render.js";
15
16
  import { getLogger } from "../util/logger.js";
16
17
  import { needsBoundarySpace } from "../util/text-spacing.js";
17
- import { deliverChannelReply } from "./gateway-client.js";
18
18
  import {
19
19
  hasDeliverableAssistantText,
20
20
  NO_RESPONSE_INLINE_RE,
@@ -113,7 +113,6 @@ export function createSlackReplySession(params: {
113
113
  chatType?: string;
114
114
  replyCallbackUrl?: string;
115
115
  chatId: string;
116
- assistantId?: string;
117
116
  /** Slack user ID of the reader; required to stream into a channel. */
118
117
  recipientUserId?: string;
119
118
  /** Slack team ID of the reader; required to stream into a channel. */
@@ -132,7 +131,7 @@ export function createSlackReplySession(params: {
132
131
  if (!shouldStreamSlackReply(params) || !params.replyCallbackUrl) {
133
132
  return undefined;
134
133
  }
135
- const { chatId, assistantId, recipientUserId, recipientTeamId } = params;
134
+ const { chatId, recipientUserId, recipientTeamId } = params;
136
135
  const coalesceMs = params.coalesceMs ?? STREAM_COALESCE_MS;
137
136
  const replyCallbackUrl = params.replyCallbackUrl;
138
137
  const threadTs = extractThreadTsFromCallbackUrl(replyCallbackUrl);
@@ -197,11 +196,11 @@ export function createSlackReplySession(params: {
197
196
  ): string | undefined =>
198
197
  tasks ? JSON.stringify({ title, tasks }) : undefined;
199
198
 
199
+ // Typed from the stream operation these are passed to, so the element type
200
+ // follows that contract rather than drifting from it.
200
201
  const imageBlocks = (
201
202
  text: string,
202
- ):
203
- | NonNullable<Parameters<typeof deliverChannelReply>[1]["blocks"]>
204
- | undefined => {
203
+ ): Extract<SlackStreamOp, { action: "stop" }>["blocks"] => {
205
204
  const blocks = renderSlackBlocks(text)?.filter(
206
205
  (block) => block.type === "image",
207
206
  );
@@ -222,24 +221,20 @@ export function createSlackReplySession(params: {
222
221
  const title = activeProgress?.title;
223
222
  const tasks = planTasks();
224
223
  try {
225
- const result = await deliverChannelReply(replyCallbackUrl, {
226
- chatId,
227
- assistantId,
228
- slackStream: {
229
- action: "start",
230
- threadTs,
231
- markdownText: firstChunk,
232
- // The task display mode is fixed for the stream's lifetime at
233
- // start, while a `task_progress` surface usually appears only
234
- // after the first text flush has opened the stream. Plan mode
235
- // only affects how task chunks render, so a stream that never
236
- // carries tasks still reads as a plain message.
237
- taskDisplayMode: "plan" as const,
238
- ...(title ? { planTitle: title } : {}),
239
- ...(tasks ? { tasks } : {}),
240
- ...(recipientUserId ? { recipientUserId } : {}),
241
- ...(recipientTeamId ? { recipientTeamId } : {}),
242
- },
224
+ const result = await sendChannelStreamOp(replyCallbackUrl, chatId, {
225
+ action: "start",
226
+ threadTs,
227
+ markdownText: firstChunk,
228
+ // The task display mode is fixed for the stream's lifetime at
229
+ // start, while a `task_progress` surface usually appears only
230
+ // after the first text flush has opened the stream. Plan mode
231
+ // only affects how task chunks render, so a stream that never
232
+ // carries tasks still reads as a plain message.
233
+ taskDisplayMode: "plan" as const,
234
+ ...(title ? { planTitle: title } : {}),
235
+ ...(tasks ? { tasks } : {}),
236
+ ...(recipientUserId ? { recipientUserId } : {}),
237
+ ...(recipientTeamId ? { recipientTeamId } : {}),
243
238
  });
244
239
  if (result.ok && result.ts) {
245
240
  streamTs = result.ts;
@@ -287,16 +282,12 @@ export function createSlackReplySession(params: {
287
282
  confirmedLength + SLACK_STREAM_MARKDOWN_LIMIT,
288
283
  );
289
284
  try {
290
- await deliverChannelReply(replyCallbackUrl, {
291
- chatId,
292
- assistantId,
293
- slackStream: {
294
- action: "append",
295
- streamTs,
296
- markdownText: chunk,
297
- ...(title ? { planTitle: title } : {}),
298
- ...(tasks ? { tasks } : {}),
299
- },
285
+ await sendChannelStreamOp(replyCallbackUrl, chatId, {
286
+ action: "append",
287
+ streamTs,
288
+ markdownText: chunk,
289
+ ...(title ? { planTitle: title } : {}),
290
+ ...(tasks ? { tasks } : {}),
300
291
  });
301
292
  confirmedLength += chunk.length;
302
293
  deliveredProgressKey = key ?? deliveredProgressKey;
@@ -316,15 +307,11 @@ export function createSlackReplySession(params: {
316
307
  // next text append and `stopStream` carries the final state.
317
308
  if (!taskOnlyAppendsDisabled && tasks && key !== deliveredProgressKey) {
318
309
  try {
319
- await deliverChannelReply(replyCallbackUrl, {
320
- chatId,
321
- assistantId,
322
- slackStream: {
323
- action: "append",
324
- streamTs,
325
- ...(title ? { planTitle: title } : {}),
326
- tasks,
327
- },
310
+ await sendChannelStreamOp(replyCallbackUrl, chatId, {
311
+ action: "append",
312
+ streamTs,
313
+ ...(title ? { planTitle: title } : {}),
314
+ tasks,
328
315
  });
329
316
  deliveredProgressKey = key;
330
317
  } catch (err) {
@@ -450,17 +437,13 @@ export function createSlackReplySession(params: {
450
437
  const title = activeProgress?.title;
451
438
  const tasks = planTasks();
452
439
  try {
453
- await deliverChannelReply(replyCallbackUrl, {
454
- chatId,
455
- assistantId,
456
- slackStream: {
457
- action: "stop",
458
- streamTs,
459
- ...(remaining.length > 0 ? { markdownText: remaining } : {}),
460
- ...(blocks ? { blocks } : {}),
461
- ...(title ? { planTitle: title } : {}),
462
- ...(tasks ? { tasks } : {}),
463
- },
440
+ await sendChannelStreamOp(replyCallbackUrl, chatId, {
441
+ action: "stop",
442
+ streamTs,
443
+ ...(remaining.length > 0 ? { markdownText: remaining } : {}),
444
+ ...(blocks ? { blocks } : {}),
445
+ ...(title ? { planTitle: title } : {}),
446
+ ...(tasks ? { tasks } : {}),
464
447
  });
465
448
  confirmedLength = clean.length;
466
449
  } catch (err) {
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Web presence policy.
3
+ *
4
+ * Answers exactly one question for notification producers: is the user
5
+ * demonstrably looking at this specific conversation in a web browser tab
6
+ * right now? Producers use the answer to skip an APNs push the user would
7
+ * see on screen anyway.
8
+ *
9
+ * Fails open by design, the same posture as desktop presence
10
+ * (`runtime/desktop-presence.ts`): presence is in-memory, best-effort, and
11
+ * reported by a client that can drop off (crash, network loss, backgrounded
12
+ * tab killed by the OS) without telling the daemon, so anything short of a
13
+ * fresh, visible, matching-conversation report answers `false` and the push
14
+ * goes out. A missed notification is strictly worse than a redundant one.
15
+ *
16
+ * Deliberately separate from desktop presence: a web report is scoped to one
17
+ * conversation (the tab's focused thread), not the whole app. The Electron
18
+ * desktop renderer detects its own host OS and tags its turns
19
+ * `macos`/`windows` (`detectClientOs()` in the web client), so an Electron
20
+ * turn is already covered by `isDesktopAttended()` and never reaches this
21
+ * gate. Do not fold this into `setClientPresence` / the `macos`-scoped
22
+ * desktop-attendance path.
23
+ */
24
+
25
+ import { getLogger } from "../util/logger.js";
26
+ import { assistantEventHub } from "./assistant-event-hub.js";
27
+
28
+ const log = getLogger("web-presence");
29
+
30
+ /**
31
+ * Shared semantic + transport freshness bound. The semantic report says what
32
+ * the tab was showing; `lastActiveAt` says the owning SSE connection is still
33
+ * alive. Both must be fresh so a live-but-hidden stream cannot preserve an old
34
+ * visible report, and a fresh report cannot suppress after its transport dies.
35
+ */
36
+ export const WEB_PRESENCE_STALE_AFTER_MS = 180_000;
37
+
38
+ export interface WebPresenceOptions {
39
+ /**
40
+ * Only count web clients whose verified actor principal matches this id.
41
+ * A client that connected without a principal (legacy or service token)
42
+ * never matches a supplied id.
43
+ */
44
+ actorPrincipalId?: string;
45
+ /** Clock used for the staleness comparison. */
46
+ now?: Date;
47
+ }
48
+
49
+ /**
50
+ * Whether some web client has reported, recently enough to trust, that the
51
+ * given conversation is visible and focused. Stale, absent, hidden,
52
+ * unfocused, non-web, and error reads all answer `false`.
53
+ *
54
+ * Callers whose notification targets one recipient must pass that
55
+ * recipient's `actorPrincipalId`, or another user's focused tab suppresses
56
+ * the push. Omitting it treats any matching web client as focus, which only
57
+ * suits notifications with no single recipient.
58
+ */
59
+ export function isWebConversationFocused(
60
+ conversationId: string,
61
+ options: WebPresenceOptions = {},
62
+ ): boolean {
63
+ const { actorPrincipalId, now = new Date() } = options;
64
+ try {
65
+ return assistantEventHub.listClientsByInterface("web").some((client) => {
66
+ if (
67
+ actorPrincipalId !== undefined &&
68
+ client.actorPrincipalId !== actorPrincipalId
69
+ ) {
70
+ return false;
71
+ }
72
+ const report = client.webPresence;
73
+ if (!report) {
74
+ return false;
75
+ }
76
+ const nowMs = now.getTime();
77
+ if (
78
+ nowMs - report.reportedAt.getTime() > WEB_PRESENCE_STALE_AFTER_MS ||
79
+ nowMs - client.lastActiveAt.getTime() > WEB_PRESENCE_STALE_AFTER_MS
80
+ ) {
81
+ return false;
82
+ }
83
+ return report.visible && report.focusedConversationId === conversationId;
84
+ });
85
+ } catch (err) {
86
+ // Returning false sends the push, which is the safe direction.
87
+ log.warn({ err }, "web presence read failed; treating as unfocused");
88
+ return false;
89
+ }
90
+ }