@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
@@ -3,7 +3,10 @@ import { join } from "node:path";
3
3
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
5
5
  import type { RouteInvokeParams } from "../../../routes/route-host-protocol.js";
6
- import { getWorkspaceRoutesDir } from "../../../util/platform.js";
6
+ import {
7
+ getWorkspacePluginsDir,
8
+ getWorkspaceRoutesDir,
9
+ } from "../../../util/platform.js";
7
10
 
8
11
  // The dispatcher constructs the route host client inline and reads the enabled
9
12
  // flag from config, so both are mocked here (there is no injection seam).
@@ -58,6 +61,16 @@ function writeHandler(name: string, content: string): void {
58
61
  writeFileSync(join(getWorkspaceRoutesDir(), name), content);
59
62
  }
60
63
 
64
+ function writePluginHandler(
65
+ pluginName: string,
66
+ name: string,
67
+ content: string,
68
+ ): void {
69
+ const dir = join(getWorkspacePluginsDir(), pluginName, "routes");
70
+ mkdirSync(dir, { recursive: true });
71
+ writeFileSync(join(dir, name), content);
72
+ }
73
+
61
74
  function jsonResponse(status: number, value: unknown): RouteInvokeResponse {
62
75
  return {
63
76
  status,
@@ -75,6 +88,7 @@ beforeEach(() => {
75
88
 
76
89
  afterEach(() => {
77
90
  rmSync(getWorkspaceRoutesDir(), { recursive: true, force: true });
91
+ rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
78
92
  });
79
93
 
80
94
  describe("UserRouteDispatcher — route host delegation", () => {
@@ -101,6 +115,43 @@ describe("UserRouteDispatcher — route host delegation", () => {
101
115
  expect(invokeCalls[0].params.url).toContain("/v1/x/foo");
102
116
  });
103
117
 
118
+ test("names the owning plugin so the host executes in its context", async () => {
119
+ // The host marks this plugin as in context for the handler, so
120
+ // plugin-scoped host APIs behave the same in the host and in-thread.
121
+ writePluginHandler(
122
+ "my-plugin",
123
+ "status.ts",
124
+ `export function GET() { return new Response(); }`,
125
+ );
126
+ hostEnabled = true;
127
+ const dispatcher = makeDispatcher();
128
+
129
+ await dispatcher.dispatch(
130
+ "plugins/my-plugin/status",
131
+ new Request("http://localhost/v1/x/plugins/my-plugin/status", {
132
+ method: "GET",
133
+ }),
134
+ );
135
+
136
+ expect(invokeCalls[0].params.pluginName).toBe("my-plugin");
137
+ });
138
+
139
+ test("leaves a workspace route unowned", async () => {
140
+ writeHandler(
141
+ "plain.ts",
142
+ `export function GET() { return new Response(); }`,
143
+ );
144
+ hostEnabled = true;
145
+ const dispatcher = makeDispatcher();
146
+
147
+ await dispatcher.dispatch(
148
+ "plain",
149
+ new Request("http://localhost/v1/x/plain", { method: "GET" }),
150
+ );
151
+
152
+ expect(invokeCalls[0].params.pluginName).toBeUndefined();
153
+ });
154
+
104
155
  test("runs in-band when disabled (host never called)", async () => {
105
156
  writeHandler(
106
157
  "bar.ts",
@@ -56,6 +56,15 @@ function writePluginHandler(
56
56
  return fullPath;
57
57
  }
58
58
 
59
+ /**
60
+ * Absolute specifier for the plugin execution context, so a handler written to
61
+ * a temp routes directory imports the same module instance this process holds.
62
+ */
63
+ const PLUGIN_CONTEXT_MODULE = new URL(
64
+ "../../../plugins/plugin-execution-context.ts",
65
+ import.meta.url,
66
+ ).href;
67
+
59
68
  async function readErrorBody(
60
69
  response: Response,
61
70
  ): Promise<{ error: { code: string; message: string } }> {
@@ -466,6 +475,71 @@ describe("plugin routes", () => {
466
475
  expect(body.plugin).toBe(true);
467
476
  });
468
477
 
478
+ test("executes the handler as the plugin that owns the namespace", async () => {
479
+ // Plugin-scoped host APIs (resolveCredential, indexDocument) read the
480
+ // execution context to decide what the caller may touch. A route handler
481
+ // that ran outside it would fall through to their unscoped branch and
482
+ // reach every plugin's credentials.
483
+ writePluginHandler(
484
+ "my-plugin",
485
+ "whoami.ts",
486
+ `import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
487
+ export function GET(request) {
488
+ return Response.json({ plugin: getCurrentPluginName() ?? null });
489
+ }`,
490
+ );
491
+
492
+ const dispatcher = makeDispatcher();
493
+ const res = await dispatcher.dispatch(
494
+ "plugins/my-plugin/whoami",
495
+ makeRequest("GET"),
496
+ );
497
+ expect(res.status).toBe(200);
498
+ const body = await res.json();
499
+ expect(body.plugin).toBe("my-plugin");
500
+ });
501
+
502
+ test("evaluates the route module in the plugin's context too", async () => {
503
+ // A module can reach a scoped host API at import time, and the import
504
+ // happens once per mtime. A load left outside the context would take the
505
+ // unscoped branch and never be re-run scoped, so the context has to cover
506
+ // module evaluation and not just the handler call.
507
+ writePluginHandler(
508
+ "my-plugin",
509
+ "at-import.ts",
510
+ `import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
511
+ const atImport = getCurrentPluginName() ?? null;
512
+ export function GET(request) {
513
+ return Response.json({ atImport });
514
+ }`,
515
+ );
516
+
517
+ const dispatcher = makeDispatcher();
518
+ const res = await dispatcher.dispatch(
519
+ "plugins/my-plugin/at-import",
520
+ makeRequest("GET"),
521
+ );
522
+ expect(res.status).toBe(200);
523
+ const body = await res.json();
524
+ expect(body.atImport).toBe("my-plugin");
525
+ });
526
+
527
+ test("leaves a workspace route outside any plugin context", async () => {
528
+ writeHandler(
529
+ "whoami.ts",
530
+ `import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
531
+ export function GET(request) {
532
+ return Response.json({ plugin: getCurrentPluginName() ?? null });
533
+ }`,
534
+ );
535
+
536
+ const dispatcher = makeDispatcher();
537
+ const res = await dispatcher.dispatch("whoami", makeRequest("GET"));
538
+ expect(res.status).toBe(200);
539
+ const body = await res.json();
540
+ expect(body.plugin).toBeNull();
541
+ });
542
+
469
543
  test("resolves nested paths and the index (namespace root)", async () => {
470
544
  writePluginHandler(
471
545
  "my-plugin",
@@ -583,10 +657,7 @@ describe("plugin routes", () => {
583
657
  const plugin = "stale-helper";
584
658
  const helperPath = join(getWorkspacePluginsDir(), plugin, "src", "http.ts");
585
659
  mkdirSync(dirname(helperPath), { recursive: true });
586
- writeFileSync(
587
- helperPath,
588
- `export function label() { return "v1"; }\n`,
589
- );
660
+ writeFileSync(helperPath, `export function label() { return "v1"; }\n`);
590
661
  writePluginHandler(
591
662
  plugin,
592
663
  "frame.ts",
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { z } from "zod";
11
11
 
12
+ import { PendingToolQuestionSchema } from "../../api/responses/conversation-message.js";
12
13
  import { findConversation } from "../../daemon/conversation-registry.js";
13
14
  import type {
14
15
  SecretDelivery,
@@ -210,9 +211,18 @@ function handleSecret({ body }: RouteHandlerArgs) {
210
211
  * GET /v1/pending-interactions?conversationKey=...&conversationId=...
211
212
  *
212
213
  * Returns pending interactions. When conversationKey or conversationId is
213
- * provided, returns the first pending confirmation and secret for that
214
- * conversation. When neither is provided, returns all pending interactions
215
- * across all conversations (diagnostic mode).
214
+ * provided, returns the outstanding confirmation, secret, and question for
215
+ * that conversation. When neither is provided, returns all pending
216
+ * interactions across all conversations (diagnostic mode).
217
+ *
218
+ * This registry is the authority on whether a prompt is still awaiting the
219
+ * user, so the conversation-scoped shape is what clients project their prompt
220
+ * UI from: a key present with a `null` value states "nothing outstanding of
221
+ * this kind" as positively as an object states the opposite. Every
222
+ * conversation-scoped return path below therefore names all three keys, even
223
+ * the early return for an unresolvable conversation. A client that sees a key
224
+ * missing entirely is talking to an assistant that predates it, which is how
225
+ * `pendingQuestion` consumers detect support without a version check.
216
226
  */
217
227
  function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
218
228
  const conversationKey = queryParams?.conversationKey;
@@ -241,7 +251,11 @@ function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
241
251
  }
242
252
 
243
253
  if (!resolvedConversationId) {
244
- return { pendingConfirmation: null, pendingSecret: null };
254
+ return {
255
+ pendingConfirmation: null,
256
+ pendingSecret: null,
257
+ pendingQuestion: null,
258
+ };
245
259
  }
246
260
 
247
261
  const interactions = pendingInteractions.getByConversation(
@@ -252,6 +266,13 @@ function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
252
266
  (i) => i.kind === "confirmation" || i.kind === "acp_confirmation",
253
267
  );
254
268
  const secret = interactions.find((i) => i.kind === "secret");
269
+ // Newest-first for questions: a new user message supersedes an open
270
+ // ask_question by steering to it, which settles the old prompt through its
271
+ // abort signal rather than synchronously, so both can sit in the registry
272
+ // for the width of that turn. The newer prompt is the one the user is being
273
+ // asked, so it is the one to report.
274
+ const questions = interactions.filter((i) => i.kind === "question");
275
+ const question = questions[questions.length - 1];
255
276
 
256
277
  return {
257
278
  pendingConfirmation: confirmation
@@ -288,6 +309,15 @@ function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
288
309
  allowOneTimeSend: secret.secretDetails?.allowOneTimeSend,
289
310
  }
290
311
  : null,
312
+ // Reported only with its `entries`, since the batch is what the card
313
+ // renders; a prompt that somehow carries none is not something a client
314
+ // could raise.
315
+ pendingQuestion: question?.questionDetails
316
+ ? {
317
+ requestId: question.requestId,
318
+ entries: question.questionDetails.entries,
319
+ }
320
+ : null,
291
321
  };
292
322
  }
293
323
 
@@ -396,6 +426,9 @@ export const ROUTES: RouteDefinition[] = [
396
426
  .nullable()
397
427
  .describe("Pending secret request or null")
398
428
  .optional(),
429
+ pendingQuestion: PendingToolQuestionSchema.nullable()
430
+ .describe("Outstanding ask_question prompt or null")
431
+ .optional(),
399
432
  interactions: z
400
433
  .array(
401
434
  z.object({
@@ -3,6 +3,7 @@
3
3
  * the conversational approval engine when an approvalConversationGenerator is
4
4
  * available. Classifies natural language and responds conversationally.
5
5
  */
6
+ import { audienceForReader } from "../../../channels/message-audience.js";
6
7
  import type { ChannelId } from "../../../channels/types.js";
7
8
  import { getLogger } from "../../../util/logger.js";
8
9
  import { runApprovalConversationTurn } from "../../approval-conversation-turn.js";
@@ -22,17 +23,6 @@ import { deliverStaleApprovalReply } from "../guardian-approval-reply-helpers.js
22
23
 
23
24
  const log = getLogger("runtime-http");
24
25
 
25
- /**
26
- * Resolve the Slack ephemeral user ID when the source channel is Slack.
27
- * Returns `undefined` for non-Slack channels.
28
- */
29
- function slackEphemeralUserId(
30
- sourceChannel: ChannelId,
31
- userId: string | undefined,
32
- ): string | undefined {
33
- return sourceChannel === "slack" && userId ? userId : undefined;
34
- }
35
-
36
26
  export interface TextEngineDecisionParams {
37
27
  conversationId: string;
38
28
  conversationExternalId: string;
@@ -96,11 +86,11 @@ export async function handleGuardianTextEngineDecision(
96
86
  text: engineResult.replyText,
97
87
  assistantId,
98
88
  };
99
- const ephemeral = slackEphemeralUserId(sourceChannel, actorExternalId);
100
- if (ephemeral) {
101
- keepPendingPayload.ephemeral = true;
102
- keepPendingPayload.user = ephemeral;
103
- }
89
+ keepPendingPayload.audience = audienceForReader(
90
+ sourceChannel,
91
+ conversationExternalId,
92
+ actorExternalId,
93
+ );
104
94
  await deliverChannelReply(replyCallbackUrl, keepPendingPayload);
105
95
  } catch (err) {
106
96
  log.error(
@@ -131,11 +121,11 @@ export async function handleGuardianTextEngineDecision(
131
121
  text: engineResult.replyText,
132
122
  assistantId,
133
123
  };
134
- const ephemeral = slackEphemeralUserId(sourceChannel, actorExternalId);
135
- if (ephemeral) {
136
- decisionPayload.ephemeral = true;
137
- decisionPayload.user = ephemeral;
138
- }
124
+ decisionPayload.audience = audienceForReader(
125
+ sourceChannel,
126
+ conversationExternalId,
127
+ actorExternalId,
128
+ );
139
129
  await deliverChannelReply(replyCallbackUrl, decisionPayload);
140
130
  } catch (err) {
141
131
  log.error(
@@ -160,7 +150,11 @@ export async function handleGuardianTextEngineDecision(
160
150
  logger: log,
161
151
  errorLogMessage: "Failed to deliver stale approval notice",
162
152
  errorLogContext: { conversationId },
163
- ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
153
+ audience: audienceForReader(
154
+ sourceChannel,
155
+ conversationExternalId,
156
+ actorExternalId,
157
+ ),
164
158
  });
165
159
 
166
160
  return { handled: true, type: "stale_ignored" };
@@ -3,8 +3,8 @@ import type { AssistantEvent } from "../../api/index.js";
3
3
  import {
4
4
  addMessage,
5
5
  recordConversationPersistedSeq,
6
- SYSTEM_CARD_MESSAGE_KIND,
7
6
  } from "../../persistence/conversation-crud.js";
7
+ import { SYSTEM_CARD_MESSAGE_KIND } from "../../persistence/conversation-types.js";
8
8
  import type { Message } from "../../providers/types.js";
9
9
  import { broadcastMessage } from "../assistant-event-hub.js";
10
10
  import { getCurrentSeq } from "../assistant-stream-state.js";
@@ -40,33 +40,42 @@ export function emitCannedMessageComplete(
40
40
  }
41
41
 
42
42
  /**
43
- * Persist a canned assistant "card" — a pre-composed reply that bypasses the
44
- * agent loop (the /compact, /clean, and summarize-up-to result cards). The
45
- * row is stamped `messageKind: "system_card"` so transcripts render it as a
46
- * standalone system notice instead of assistant-persona speech, and display
47
- * merging never folds it into an adjacent assistant turn.
43
+ * Persist a system card: a daemon-authored notice that bypasses the agent
44
+ * loop. The row is stamped `messageKind: "system_card"` so transcripts render
45
+ * it as a standalone system notice instead of assistant-persona speech, and
46
+ * display merging never folds it into an adjacent assistant turn.
48
47
  *
49
- * Cards are announced with `message_complete` (persisted assistant id), the
50
- * persisted-seq anchor advance (so a stale /messages reseed cannot erase the
51
- * card), and the messages-changed sync invalidation that drives every client
52
- * to refetch. That invalidation carries no origin-client id: the card body
53
- * streams nowhere (`message_complete` is bodyless, no `assistant_text_delta`),
54
- * so the initiating client materializes it only by refetching, and origin
55
- * self-echo suppression (meant for content the origin already rendered) would
56
- * otherwise hide the card from the initiator until a reload. A delta is
57
- * deliberately omitted; it would stream the card into the tail assistant
58
- * bubble as if the persona were speaking.
48
+ * The card is persisted and announced to clients but not appended to the live
49
+ * conversation's working history, so a card written while a turn is in flight
50
+ * cannot leave a trailing assistant message for the next provider call to
51
+ * continue from. Callers whose card ends the turn use
52
+ * {@link persistCannedAssistantCard}, which also seats it in the live history.
59
53
  *
60
- * Returns the persisted card's message id so callers can link related
61
- * records to it (e.g. the compaction `llm_request_logs` row).
54
+ * Every card advances the persisted-seq anchor (so a stale /messages reseed
55
+ * cannot erase it) and publishes the messages-changed sync invalidation that
56
+ * drives every client to refetch. That invalidation carries no origin-client
57
+ * id: the card body streams nowhere (no `assistant_text_delta`), so the
58
+ * initiating client materializes it only by refetching, and origin self-echo
59
+ * suppression (meant for content the origin already rendered) would otherwise
60
+ * hide the card from the initiator until a reload. A delta is deliberately
61
+ * omitted; it would stream the card into the tail assistant bubble as if the
62
+ * persona were speaking.
63
+ *
64
+ * `endsTurn` decides whether the card also emits `message_complete`. Clients
65
+ * treat that event as terminal (it clears the processing state and closes the
66
+ * turn), so only a card that *is* the reply carries it. A card posted while a
67
+ * turn is still running (`endsTurn: false`) rides the sync invalidation alone,
68
+ * leaving the in-flight turn's streaming state intact.
69
+ *
70
+ * Returns the persisted card's id and the in-memory message it persisted.
62
71
  */
63
- export async function persistCannedAssistantCard(opts: {
64
- conversation: { getMessages(): Message[] };
72
+ export async function persistSystemCard(opts: {
65
73
  conversationId: string;
66
74
  text: string;
67
75
  metadata: Record<string, unknown>;
68
- }): Promise<string> {
69
- const { conversation, conversationId, text, metadata } = opts;
76
+ endsTurn: boolean;
77
+ }): Promise<{ id: string; message: Message }> {
78
+ const { conversationId, text, metadata, endsTurn } = opts;
70
79
  const assistantMsg = createAssistantMessage(text);
71
80
  const persistedAssistant = await addMessage(
72
81
  conversationId,
@@ -74,13 +83,43 @@ export async function persistCannedAssistantCard(opts: {
74
83
  JSON.stringify(assistantMsg.content),
75
84
  { metadata: { ...metadata, messageKind: SYSTEM_CARD_MESSAGE_KIND } },
76
85
  );
77
- conversation.getMessages().push(assistantMsg);
78
- emitCannedMessageComplete(
79
- broadcastMessage,
80
- conversationId,
81
- persistedAssistant.id,
82
- );
86
+ if (endsTurn) {
87
+ emitCannedMessageComplete(
88
+ broadcastMessage,
89
+ conversationId,
90
+ persistedAssistant.id,
91
+ );
92
+ }
83
93
  recordConversationPersistedSeq(conversationId, getCurrentSeq());
84
94
  publishConversationMessagesChanged(conversationId);
85
- return persistedAssistant.id;
95
+ return { id: persistedAssistant.id, message: assistantMsg };
96
+ }
97
+
98
+ /**
99
+ * Persist a canned assistant "card" (a pre-composed reply that bypasses the
100
+ * agent loop: the /compact, /clean, and summarize-up-to result cards) and
101
+ * seat it in the live conversation's history so the turn that follows sees it.
102
+ * The row is stamped `messageKind: "system_card"` so transcripts render it as a
103
+ * standalone system notice instead of assistant-persona speech, and display
104
+ * merging never folds it into an adjacent assistant turn. Delivery is
105
+ * {@link persistSystemCard}'s.
106
+ *
107
+ * Returns the persisted card's message id so callers can link related
108
+ * records to it (e.g. the compaction `llm_request_logs` row).
109
+ */
110
+ export async function persistCannedAssistantCard(opts: {
111
+ conversation: { getMessages(): Message[] };
112
+ conversationId: string;
113
+ text: string;
114
+ metadata: Record<string, unknown>;
115
+ }): Promise<string> {
116
+ const { conversation, conversationId, text, metadata } = opts;
117
+ const card = await persistSystemCard({
118
+ conversationId,
119
+ text,
120
+ metadata,
121
+ endsTurn: true,
122
+ });
123
+ conversation.getMessages().push(card.message);
124
+ return card.id;
86
125
  }
@@ -37,6 +37,23 @@ const PresenceBodySchema = z.object({
37
37
  .describe("Reported desktop presence state."),
38
38
  });
39
39
 
40
+ /**
41
+ * Body of `POST /v1/clients/web-presence`, declared as the route's
42
+ * `requestBody` and parsed by the handler, so the OpenAPI contract and the
43
+ * runtime check are one schema rather than two hand-kept copies.
44
+ */
45
+ const WebPresenceBodySchema = z.object({
46
+ visible: z
47
+ .boolean()
48
+ .describe("Whether the reporting tab is currently visible."),
49
+ focusedConversationId: z
50
+ .string()
51
+ .nullable()
52
+ .describe(
53
+ "The conversation currently focused (chat composer on screen) in the tab, or null.",
54
+ ),
55
+ });
56
+
40
57
  export const ROUTES: RouteDefinition[] = [
41
58
  {
42
59
  operationId: "list_clients",
@@ -228,4 +245,100 @@ export const ROUTES: RouteDefinition[] = [
228
245
  return { recorded: true };
229
246
  },
230
247
  },
248
+ {
249
+ operationId: "report_web_presence",
250
+ endpoint: "clients/web-presence",
251
+ method: "POST",
252
+ policy: {
253
+ requiredScopes: ["settings.write"],
254
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
255
+ },
256
+ summary: "Report web tab visibility and focused conversation",
257
+ description:
258
+ "Record the visibility and focused conversation reported by the web client identified by the X-Vellum-Client-Id header.",
259
+ tags: ["clients"],
260
+ requestBody: WebPresenceBodySchema,
261
+ responseBody: z.object({
262
+ recorded: z
263
+ .boolean()
264
+ .describe("Whether a connected client matched the reporting clientId."),
265
+ }),
266
+ handler: ({ body, headers }) => {
267
+ const clientId = headers?.["x-vellum-client-id"]?.trim();
268
+ if (!clientId) {
269
+ throw new BadRequestError(
270
+ "x-vellum-client-id header is required to report presence.",
271
+ );
272
+ }
273
+ const { visible, focusedConversationId } = parseBody(
274
+ WebPresenceBodySchema,
275
+ body,
276
+ );
277
+
278
+ // Resolving the client first separates "nobody is listening" from
279
+ // "somebody else owns this", so only the second one is worth a warn.
280
+ const client = assistantEventHub.getClientById(clientId);
281
+ if (!client) {
282
+ // Debug, not warn: a report racing an SSE reconnect matches nothing,
283
+ // and a reconnect storm would otherwise emit a warn per client for a
284
+ // benign race. Not a 404 either, for the same reason.
285
+ log.debug(
286
+ { op: "report_web_presence" },
287
+ "Web presence report matched no connected client",
288
+ );
289
+ return { recorded: false };
290
+ }
291
+
292
+ // Only the actor that opened the target client's SSE stream may report
293
+ // its presence, so a caller who learns another user's clientId cannot
294
+ // spoof that tab. Clients with no stored `actorPrincipalId` (legacy SSE
295
+ // subscribers, service-gateway tokens) never match: fail-closed, the
296
+ // same posture as `report_client_presence`. Dev-bypass mode
297
+ // (DISABLE_HTTP_AUTH=true) skips the check for platform-managed
298
+ // deployments where the platform handles auth.
299
+ if (!isHttpAuthDisabled()) {
300
+ const callerPrincipalId = headers?.["x-vellum-actor-principal-id"];
301
+ const ownerPrincipalId = client.actorPrincipalId;
302
+ if (
303
+ ownerPrincipalId === undefined ||
304
+ ownerPrincipalId !== callerPrincipalId
305
+ ) {
306
+ // Warn, not debug: the client is connected, so a mismatch means the
307
+ // gateway stopped forwarding the actor header, a scope profile
308
+ // shifted, or a caller is probing someone else's client. Without
309
+ // this line presence gating dies silently and every suppressed
310
+ // push quietly resumes. Client and principal ids stay out of the
311
+ // entry so the log cannot be used to enumerate them.
312
+ log.warn(
313
+ {
314
+ op: "report_web_presence",
315
+ hasStoredOwner: ownerPrincipalId !== undefined,
316
+ hasCallerPrincipal: callerPrincipalId !== undefined,
317
+ targetInterfaceId: client.interfaceId,
318
+ },
319
+ "Rejecting web presence report from a caller that does not own the client",
320
+ );
321
+ // Answering like "no match" keeps the reply from probing client ids.
322
+ return { recorded: false };
323
+ }
324
+ }
325
+
326
+ // The lookup ran in this same synchronous handler, so the write lands on
327
+ // the entry it resolved.
328
+ assistantEventHub.setClientWebPresence(clientId, {
329
+ visible,
330
+ focusedConversationId,
331
+ });
332
+
333
+ log.debug(
334
+ {
335
+ op: "report_web_presence",
336
+ visible,
337
+ interfaceId: client.interfaceId,
338
+ },
339
+ "Recorded web presence",
340
+ );
341
+ return { recorded: true };
342
+ },
343
+ },
231
344
  ];
@@ -269,12 +269,18 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
269
269
  ? true
270
270
  : undefined;
271
271
 
272
+ const foregroundOnly =
273
+ parseEnumQueryParam(queryParams, "foregroundOnly", ["true"]) === "true"
274
+ ? true
275
+ : undefined;
276
+
272
277
  const filter: ConversationListFilter = {
273
278
  conversationType,
274
279
  archiveStatus,
275
280
  originChannel,
276
281
  groupId,
277
282
  needsAttention,
283
+ foregroundOnly,
278
284
  };
279
285
  let rows = listConversations({ ...filter, limit, offset });
280
286
  const totalCount = countConversations(filter);
@@ -289,19 +295,20 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
289
295
  // Skipped for group-scoped queries for the same reason: a caller asking
290
296
  // for one group gets that group, and a client that fetches the Pinned
291
297
  // section via `groupId=system:pinned` has no use for rows appended to
292
- // some other group's page. Skipped for attention-scoped queries too: the
293
- // caller asked for the unseen subset, and a seen pinned row appended to
294
- // it would put a row outside the filter in the page while `hasMore` is
295
- // computed from the filtered count. This is the compatibility shim for
296
- // clients that still read Pinned out of the unfiltered list; it goes
297
- // away once every section fetches its own group.
298
+ // some other group's page. Skipped for attention-scoped and
299
+ // foreground-only queries too: the caller asked for a subset, and a
300
+ // pinned row outside it appended to the page would sit there while
301
+ // `hasMore` is computed from the filtered count. This is the
302
+ // compatibility shim for clients that still read Pinned out of the
303
+ // unfiltered list; it goes away once every section fetches its own group.
298
304
  if (
299
305
  offset === 0 &&
300
306
  conversationType === "standard" &&
301
307
  archiveStatus === "active" &&
302
308
  originChannel === undefined &&
303
309
  groupId === undefined &&
304
- needsAttention === undefined
310
+ needsAttention === undefined &&
311
+ foregroundOnly === undefined
305
312
  ) {
306
313
  const pinned = listPinnedConversations(archiveStatus);
307
314
  const seen = new Set(rows.map((c) => c.id));
@@ -611,6 +618,14 @@ export const ROUTES: RouteDefinition[] = [
611
618
  'Pass "true" to return only conversations whose latest assistant message the user has not seen: the same predicate behind the unread count and the section index, so a client that keeps no complete conversation list can ask for exactly the rows its attention surfaces need. Composes with every other filter. Any value other than "true" is rejected. Omit to span every conversation.',
612
619
  schema: { type: "string", enum: ["true"] },
613
620
  },
621
+ {
622
+ name: "foregroundOnly",
623
+ type: "string",
624
+ required: false,
625
+ description:
626
+ 'Pass "true" to return only user-facing foreground conversations, dropping the automated background and scheduled runs the standard listing admits when they are filed in a custom group (a surfaced run stays). This is the same predicate the unread count and the section index apply, so a client can ask for the newest conversation a user can open in one row instead of paging past runs it would skip. Composes with every other filter; a foreground-only first page never has pinned rows appended to it. Any value other than "true" is rejected. Omit to keep every visible row.',
627
+ schema: { type: "string", enum: ["true"] },
628
+ },
614
629
  ],
615
630
  responseBody: listConversationsResponseSchema,
616
631
  handler: handleListConversations,