@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5

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 (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -10,13 +10,13 @@
10
10
  */
11
11
 
12
12
  import type {
13
- PluginHookFn,
13
+ HookFunction,
14
14
  UserPromptSubmitContext,
15
15
  } from "@vellumai/plugin-api";
16
16
 
17
17
  import { repairHistory } from "../terminal.js";
18
18
 
19
- const userPromptSubmit: PluginHookFn<UserPromptSubmitContext> = async (ctx) => {
19
+ const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
20
20
  const { messages, stats } = repairHistory(ctx.latestMessages);
21
21
  ctx.latestMessages = messages;
22
22
  if (
@@ -12,8 +12,11 @@ import type {
12
12
 
13
13
  // ─── Mocks ──────────────────────────────────────────────────────────────────
14
14
 
15
- // Control doesSupportVision per-profile from the test.
15
+ // Control doesSupportVision from the test: by profile key for the
16
+ // user-prompt-submit path (ModelProfileInfo) and by model id for the
17
+ // post-tool-use path (bare string).
16
18
  let visionProfiles: Set<string>;
19
+ let visionModels: Set<string>;
17
20
  let mockProfiles: ModelProfileInfo[];
18
21
  let sendMessageResponse = {
19
22
  content: [{ type: "text", text: "A red chart showing Q3 revenue." }],
@@ -30,8 +33,10 @@ const fakeProvider = {
30
33
  // Mock @vellumai/plugin-api — only the runtime handles the plugin imports.
31
34
  // `extractAllText` stays real (imported from the relative path, not plugin-api).
32
35
  mock.module("@vellumai/plugin-api", () => ({
33
- doesSupportVision: (profile: ModelProfileInfo) =>
34
- visionProfiles.has(profile.key),
36
+ doesSupportVision: (arg: ModelProfileInfo | string) =>
37
+ typeof arg === "string"
38
+ ? visionModels.has(arg)
39
+ : visionProfiles.has(arg.key),
35
40
  getModelProfiles: () => mockProfiles,
36
41
  getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
37
42
  }));
@@ -136,6 +141,9 @@ function makeToolCtx(
136
141
 
137
142
  beforeEach(() => {
138
143
  visionProfiles = new Set<string>(["vision-profile"]);
144
+ // "text-only-model" (the default post-tool-use ctx.model) is absent, so it
145
+ // reads as text-only; a vision model id is added per-test.
146
+ visionModels = new Set<string>();
139
147
  mockProfiles = [
140
148
  profile("text-only", { label: "Text Only", isActive: true }),
141
149
  profile("vision-profile", { label: "Vision" }),
@@ -255,7 +263,10 @@ describe("image-fallback user-prompt-submit hook", () => {
255
263
  };
256
264
  // Override the mock to track calls.
257
265
  mock.module("@vellumai/plugin-api", () => ({
258
- doesSupportVision: (p: ModelProfileInfo) => visionProfiles.has(p.key),
266
+ doesSupportVision: (arg: ModelProfileInfo | string) =>
267
+ typeof arg === "string"
268
+ ? visionModels.has(arg)
269
+ : visionProfiles.has(arg.key),
259
270
  getModelProfiles: () => mockProfiles,
260
271
  getConfiguredProvider: async () => trackingProvider,
261
272
  }));
@@ -273,7 +284,10 @@ describe("image-fallback user-prompt-submit hook", () => {
273
284
 
274
285
  // Restore the original mock for other tests.
275
286
  mock.module("@vellumai/plugin-api", () => ({
276
- doesSupportVision: (p: ModelProfileInfo) => visionProfiles.has(p.key),
287
+ doesSupportVision: (arg: ModelProfileInfo | string) =>
288
+ typeof arg === "string"
289
+ ? visionModels.has(arg)
290
+ : visionProfiles.has(arg.key),
277
291
  getModelProfiles: () => mockProfiles,
278
292
  getConfiguredProvider: async () =>
279
293
  providerResolves ? fakeProvider : null,
@@ -346,9 +360,10 @@ describe("image-fallback post-tool-use hook", () => {
346
360
  );
347
361
  });
348
362
 
349
- test("is a no-op when the active model supports vision", async () => {
350
- visionProfiles = new Set(["text-only"]); // active profile supports vision
363
+ test("is a no-op when the model that ran supports vision", async () => {
364
+ visionModels = new Set(["vision-model"]);
351
365
  const ctx = makeToolCtx({
366
+ model: "vision-model",
352
367
  toolResponse: toolResult([imageBlock("shot1")]),
353
368
  });
354
369
  await postToolUse(ctx);
@@ -398,4 +413,29 @@ describe("image-fallback post-tool-use hook", () => {
398
413
  const text = (ctx.toolResponse.contentBlocks![0] as { text: string }).text;
399
414
  expect(text).not.toContain("saved to");
400
415
  });
416
+
417
+ test("is a no-op when contentBlocks carry no image", async () => {
418
+ const textBlock = { type: "text" as const, text: "just text" };
419
+ const ctx = makeToolCtx({ toolResponse: toolResult([textBlock]) });
420
+ await postToolUse(ctx);
421
+ expect(ctx.toolResponse.contentBlocks![0]).toEqual(textBlock);
422
+ });
423
+
424
+ test("gates on ctx.model, not the workspace active profile", async () => {
425
+ // The active profile is vision-capable, but the model that actually ran
426
+ // (ctx.model) is text-only — the model that ran must win, so the image is
427
+ // captioned.
428
+ mockProfiles = [
429
+ profile("vision-active", { isActive: true }),
430
+ profile("vision-profile", {}),
431
+ ];
432
+ visionProfiles = new Set(["vision-active", "vision-profile"]);
433
+ visionModels = new Set<string>(); // "text-only-model" is text-only
434
+ const ctx = makeToolCtx({
435
+ model: "text-only-model",
436
+ toolResponse: toolResult([imageBlock("shot1")]),
437
+ });
438
+ await postToolUse(ctx);
439
+ expect(ctx.toolResponse.contentBlocks![0].type).toBe("text");
440
+ });
401
441
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Default `post-tool-use` hook: when the active model is text-only, captions
2
+ * Default `post-tool-use` hook: when the turn's model is text-only, captions
3
3
  * the image blocks a tool returns (e.g. a `browser_screenshot`) and
4
4
  * substitutes the caption as a text block so the result stays sendable to a
5
5
  * provider that would otherwise reject the raw image.
@@ -9,30 +9,29 @@
9
9
  * rather than the top-level message content the `user-prompt-submit` hook
10
10
  * handles. Both share {@link captionImageBlocks}.
11
11
  *
12
- * The active model is resolved from the workspace's active profile — the
13
- * post-tool-use context carries the running model, and the active profile is
14
- * what the loop is executing this turn. If that profile supports vision, the
15
- * hook is a no-op and the image reaches the model untouched.
12
+ * Capability is read straight off `ctx.model` the provider-reported model id
13
+ * for the turn that issued this tool call — so the decision tracks the model
14
+ * that actually ran, including a text-only override. The substitution is in
15
+ * place, so the persisted/displayed tool result carries the caption too.
16
16
  */
17
17
 
18
18
  import {
19
19
  doesSupportVision,
20
- getModelProfiles,
21
- type PluginHookFn,
20
+ type HookFunction,
22
21
  type PostToolUseContext,
23
22
  } from "@vellumai/plugin-api";
24
23
 
25
24
  import { captionImageBlocks } from "../src/caption-blocks.js";
26
25
  import { findVisionProfile } from "../src/vision-caption.js";
27
26
 
28
- const postToolUse: PluginHookFn<PostToolUseContext> = async (ctx) => {
27
+ const postToolUse: HookFunction<PostToolUseContext> = async (ctx) => {
28
+ // Cheapest gate first: bail unless the tool actually returned an image,
29
+ // before touching the model catalog or resolving a vision profile.
29
30
  const blocks = ctx.toolResponse.contentBlocks;
30
- if (blocks == null || blocks.length === 0) return;
31
+ if (blocks == null || !blocks.some((b) => b.type === "image")) return;
31
32
 
32
- // If the active model already supports vision, leave the image in place.
33
- const activeProfile = getModelProfiles().find((p) => p.isActive);
34
- if (activeProfile == null) return;
35
- if (doesSupportVision(activeProfile)) return;
33
+ // If the model that ran already supports vision, leave the image in place.
34
+ if (doesSupportVision(ctx.model)) return;
36
35
 
37
36
  // Find a vision-capable profile for captioning.
38
37
  const visionProfileKey = findVisionProfile();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Default `user-prompt-submit` hook: when the active model is text-only,
2
+ * Default `user-prompt-submit` hook: when the turn's model is text-only,
3
3
  * captions image blocks via a vision-capable profile and substitutes the
4
4
  * caption as a text block so the model can still reason about the image's
5
5
  * content.
@@ -7,13 +7,14 @@
7
7
  * The hook runs once per user turn, after the assistant assembles
8
8
  * `latestMessages` and before they flow into `agentLoop.run()`. It:
9
9
  *
10
- * 1. Resolves the active profile from `modelProfileKey` (or the workspace's
11
- * active profile when the key is `null`) and checks `doesSupportVision`.
12
- * If the model already handles images, the hook is a no-op.
10
+ * 1. Checks whether the turn's model needs image→text fallback via
11
+ * {@link needsImageFallback} (resolving the turn's `modelProfileKey`, or the
12
+ * workspace's active profile when the key is `null`). If the model handles
13
+ * images, the hook is a no-op.
13
14
  * 2. Finds a vision-capable profile for captioning via `findVisionProfile`.
14
15
  * If none exists, images are replaced with a fail-open placeholder so the
15
16
  * model at least knows an image was present.
16
- * 3. Replaces each `ImageContent` block with a `[Image …]` text caption via
17
+ * 3. Replaces each image block with a `[Image …]` text caption via
17
18
  * {@link captionImageBlocks} (which also persists the original and caches
18
19
  * captions across turns).
19
20
  *
@@ -22,28 +23,19 @@
22
23
  */
23
24
 
24
25
  import {
25
- doesSupportVision,
26
- getModelProfiles,
27
- type PluginHookFn,
26
+ type HookFunction,
28
27
  type UserPromptSubmitContext,
29
28
  } from "@vellumai/plugin-api";
30
29
 
31
- import { captionImageBlocks } from "../src/caption-blocks.js";
30
+ import {
31
+ captionImageBlocks,
32
+ needsImageFallback,
33
+ } from "../src/caption-blocks.js";
32
34
  import { findVisionProfile } from "../src/vision-caption.js";
33
35
 
34
- const userPromptSubmit: PluginHookFn<UserPromptSubmitContext> = async (ctx) => {
35
- // Resolve the active profile from modelProfileKey, falling back to the
36
- // workspace's active profile when the key is null (profile unchanged since
37
- // the last notified turn).
38
- const profiles = getModelProfiles();
39
- const activeProfile =
40
- ctx.modelProfileKey != null
41
- ? profiles.find((p) => p.key === ctx.modelProfileKey)
42
- : profiles.find((p) => p.isActive);
43
- if (activeProfile == null) return;
44
-
45
- // If the active model already supports vision, nothing to do.
46
- if (doesSupportVision(activeProfile)) return;
36
+ const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
37
+ // If the turn's model already supports vision, nothing to do.
38
+ if (!needsImageFallback(ctx.modelProfileKey)) return;
47
39
 
48
40
  // Find a vision-capable profile for captioning.
49
41
  const visionProfileKey = findVisionProfile();
@@ -1,31 +1,62 @@
1
1
  /**
2
2
  * Shared image→text substitution for the image-fallback plugin's hooks.
3
3
  *
4
- * Two hooks replace `image` content blocks with a text caption when the active
4
+ * Two hooks replace `image` content blocks with a text caption when the turn's
5
5
  * model can't process images: `user-prompt-submit` handles user-attached
6
6
  * images, and `post-tool-use` handles images a tool returns (e.g. a browser
7
- * screenshot). This module holds the per-block substitution they share —
8
- * persist the original image to a known location, caption it via a
9
- * vision-capable profile, and swap in a `[Image …]` text block.
7
+ * screenshot). This module holds what they share — deciding whether a profile
8
+ * needs the fallback ({@link needsImageFallback}) and the per-block
9
+ * substitution ({@link captionImageBlocks}): persist the original image to a
10
+ * known location, caption it via a vision-capable profile, and swap in a
11
+ * `[Image …]` text block.
10
12
  *
11
- * The caption text states up front that the active model can't view images and
12
- * the image was auto-described to text, so the model treats the block as a
13
- * derived description rather than a verbatim transcript.
13
+ * The substitution mutates the blocks in place, so the caption replaces the
14
+ * image everywhere the block is referenced (the provider-bound history and the
15
+ * persisted/displayed copy alike) a text-only turn does not keep the raw
16
+ * image around.
17
+ *
18
+ * The caption text states up front that the model can't view images and the
19
+ * image was auto-described to text, so the model treats the block as a derived
20
+ * description rather than a verbatim transcript.
14
21
  *
15
22
  * Fail-open is the dominant error mode: a captioning failure leaves a
16
23
  * placeholder text block rather than the raw image (which a text-only provider
17
24
  * would reject) or nothing (which would lose information).
18
25
  */
19
26
 
20
- import type {
21
- ContentBlock,
22
- ImageContent,
23
- PluginLogger,
27
+ import {
28
+ type ContentBlock,
29
+ doesSupportVision,
30
+ getModelProfiles,
31
+ type ImageContent,
32
+ type PluginLogger,
24
33
  } from "@vellumai/plugin-api";
25
34
 
26
35
  import { persistImage } from "./image-persist.js";
27
36
  import { captionImage } from "./vision-caption.js";
28
37
 
38
+ /**
39
+ * Whether the profile a turn runs needs image→text fallback (i.e. it can't
40
+ * process images itself).
41
+ *
42
+ * Used by `user-prompt-submit`, whose context carries the profile key rather
43
+ * than the resolved model id: prefer the turn's `modelProfileKey` — which
44
+ * carries a text-only override even when the workspace's active profile is
45
+ * vision-capable — and fall back to the active profile only when the key is
46
+ * `null` (profile unchanged since the last notified turn). Returns `false` when
47
+ * no profile resolves or the resolved model already supports vision, in which
48
+ * case the image reaches the model untouched.
49
+ */
50
+ export function needsImageFallback(modelProfileKey: string | null): boolean {
51
+ const profiles = getModelProfiles();
52
+ const activeProfile =
53
+ modelProfileKey != null
54
+ ? profiles.find((p) => p.key === modelProfileKey)
55
+ : profiles.find((p) => p.isActive);
56
+ if (activeProfile == null) return false;
57
+ return !doesSupportVision(activeProfile);
58
+ }
59
+
29
60
  /**
30
61
  * Replace every `image` block in `blocks` (in place) with a text caption so a
31
62
  * text-only model can still reason about the image's content. Returns the
@@ -24,7 +24,7 @@
24
24
  * empty-response hook; only a provider rejection is this hook's to act on.
25
25
  */
26
26
 
27
- import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
27
+ import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
28
28
 
29
29
  import { isImageDimensionsTooLargeError } from "../detect.js";
30
30
  import {
@@ -36,7 +36,7 @@ import {
36
36
  recoverOversizedImages,
37
37
  } from "../recover.js";
38
38
 
39
- const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
39
+ const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
40
40
  if (
41
41
  ctx.error &&
42
42
  isImageDimensionsTooLargeError(ctx.error.message) &&
@@ -11,11 +11,11 @@
11
11
  * retry the loop's per-run backstop refused).
12
12
  */
13
13
 
14
- import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
14
+ import type { HookFunction, StopContext } from "@vellumai/plugin-api";
15
15
 
16
16
  import { clearImageRecoveryAttempted } from "../image-recovery-state-store.js";
17
17
 
18
- const stop: PluginHookFn<StopContext> = async (ctx) => {
18
+ const stop: HookFunction<StopContext> = async (ctx) => {
19
19
  clearImageRecoveryAttempted(ctx.conversationId);
20
20
  };
21
21
 
@@ -24,15 +24,8 @@
24
24
  * {@link registerDefaultPlugins} at call time.
25
25
  */
26
26
 
27
- import { finalizeTool } from "../../tools/tool-defaults.js";
28
27
  import { registerPlugin, resetPluginRegistryForTests } from "../registry.js";
29
28
  import { type Plugin, PluginExecutionError } from "../types.js";
30
- import { resetAdvisorStateForTests } from "./advisor/advisor-state-store.js";
31
- import advisorPostModelCall from "./advisor/hooks/post-model-call.js";
32
- import advisorPreModelCall from "./advisor/hooks/pre-model-call.js";
33
- import advisorUserPromptSubmit from "./advisor/hooks/user-prompt-submit.js";
34
- import advisorPkg from "./advisor/package.json" with { type: "json" };
35
- import advisorTool from "./advisor/tools/advisor.js";
36
29
  import compactionPkg from "./compaction/package.json" with { type: "json" };
37
30
  import emptyResponsePostModelCall from "./empty-response/hooks/post-model-call.js";
38
31
  import emptyResponseStop from "./empty-response/hooks/stop.js";
@@ -329,30 +322,6 @@ export const defaultToolResultTruncatePlugin: Plugin = {
329
322
  },
330
323
  };
331
324
 
332
- /**
333
- * `advisor` — adds the model-visible `advisor` tool: a no-argument tool the
334
- * model calls to consult a stronger inference profile (the `inference` call
335
- * site with a `quality-optimized` override) on the full transcript, routed
336
- * through the assistant's own inference. Three hooks feed it: `user-prompt-submit`
337
- * seeds the capture, `pre-model-call` records the executor's system prompt and
338
- * injects the steering that nudges the model to consult, and `post-model-call`
339
- * snapshots the transcript the tool reads. `finalizeTool` fills the tool's
340
- * defaults so it satisfies `Tool`, and `bootstrapPlugins` registers it into the
341
- * catalog.
342
- */
343
- export const defaultAdvisorPlugin: Plugin = {
344
- manifest: {
345
- name: advisorPkg.name,
346
- version: advisorPkg.version,
347
- },
348
- hooks: {
349
- "user-prompt-submit": advisorUserPromptSubmit,
350
- "pre-model-call": advisorPreModelCall,
351
- "post-model-call": advisorPostModelCall,
352
- },
353
- tools: [finalizeTool(advisorTool, "advisor")],
354
- };
355
-
356
325
  /**
357
326
  * Full set of first-party default plugins. Used by
358
327
  * {@link registerDefaultPlugins} to drive the registration loop; the array
@@ -375,9 +344,6 @@ function getAllDefaultPlugins(): readonly Plugin[] {
375
344
  defaultCompactionPlugin,
376
345
  defaultTitleGeneratePlugin,
377
346
  memoryV3ShadowPlugin,
378
- // Registered last so its capture hooks observe the fully-processed turn
379
- // (memory injections, history repair) that the executor actually sees.
380
- defaultAdvisorPlugin,
381
347
  ];
382
348
  }
383
349
 
@@ -424,7 +390,6 @@ export function resetPluginRegistryAndRegisterDefaults(): void {
424
390
  resetExplorationDriftStateForTests();
425
391
  resetTaskProgressNudgeStateForTests();
426
392
  resetSurfaceCompletionNudgeStoreForTests();
427
- resetAdvisorStateForTests();
428
393
  resetCaptionCacheForTests();
429
394
  registerDefaultPlugins();
430
395
  }
@@ -30,7 +30,7 @@
30
30
  * reject.
31
31
  */
32
32
 
33
- import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
33
+ import type { HookFunction, PostModelCallContext } from "@vellumai/plugin-api";
34
34
 
35
35
  import { isMaxTokensStopReason } from "../../../../agent/loop.js";
36
36
  import {
@@ -45,7 +45,7 @@ import {
45
45
  export const MAX_TOKENS_CONTINUE_NUDGE_TEXT =
46
46
  "<system_notice>Your previous response was cut off because it reached the maximum output length. Continue exactly where you stopped — do not repeat content you already sent and do not start over.</system_notice>";
47
47
 
48
- const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
48
+ const postModelCall: HookFunction<PostModelCallContext> = async (ctx) => {
49
49
  if (ctx.error) return;
50
50
  if (!isMaxTokensStopReason(ctx.stopReason)) return;
51
51
  if (ctx.callSite !== "mainAgent") return;
@@ -9,11 +9,11 @@
9
9
  * budget, no matter how the turn ended.
10
10
  */
11
11
 
12
- import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
12
+ import type { HookFunction, StopContext } from "@vellumai/plugin-api";
13
13
 
14
14
  import { clearMaxTokensContinueBudget } from "../continue-state-store.js";
15
15
 
16
- const stop: PluginHookFn<StopContext> = async (ctx) => {
16
+ const stop: HookFunction<StopContext> = async (ctx) => {
17
17
  clearMaxTokensContinueBudget(ctx.conversationId);
18
18
  };
19
19
 
@@ -38,7 +38,7 @@
38
38
  * {@link PostCompactContext}.
39
39
  */
40
40
 
41
- import type { PluginHookFn, PostCompactContext } from "@vellumai/plugin-api";
41
+ import type { HookFunction, PostCompactContext } from "@vellumai/plugin-api";
42
42
 
43
43
  import { getConfig } from "../../../../config/loader.js";
44
44
  import { findConversationOrSubagent } from "../../../../daemon/conversation-registry.js";
@@ -53,7 +53,7 @@ import {
53
53
  import { getLiveGraphMemory } from "../../../../memory/graph/conversation-graph-memory.js";
54
54
  import { stripTailInjectionsForReinjection } from "../tail-reinjection-strip.js";
55
55
 
56
- const postCompact: PluginHookFn<PostCompactContext> = async (ctx) => {
56
+ const postCompact: HookFunction<PostCompactContext> = async (ctx) => {
57
57
  const {
58
58
  history,
59
59
  requestId,
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  import type {
31
- PluginHookFn,
31
+ HookFunction,
32
32
  UserPromptSubmitContext,
33
33
  } from "@vellumai/plugin-api";
34
34
 
@@ -264,7 +264,7 @@ function persistInjectionBlocks(
264
264
  * slower turn. Cancellation still works via `ctx.signal`, which is threaded
265
265
  * into `prepareMemory`.
266
266
  */
267
- const userPromptSubmitMemoryRetrieval: PluginHookFn<
267
+ const userPromptSubmitMemoryRetrieval: HookFunction<
268
268
  UserPromptSubmitContext
269
269
  > = async (ctx) => {
270
270
  // The conversation-scoped retrieval state — graph handle, abort signal, and
@@ -67,13 +67,17 @@ let pruneConfig: {
67
67
  maxResidentBytes: number;
68
68
  targetResidentBytes: number;
69
69
  } | null = null;
70
- /** Canned orchestrate result per turnIndex; `null` simulates a failed turn. */
71
- let turnResults = new Map<number, OrchestrateResult | null>();
70
+ /** Canned orchestrate result per turnIndex; `null` simulates an ordinary miss. */
71
+ let turnResults = new Map<number, OrchestrateResult | null | Error>();
72
72
  const observeTurnSpy = mock(
73
73
  async (
74
74
  _conversationId: string,
75
75
  turnIndex: number,
76
- ): Promise<OrchestrateResult | null> => turnResults.get(turnIndex) ?? null,
76
+ ): Promise<OrchestrateResult | null> => {
77
+ const value = turnResults.get(turnIndex) ?? null;
78
+ if (value instanceof Error) throw value;
79
+ return value;
80
+ },
77
81
  );
78
82
 
79
83
  const logCalls: Array<{ data: unknown; msg: string }> = [];
@@ -199,6 +203,9 @@ const {
199
203
  } = await import("../ever-injected-store.js");
200
204
  const { V3_CARDS_INJECTION_HEADER } = await import("../render-injection.js");
201
205
  const { flushPruneValveForTests } = await import("../prune.js");
206
+ const { drainConversationNotices, resetConversationNoticesForTests } =
207
+ await import("../../../../daemon/conversation-notices.js");
208
+ const { MemoryV3RetrievalUnavailableError } = await import("../pool-select.js");
202
209
 
203
210
  // ─── helpers ────────────────────────────────────────────────────────────────
204
211
 
@@ -313,6 +320,7 @@ beforeEach(async () => {
313
320
  logCalls.length = 0;
314
321
  testDb = makeDb();
315
322
  resetMemoryV3InjectorStateForTests();
323
+ resetConversationNoticesForTests();
316
324
  });
317
325
 
318
326
  afterAll(async () => {
@@ -335,6 +343,28 @@ describe("memoryV3Injector — frozen net-new cards", () => {
335
343
  expect(getActiveSlugs("conv-1")).toEqual(new Set());
336
344
  });
337
345
 
346
+ test("live retrieval failure queues a degraded-memory notice", async () => {
347
+ liveEnabled = true;
348
+ turnResults.set(
349
+ 0,
350
+ new MemoryV3RetrievalUnavailableError("selector unavailable"),
351
+ );
352
+
353
+ await expect(produceCardsWithoutCommit("conv-1", 0)).resolves.toBeNull();
354
+
355
+ expect(drainConversationNotices("conv-1")).toEqual([
356
+ {
357
+ type: "conversation_notice",
358
+ conversationId: "conv-1",
359
+ source: "memory_v3",
360
+ code: "UNKNOWN",
361
+ userMessage:
362
+ "Memory is temporarily unavailable, so this response may not use your saved memories. You can retry in a moment.",
363
+ errorCategory: "memory_v3_degraded",
364
+ },
365
+ ]);
366
+ });
367
+
338
368
  test("turn 1 renders cards; turn 2 re-selecting the same pages renders ZERO new cards", async () => {
339
369
  liveEnabled = true;
340
370
  turnResults.set(0, result(["page-a", "page-b"]));
@@ -26,6 +26,7 @@
26
26
  * The provider is stubbed so no network calls fire; mirrors selector.test.ts.
27
27
  */
28
28
 
29
+ import { createRequire } from "node:module";
29
30
  import { beforeEach, describe, expect, mock, test } from "bun:test";
30
31
 
31
32
  import type {
@@ -35,6 +36,7 @@ import type {
35
36
  SendMessageOptions,
36
37
  ToolUseContent,
37
38
  } from "../../../../providers/types.js";
39
+ import { ProviderError } from "../../../../util/errors.js";
38
40
  import type { MemoryRoutingTurn } from "../types.js";
39
41
 
40
42
  // ---------------------------------------------------------------------------
@@ -43,6 +45,11 @@ import type { MemoryRoutingTurn } from "../types.js";
43
45
  // ---------------------------------------------------------------------------
44
46
 
45
47
  let providerStub: Provider | null = null;
48
+ const registryReal = {
49
+ ...(createRequire(import.meta.url)(
50
+ "../../../../providers/registry.js",
51
+ ) as Record<string, unknown>),
52
+ };
46
53
 
47
54
  interface ProviderCall {
48
55
  messages: Message[];
@@ -57,6 +64,12 @@ mock.module("../../../../providers/provider-send-message.js", () => ({
57
64
  response.content.find((b): b is ToolUseContent => b.type === "tool_use"),
58
65
  }));
59
66
 
67
+ mock.module("../../../../providers/registry.js", () => ({
68
+ ...registryReal,
69
+ getProviderRoutingSource: (providerName: string) =>
70
+ providerName === "managed" ? "managed-proxy" : "user-key",
71
+ }));
72
+
60
73
  mock.module("../../../../util/logger.js", () => ({
61
74
  getLogger: () => ({
62
75
  warn: (...args: unknown[]) => warnCalls.push({ args }),
@@ -253,10 +266,9 @@ describe("selectPool — id mapping", () => {
253
266
  });
254
267
 
255
268
  // ---------------------------------------------------------------------------
256
- // selectPool — infrastructure failures THROW (no silent degradation). A
257
- // deliberate empty selection and an empty pool (covered above) still return
258
- // normally; only a genuine infra failure throws so the LIVE injector can
259
- // hard-fail the turn instead of shipping it with no memory.
269
+ // selectPool — infrastructure failures THROW. A deliberate empty selection and
270
+ // an empty pool (covered above) still return normally; only a genuine infra
271
+ // failure throws so callers can log it distinctly from an empty selection.
260
272
  // ---------------------------------------------------------------------------
261
273
 
262
274
  describe("selectPool — infrastructure failures throw", () => {
@@ -371,6 +383,38 @@ describe("selectPool — infrastructure failures throw", () => {
371
383
  ]);
372
384
  });
373
385
 
386
+ test("managed provider 402 attaches a non-terminal credits notice", async () => {
387
+ providerStub = {
388
+ name: "managed",
389
+ sendMessage: async (messages, options) => {
390
+ providerCalls.push({ messages, options });
391
+ throw new ProviderError(
392
+ "Together AI API error (402): 402 status code (no body)",
393
+ "managed",
394
+ 402,
395
+ );
396
+ },
397
+ };
398
+ let caught: unknown;
399
+ try {
400
+ await selectPool(makePool(), makeTurn("x"));
401
+ } catch (err) {
402
+ caught = err;
403
+ }
404
+
405
+ expect(caught).toBeInstanceOf(MemoryV3RetrievalUnavailableError);
406
+ const notice = (
407
+ caught as InstanceType<typeof MemoryV3RetrievalUnavailableError>
408
+ ).conversationNotice;
409
+ expect(notice).toEqual({
410
+ source: "memory_v3",
411
+ code: "PROVIDER_BILLING",
412
+ userMessage:
413
+ "You've run out of credits. Add funds to continue using the assistant.",
414
+ errorCategory: "credits_exhausted",
415
+ });
416
+ });
417
+
374
418
  test("provider throw redacts sensitive message details in diagnostics", async () => {
375
419
  const providerSecret = ["sk-proj-", "a".repeat(40)].join("");
376
420
  const message = `provider rejected Authorization: Bearer ${providerSecret}`;