@vellumai/assistant 0.11.4 → 0.11.5-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -15,10 +15,12 @@ import type {
15
15
  UserPromptSubmitContext,
16
16
  } from "@vellumai/plugin-api";
17
17
 
18
- // The current-turn boundary is the real host helper `caption-blocks.ts`
19
- // reaches through `@vellumai/plugin-api`; wire it into the mock so the sweep
20
- // tests exercise the shipped scope behavior rather than a stand-in.
18
+ // The current-turn boundary and the vision-rejection classifier are the real
19
+ // host helpers the plugin reaches through `@vellumai/plugin-api`; wire them
20
+ // into the mock so the tests exercise the shipped behavior rather than a
21
+ // stand-in.
21
22
  import { lastToolResultUserMessageIndex } from "../../../../context/outbound-sanitize.js";
23
+ import { isVisionNotSupportedError } from "../../../../util/provider-error-patterns.js";
22
24
 
23
25
  // ─── Mocks ──────────────────────────────────────────────────────────────────
24
26
 
@@ -48,18 +50,48 @@ const mockResolveMediaSourceData = (source: ImageContent["source"]) =>
48
50
  ? { data: source.data, media_type: source.media_type }
49
51
  : null;
50
52
 
53
+ // System cards the plugin posts, captured instead of persisted.
54
+ let persistedCards: Array<{
55
+ conversationId: string;
56
+ text: string;
57
+ metadata: Record<string, unknown>;
58
+ }> = [];
59
+ let cardPersistenceFails = false;
60
+
51
61
  // Mock @vellumai/plugin-api — only the runtime handles the plugin imports.
52
62
  // `extractAllText` stays real (imported from the relative path, not plugin-api).
53
- mock.module("@vellumai/plugin-api", () => ({
54
- doesSupportVision: (arg: ModelProfileInfo | string) =>
55
- typeof arg === "string"
56
- ? visionModels.has(arg)
57
- : visionProfiles.has(arg.key),
58
- getModelProfiles: () => mockProfiles,
59
- resolveMediaSourceData: mockResolveMediaSourceData,
60
- getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
61
- lastToolResultUserMessageIndex,
62
- }));
63
+ // One factory backs every install so a per-test override (e.g. a
64
+ // call-counting provider) cannot silently drop the rest of the surface. The
65
+ // module registry is process-wide, so the surface covers every plugin-api
66
+ // export the plugin's hooks import, including ones only sibling test files
67
+ // exercise.
68
+ function installPluginApiMock(overrides: Record<string, unknown> = {}): void {
69
+ mock.module("@vellumai/plugin-api", () => ({
70
+ doesSupportVision: (arg: ModelProfileInfo | string) =>
71
+ typeof arg === "string"
72
+ ? visionModels.has(arg)
73
+ : visionProfiles.has(arg.key),
74
+ getModelProfiles: () => mockProfiles,
75
+ resolveMediaSourceData: mockResolveMediaSourceData,
76
+ getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
77
+ lastToolResultUserMessageIndex,
78
+ isVisionNotSupportedError,
79
+ persistSystemCard: async (opts: {
80
+ conversationId: string;
81
+ text: string;
82
+ metadata: Record<string, unknown>;
83
+ }) => {
84
+ if (cardPersistenceFails) {
85
+ throw new Error("card write failed");
86
+ }
87
+ persistedCards.push(opts);
88
+ return "card-1";
89
+ },
90
+ ...overrides,
91
+ }));
92
+ }
93
+
94
+ installPluginApiMock();
63
95
 
64
96
  // Mock the image-persist module to avoid filesystem side effects in tests.
65
97
  let mockPersistPath: string | null =
@@ -77,6 +109,7 @@ const postCompact = (await import("../hooks/post-compact.js")).default;
77
109
  const conversationDeleted = (await import("../hooks/conversation-deleted.js"))
78
110
  .default;
79
111
  const { findVisionProfile } = await import("../src/vision-caption.js");
112
+ const { flattenTextOnlyBlocks } = await import("../src/caption-blocks.js");
80
113
  const { closeCaptionStore, initCaptionStore, resetCaptionCacheForTests } =
81
114
  await import("../src/caption-cache.js");
82
115
 
@@ -130,9 +163,16 @@ function textMsg(text: string): Message {
130
163
  return { role: "user", content: [{ type: "text", text }] };
131
164
  }
132
165
 
166
+ /**
167
+ * A user-prompt-submit context whose `originalMessages` tail is the submitted
168
+ * message, the way the agent loop snapshots the history before the hook chain
169
+ * runs. Pass `originalMessages` explicitly to model a history the turn did not
170
+ * submit anything into.
171
+ */
133
172
  function makeCtx(
134
173
  overrides: Partial<UserPromptSubmitContext> = {},
135
174
  ): UserPromptSubmitContext {
175
+ const latestMessages = overrides.latestMessages ?? [];
136
176
  return {
137
177
  conversationId: "c1",
138
178
  userMessageId: "m1",
@@ -140,8 +180,8 @@ function makeCtx(
140
180
  modelProfileKey: "text-only",
141
181
  isNonInteractive: false,
142
182
  prompt: "What is in this image?",
143
- originalMessages: [],
144
- latestMessages: [],
183
+ latestMessages,
184
+ originalMessages: Object.freeze([...latestMessages]),
145
185
  logger,
146
186
  ...overrides,
147
187
  } as unknown as UserPromptSubmitContext;
@@ -201,6 +241,9 @@ beforeEach(() => {
201
241
  };
202
242
  providerResolves = true;
203
243
  mockPersistPath = "/workspace/data/attachments/mock-hash.png";
244
+ persistedCards = [];
245
+ cardPersistenceFails = false;
246
+ installPluginApiMock();
204
247
  resetCaptionCacheForTests();
205
248
  });
206
249
 
@@ -276,7 +319,13 @@ describe("image-fallback user-prompt-submit hook", () => {
276
319
  expect(text).not.toContain("/workspace/data/attachments/");
277
320
  });
278
321
 
279
- test("preserves non-image blocks and captions only images", async () => {
322
+ test("preserves the user's own text around the caption it substitutes", async () => {
323
+ /**
324
+ * Tests that surrounding prose survives the substitution, merged with the
325
+ * caption in submitted order.
326
+ */
327
+
328
+ // GIVEN a submitted message that wraps an image in the user's own prose
280
329
  const messages: Message[] = [
281
330
  {
282
331
  role: "user",
@@ -288,16 +337,16 @@ describe("image-fallback user-prompt-submit hook", () => {
288
337
  },
289
338
  ];
290
339
  const ctx = makeCtx({ latestMessages: messages });
340
+
341
+ // WHEN the turn starts
291
342
  await userPromptSubmit(ctx);
343
+
344
+ // THEN the caption replaces the image between the two prose blocks, and
345
+ // the now text-only content is merged into the single block providers
346
+ // serialize as a plain string
347
+ expect(ctx.latestMessages[0].content).toHaveLength(1);
292
348
  expect((ctx.latestMessages[0].content[0] as { text: string }).text).toBe(
293
- "Look at this:",
294
- );
295
- expect(ctx.latestMessages[0].content[1].type).toBe("text");
296
- expect(
297
- (ctx.latestMessages[0].content[1] as { text: string }).text,
298
- ).toContain("[Image auto-described");
299
- expect((ctx.latestMessages[0].content[2] as { text: string }).text).toBe(
300
- "What do you see?",
349
+ "Look at this:\n\n[Image auto-described for text-only model: A red chart showing Q3 revenue.]\n\nWhat do you see?",
301
350
  );
302
351
  });
303
352
 
@@ -333,15 +382,9 @@ describe("image-fallback user-prompt-submit hook", () => {
333
382
  },
334
383
  };
335
384
  // Override the mock to track calls.
336
- mock.module("@vellumai/plugin-api", () => ({
337
- doesSupportVision: (arg: ModelProfileInfo | string) =>
338
- typeof arg === "string"
339
- ? visionModels.has(arg)
340
- : visionProfiles.has(arg.key),
341
- getModelProfiles: () => mockProfiles,
342
- resolveMediaSourceData: mockResolveMediaSourceData,
385
+ installPluginApiMock({
343
386
  getConfiguredProvider: async () => trackingProvider,
344
- }));
387
+ });
345
388
 
346
389
  const messages1 = [imageMsg("same-data")];
347
390
  const ctx1 = makeCtx({ latestMessages: messages1 });
@@ -353,18 +396,6 @@ describe("image-fallback user-prompt-submit hook", () => {
353
396
  const ctx2 = makeCtx({ latestMessages: messages2 });
354
397
  await userPromptSubmit(ctx2);
355
398
  expect(callCount).toBe(1); // still 1 — cache hit
356
-
357
- // Restore the original mock for other tests.
358
- mock.module("@vellumai/plugin-api", () => ({
359
- doesSupportVision: (arg: ModelProfileInfo | string) =>
360
- typeof arg === "string"
361
- ? visionModels.has(arg)
362
- : visionProfiles.has(arg.key),
363
- getModelProfiles: () => mockProfiles,
364
- resolveMediaSourceData: mockResolveMediaSourceData,
365
- getConfiguredProvider: async () =>
366
- providerResolves ? fakeProvider : null,
367
- }));
368
399
  });
369
400
 
370
401
  test("captions images nested in a historical tool_result's contentBlocks", async () => {
@@ -399,13 +430,287 @@ describe("image-fallback user-prompt-submit hook", () => {
399
430
  expect(
400
431
  (ctx.latestMessages[0].content[0] as { text: string }).text,
401
432
  ).toContain("[Image auto-described");
402
- expect(ctx.latestMessages[2].content[0].type).toBe("text");
433
+ expect(ctx.latestMessages[2].content).toHaveLength(1);
403
434
  expect(
404
435
  (ctx.latestMessages[2].content[0] as { text: string }).text,
405
436
  ).toContain("[Image auto-described");
406
- expect((ctx.latestMessages[2].content[1] as { text: string }).text).toBe(
407
- "both?",
437
+ expect(
438
+ (ctx.latestMessages[2].content[0] as { text: string }).text,
439
+ ).toEndWith("\n\nboth?");
440
+ });
441
+ });
442
+
443
+ describe("image-fallback dropped-image notice", () => {
444
+ test("posts a transcript card when this turn's image cannot be described", async () => {
445
+ /**
446
+ * Tests that an image the model never receives in any form is reported to
447
+ * the user instead of being dropped silently.
448
+ */
449
+
450
+ // GIVEN a workspace with no vision-capable profile to caption with
451
+ visionProfiles = new Set<string>();
452
+
453
+ // AND a turn that submits prose plus an image
454
+ const submitted: Message = {
455
+ role: "user",
456
+ content: [{ type: "text", text: "look at this" }, imageBlock("dropped")],
457
+ };
458
+ const ctx = makeCtx({ latestMessages: [submitted] });
459
+
460
+ // WHEN the turn starts
461
+ await userPromptSubmit(ctx);
462
+
463
+ // THEN the model receives the prose and a placeholder, merged into the
464
+ // single text block providers serialize as a plain string
465
+ expect(ctx.latestMessages[0].content).toHaveLength(1);
466
+ expect((ctx.latestMessages[0].content[0] as { text: string }).text).toBe(
467
+ "look at this\n\n[Image: no vision-capable model configured to describe it]",
408
468
  );
469
+
470
+ // AND the transcript carries one card telling the user the image was not sent
471
+ expect(persistedCards).toHaveLength(1);
472
+ expect(persistedCards[0].conversationId).toBe("c1");
473
+ expect(persistedCards[0].text).toBe(
474
+ "The image you attached was not sent to the model: no vision-capable model is configured to describe it. Configure one to use images in this conversation.",
475
+ );
476
+ expect(persistedCards[0].metadata).toEqual({
477
+ plugin: "image-fallback",
478
+ droppedImageCount: 1,
479
+ });
480
+ });
481
+
482
+ test("reports every image the turn attached in a single card", async () => {
483
+ /**
484
+ * Tests that one turn attaching several undescribable images produces one
485
+ * card that counts them.
486
+ */
487
+
488
+ // GIVEN no vision-capable profile
489
+ visionProfiles = new Set<string>();
490
+
491
+ // AND a turn that submits two images
492
+ const submitted: Message = {
493
+ role: "user",
494
+ content: [imageBlock("drop-a"), imageBlock("drop-b")],
495
+ };
496
+ const ctx = makeCtx({ latestMessages: [submitted] });
497
+
498
+ // WHEN the turn starts
499
+ await userPromptSubmit(ctx);
500
+
501
+ // THEN a single card reports both
502
+ expect(persistedCards).toHaveLength(1);
503
+ expect(persistedCards[0].text).toContain("The 2 images you attached");
504
+ expect(persistedCards[0].metadata.droppedImageCount).toBe(2);
505
+ });
506
+
507
+ test("posts a card when an injector rebuilt the submitted message", async () => {
508
+ /**
509
+ * Tests that the notice survives an earlier hook replacing the tail
510
+ * message object, which memory and runtime-context injection do when they
511
+ * append their blocks to the submitted message.
512
+ */
513
+
514
+ // GIVEN no vision-capable profile
515
+ visionProfiles = new Set<string>();
516
+
517
+ // AND a submitted message carrying an image
518
+ const submitted: Message = {
519
+ role: "user",
520
+ content: [imageBlock("dropped")],
521
+ };
522
+
523
+ // AND a working history whose tail is a rebuilt copy of it, carrying an
524
+ // injected context block, rather than the submitted object itself
525
+ const injected: Message = {
526
+ role: "user",
527
+ content: [
528
+ ...submitted.content,
529
+ { type: "text", text: "<runtime_context />" },
530
+ ],
531
+ };
532
+ const ctx = makeCtx({
533
+ latestMessages: [injected],
534
+ originalMessages: Object.freeze([submitted]),
535
+ });
536
+
537
+ // WHEN the turn starts
538
+ await userPromptSubmit(ctx);
539
+
540
+ // THEN the user is still told the image was not sent
541
+ expect(persistedCards).toHaveLength(1);
542
+ expect(persistedCards[0].metadata.droppedImageCount).toBe(1);
543
+ });
544
+
545
+ test("posts no card when a vision profile describes the image", async () => {
546
+ /**
547
+ * Tests that a captioned image is not reported as dropped: the model does
548
+ * receive its content.
549
+ */
550
+
551
+ // GIVEN a vision-capable profile and a submitted image
552
+ const ctx = makeCtx({ latestMessages: [imageMsg("described")] });
553
+
554
+ // WHEN the turn starts
555
+ await userPromptSubmit(ctx);
556
+
557
+ // THEN the image is captioned and nothing is reported to the user
558
+ expect(
559
+ (ctx.latestMessages[0].content[0] as { text: string }).text,
560
+ ).toContain("[Image auto-described");
561
+ expect(persistedCards).toHaveLength(0);
562
+ });
563
+
564
+ test("posts no card for raw images the sweep re-encounters from earlier turns", async () => {
565
+ /**
566
+ * Tests that the notice fires once. Persisted rows keep their raw images,
567
+ * so every later turn's sweep drops them again.
568
+ */
569
+
570
+ // GIVEN no vision-capable profile
571
+ visionProfiles = new Set<string>();
572
+
573
+ // AND a history whose earlier turn holds a raw image, with this turn
574
+ // submitting text only
575
+ const history = [imageMsg("from-an-earlier-turn"), textMsg("follow up")];
576
+ const ctx = makeCtx({ latestMessages: history });
577
+
578
+ // WHEN the turn starts
579
+ await userPromptSubmit(ctx);
580
+
581
+ // THEN the older image is still replaced for the provider call
582
+ expect(
583
+ (ctx.latestMessages[0].content[0] as { text: string }).text,
584
+ ).toContain("no vision-capable model");
585
+
586
+ // AND no card repeats a notice the user already saw
587
+ expect(persistedCards).toHaveLength(0);
588
+ });
589
+
590
+ test("posts no card for media a tool returned", async () => {
591
+ /**
592
+ * Tests that tool-returned media stays out of the user-facing tally: the
593
+ * user did not attach it.
594
+ */
595
+
596
+ // GIVEN no vision-capable profile
597
+ visionProfiles = new Set<string>();
598
+
599
+ // AND a submitted message carrying a tool result with a screenshot
600
+ const ctx = makeCtx({
601
+ latestMessages: [
602
+ { role: "user", content: [toolResult([imageBlock("screenshot")])] },
603
+ ],
604
+ });
605
+
606
+ // WHEN the turn starts
607
+ await userPromptSubmit(ctx);
608
+
609
+ // THEN the nested image is replaced without notifying the user
610
+ const result = ctx.latestMessages[0].content[0] as ToolResultContent;
611
+ expect((result.contentBlocks![0] as { text: string }).text).toContain(
612
+ "no vision-capable model",
613
+ );
614
+ expect(persistedCards).toHaveLength(0);
615
+ });
616
+
617
+ test("still runs the turn when the notice cannot be persisted", async () => {
618
+ /**
619
+ * Tests that the notice is best-effort: losing it must not fail the turn.
620
+ */
621
+
622
+ // GIVEN no vision-capable profile
623
+ visionProfiles = new Set<string>();
624
+
625
+ // AND a transcript that rejects the card write
626
+ cardPersistenceFails = true;
627
+ const ctx = makeCtx({ latestMessages: [imageMsg("dropped")] });
628
+
629
+ // WHEN the turn starts
630
+ await userPromptSubmit(ctx);
631
+
632
+ // THEN the substitution still reaches the provider-bound history
633
+ expect(
634
+ (ctx.latestMessages[0].content[0] as { text: string }).text,
635
+ ).toContain("no vision-capable model");
636
+ });
637
+ });
638
+
639
+ describe("flattenTextOnlyBlocks", () => {
640
+ test("leaves a single text block untouched", () => {
641
+ /**
642
+ * Tests that content already in the shape providers serialize as a string
643
+ * is not rebuilt.
644
+ */
645
+
646
+ // GIVEN a user message holding one text block
647
+ const block = { type: "text" as const, text: "just text" };
648
+ const messages: Message[] = [{ role: "user", content: [block] }];
649
+
650
+ // WHEN the content is flattened
651
+ const flattened = flattenTextOnlyBlocks(messages);
652
+
653
+ // THEN nothing changes
654
+ expect(flattened).toBe(0);
655
+ expect(messages[0].content).toEqual([block]);
656
+ });
657
+
658
+ test("leaves content holding a non-text block untouched", () => {
659
+ /**
660
+ * Tests that merging never discards a block the provider must serialize on
661
+ * its own, e.g. an attached file.
662
+ */
663
+
664
+ // GIVEN a user message mixing text with a file block
665
+ const messages: Message[] = [
666
+ {
667
+ role: "user",
668
+ content: [
669
+ { type: "text", text: "see the attachment" },
670
+ {
671
+ type: "file",
672
+ source: {
673
+ type: "base64",
674
+ media_type: "application/pdf",
675
+ data: "pdfdata",
676
+ },
677
+ },
678
+ ],
679
+ },
680
+ ];
681
+
682
+ // WHEN the content is flattened
683
+ const flattened = flattenTextOnlyBlocks(messages);
684
+
685
+ // THEN both blocks survive
686
+ expect(flattened).toBe(0);
687
+ expect(messages[0].content).toHaveLength(2);
688
+ expect(messages[0].content[1].type).toBe("file");
689
+ });
690
+
691
+ test("leaves assistant content untouched", () => {
692
+ /**
693
+ * Tests that only user content is merged: assistant block boundaries carry
694
+ * provider-side meaning (streamed parts, tool calls).
695
+ */
696
+
697
+ // GIVEN an assistant message with two text blocks
698
+ const messages: Message[] = [
699
+ {
700
+ role: "assistant",
701
+ content: [
702
+ { type: "text", text: "first" },
703
+ { type: "text", text: "second" },
704
+ ],
705
+ },
706
+ ];
707
+
708
+ // WHEN the content is flattened
709
+ const flattened = flattenTextOnlyBlocks(messages);
710
+
711
+ // THEN the blocks stay separate
712
+ expect(flattened).toBe(0);
713
+ expect(messages[0].content).toHaveLength(2);
409
714
  });
410
715
  });
411
716
 
@@ -545,9 +850,9 @@ describe("image-fallback post-compact hook", () => {
545
850
  ];
546
851
  const ctx = makeCompactCtx({ history });
547
852
  await postCompact(ctx);
548
- expect(ctx.history[0].content[1].type).toBe("text");
549
- expect((ctx.history[0].content[1] as { text: string }).text).toBe(
550
- "[Image auto-described for text-only model: A red chart showing Q3 revenue.]",
853
+ expect(ctx.history[0].content).toHaveLength(1);
854
+ expect((ctx.history[0].content[0] as { text: string }).text).toBe(
855
+ "Images retained from the compacted portion of the conversation:\n\n[Image auto-described for text-only model: A red chart showing Q3 revenue.]",
551
856
  );
552
857
  });
553
858
 
@@ -577,15 +882,9 @@ describe("image-fallback post-compact hook", () => {
577
882
  return sendMessageResponse;
578
883
  },
579
884
  };
580
- mock.module("@vellumai/plugin-api", () => ({
581
- doesSupportVision: (arg: ModelProfileInfo | string) =>
582
- typeof arg === "string"
583
- ? visionModels.has(arg)
584
- : visionProfiles.has(arg.key),
585
- getModelProfiles: () => mockProfiles,
586
- resolveMediaSourceData: mockResolveMediaSourceData,
885
+ installPluginApiMock({
587
886
  getConfiguredProvider: async () => trackingProvider,
588
- }));
887
+ });
589
888
 
590
889
  // The image is captioned once at ingestion (turn-start sweep)...
591
890
  const ctx1 = makeCtx({ latestMessages: [imageMsg("compacted-image")] });
@@ -598,18 +897,6 @@ describe("image-fallback post-compact hook", () => {
598
897
  await postCompact(ctx2);
599
898
  expect(callCount).toBe(1); // still 1 — cache hit
600
899
  expect(ctx2.history[0].content[0].type).toBe("text");
601
-
602
- // Restore the original mock for other tests.
603
- mock.module("@vellumai/plugin-api", () => ({
604
- doesSupportVision: (arg: ModelProfileInfo | string) =>
605
- typeof arg === "string"
606
- ? visionModels.has(arg)
607
- : visionProfiles.has(arg.key),
608
- getModelProfiles: () => mockProfiles,
609
- resolveMediaSourceData: mockResolveMediaSourceData,
610
- getConfiguredProvider: async () =>
611
- providerResolves ? fakeProvider : null,
612
- }));
613
900
  });
614
901
 
615
902
  test("uses fail-open placeholder when no vision profile is configured", async () => {
@@ -640,15 +927,9 @@ describe("image-fallback conversation-deleted hook", () => {
640
927
  return sendMessageResponse;
641
928
  },
642
929
  };
643
- mock.module("@vellumai/plugin-api", () => ({
644
- doesSupportVision: (arg: ModelProfileInfo | string) =>
645
- typeof arg === "string"
646
- ? visionModels.has(arg)
647
- : visionProfiles.has(arg.key),
648
- getModelProfiles: () => mockProfiles,
649
- resolveMediaSourceData: mockResolveMediaSourceData,
930
+ installPluginApiMock({
650
931
  getConfiguredProvider: async () => trackingProvider,
651
- }));
932
+ });
652
933
 
653
934
  // Caption an image in the doomed conversation.
654
935
  const ctx1 = makeCtx({
@@ -668,18 +949,6 @@ describe("image-fallback conversation-deleted hook", () => {
668
949
  });
669
950
  await userPromptSubmit(ctx2);
670
951
  expect(callCount).toBe(2);
671
-
672
- // Restore the original mock for other tests.
673
- mock.module("@vellumai/plugin-api", () => ({
674
- doesSupportVision: (arg: ModelProfileInfo | string) =>
675
- typeof arg === "string"
676
- ? visionModels.has(arg)
677
- : visionProfiles.has(arg.key),
678
- getModelProfiles: () => mockProfiles,
679
- resolveMediaSourceData: mockResolveMediaSourceData,
680
- getConfiguredProvider: async () =>
681
- providerResolves ? fakeProvider : null,
682
- }));
683
952
  });
684
953
 
685
954
  test("captions shared with a surviving conversation keep serving hits", async () => {
@@ -691,15 +960,9 @@ describe("image-fallback conversation-deleted hook", () => {
691
960
  return sendMessageResponse;
692
961
  },
693
962
  };
694
- mock.module("@vellumai/plugin-api", () => ({
695
- doesSupportVision: (arg: ModelProfileInfo | string) =>
696
- typeof arg === "string"
697
- ? visionModels.has(arg)
698
- : visionProfiles.has(arg.key),
699
- getModelProfiles: () => mockProfiles,
700
- resolveMediaSourceData: mockResolveMediaSourceData,
963
+ installPluginApiMock({
701
964
  getConfiguredProvider: async () => trackingProvider,
702
- }));
965
+ });
703
966
 
704
967
  // The same image is captioned in one conversation and cache-hit in a
705
968
  // second, which records the second conversation's association.
@@ -724,17 +987,5 @@ describe("image-fallback conversation-deleted hook", () => {
724
987
  });
725
988
  await userPromptSubmit(ctxB2);
726
989
  expect(callCount).toBe(1);
727
-
728
- // Restore the original mock for other tests.
729
- mock.module("@vellumai/plugin-api", () => ({
730
- doesSupportVision: (arg: ModelProfileInfo | string) =>
731
- typeof arg === "string"
732
- ? visionModels.has(arg)
733
- : visionProfiles.has(arg.key),
734
- getModelProfiles: () => mockProfiles,
735
- resolveMediaSourceData: mockResolveMediaSourceData,
736
- getConfiguredProvider: async () =>
737
- providerResolves ? fakeProvider : null,
738
- }));
739
990
  });
740
991
  });
@@ -53,6 +53,10 @@ mock.module("@vellumai/plugin-api", () => ({
53
53
  getConfiguredProvider: async () => fakeProvider,
54
54
  isVisionNotSupportedError,
55
55
  lastToolResultUserMessageIndex,
56
+ // The module registry is process-wide, so this surface covers every
57
+ // plugin-api export the plugin's hooks import, including the transcript
58
+ // notice only the sibling test file exercises.
59
+ persistSystemCard: async () => "card-1",
56
60
  }));
57
61
 
58
62
  mock.module("../src/image-persist.js", () => ({