@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -19,19 +19,26 @@ import { RiskLevel } from "@vellumai/plugin-api";
19
19
  import { advisorEnabledForProfile } from "../advisor-gate.js";
20
20
  import { getCapture } from "../advisor-state-store.js";
21
21
  import { consultAdvisor } from "../consult.js";
22
+ import { buildAdvisorContext } from "../context-pack.js";
22
23
 
23
24
  const advisorTool: ToolDefinition = {
24
25
  name: "advisor",
25
26
  description:
26
27
  "Consult a stronger advisor model to shape your plan and get strategic guidance. " +
27
28
  "Takes NO parameters — your full conversation (the task, every tool call, and every " +
28
- "result) is forwarded automatically. Call it BEFORE you start building: it can lay out " +
29
- "a plan when you don't have one yet, or review and sharpen the plan you've already " +
30
- "drafted. Also call it when you're stuck, when weighing a change in approach, and once " +
31
- "before declaring a task complete. Give its guidance serious weight.",
29
+ "result) is forwarded automatically, along with your available tools and skills, the " +
30
+ "workspace/project context, and relevant memory. Call it BEFORE you start building: it " +
31
+ "can lay out a plan when you don't have one yet, or review and sharpen the plan you've " +
32
+ "already drafted. Also call it when you're stuck, when weighing a change in approach, and " +
33
+ "once before declaring a task complete. It runs on its own — if you call it alongside " +
34
+ "other tools, those are held back until you've seen its guidance. Give its guidance " +
35
+ "serious weight.",
32
36
  input_schema: { type: "object", properties: {}, additionalProperties: false },
33
37
  // Read-only advice; low risk so the consult isn't gated behind a prompt.
34
38
  defaultRiskLevel: RiskLevel.Low,
39
+ // Runs alone in its turn: the loop defers any sibling tool calls so the model
40
+ // incorporates the advisor's guidance before acting on anything else.
41
+ exclusive: true,
35
42
  async execute(
36
43
  _input: Record<string, unknown>,
37
44
  ctx: ToolContext,
@@ -48,10 +55,30 @@ const advisorTool: ToolDefinition = {
48
55
  }
49
56
  try {
50
57
  const capture = getCapture(ctx.conversationId);
58
+ const messages = capture?.messages ?? [];
59
+ // Gather the agent's situational context (tools, skills, workspace,
60
+ // memory) so the advisor reasons with the same awareness the agent has.
61
+ // Best-effort: a failure here must not block the consult.
62
+ const runtimeContext = await buildAdvisorContext({
63
+ conversationId: ctx.conversationId,
64
+ workingDir: ctx.workingDir,
65
+ allowedToolNames: ctx.allowedToolNames,
66
+ // Per-turn trust snapshot — gates personal-memory surfaces off the same
67
+ // values the executor captured for this invocation, not live state.
68
+ trustClass: ctx.trustClass,
69
+ sourceChannel: ctx.executionChannel,
70
+ transcript: messages,
71
+ signal: ctx.signal,
72
+ }).catch(() => null);
51
73
  const advice = await consultAdvisor({
52
74
  systemPrompt: capture?.systemPrompt ?? null,
53
- messages: capture?.messages ?? [],
75
+ messages,
76
+ runtimeContext,
54
77
  signal: ctx.signal,
78
+ // Stream the advisor's guidance live as it generates: each delta is
79
+ // surfaced as a `tool_output_chunk` for this tool call and rendered in
80
+ // the tool-detail drawer. The complete text is still returned below.
81
+ onText: (chunk) => ctx.onOutput?.(chunk),
55
82
  });
56
83
  return { content: advice, isError: false };
57
84
  } catch (err) {
@@ -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,15 +9,14 @@
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
20
  type PluginHookFn,
22
21
  type PostToolUseContext,
23
22
  } from "@vellumai/plugin-api";
@@ -26,13 +25,13 @@ import { captionImageBlocks } from "../src/caption-blocks.js";
26
25
  import { findVisionProfile } from "../src/vision-caption.js";
27
26
 
28
27
  const postToolUse: PluginHookFn<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
26
  type PluginHookFn,
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
36
  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;
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
@@ -119,6 +119,10 @@ export interface OrchestrateDeps {
119
119
  /** Hard cap on total learned-lane surfaced articles; `0` disables the pass
120
120
  * (canonical value: `memory.v3.learnedEdges.cap`). */
121
121
  learnedCap?: number;
122
+ /** The selector's system prompt. Omitted → the selector's bundled default.
123
+ * The live caller resolves `memory.v3.selectorPromptPath` (workspace-relative
124
+ * file override) via `resolveSelectorPrompt` and threads the result here. */
125
+ selectorPrompt?: string;
122
126
  }
123
127
 
124
128
  /** A finder-lane candidate: the slug, the descriptor that justified it, and
@@ -378,8 +382,13 @@ export async function orchestrate(
378
382
 
379
383
  // Step 3: a SINGLE forced-tool select over the cache-ordered pool. The
380
384
  // selections come back slug-deduped (pinned flags ORed) — `selectPool`'s
381
- // contract.
382
- const selections = await selectPool({ stable, finder: finderTail }, turn);
385
+ // contract. `selectorPrompt` is the (optionally overridden) instruction
386
+ // scaffold; `undefined` falls through to the bundled default.
387
+ const selections = await selectPool(
388
+ { stable, finder: finderTail },
389
+ turn,
390
+ deps.selectorPrompt,
391
+ );
383
392
 
384
393
  return {
385
394
  selections,
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Tests for `pool-select.ts` `selectPool` — focused on error SURFACING:
3
+ * - a provider call that THROWS on every attempt surfaces the underlying
4
+ * provider error (e.g. an upstream HTTP 4xx) in the thrown
5
+ * `MemoryV3RetrievalUnavailableError` message, rather than the generic
6
+ * "no usable selection" string that hid it;
7
+ * - a 200 response carrying no usable `tool_use` still throws the generic
8
+ * "no usable selection" message;
9
+ * - happy paths preserved: explicit ids → selection, omitted ids → keepAll,
10
+ * empty pool → [].
11
+ *
12
+ * `mock.module` is process-global and leaks into sibling files in a directory
13
+ * run, so the provider-send-message stub DELEGATES to the real implementation
14
+ * (keeping the real `extractToolUse`) unless this test is actively running
15
+ * (`selectMockActive`) — mirrors `prune.test.ts` / `ever-injected-store.test.ts`.
16
+ */
17
+
18
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ import type {
21
+ ContentBlock,
22
+ Provider,
23
+ ProviderResponse,
24
+ } from "../../../providers/types.js";
25
+ import type { MemoryRoutingTurn, Slug } from "./types.js";
26
+
27
+ const realProviderSend = {
28
+ ...(await import("../../../providers/provider-send-message.js")),
29
+ };
30
+
31
+ let selectMockActive = false;
32
+ let sendMessageImpl: (() => Promise<ProviderResponse>) | null = null;
33
+
34
+ const mockProvider = {
35
+ name: "mock-memory-v3-selector",
36
+ async sendMessage(): Promise<ProviderResponse> {
37
+ if (!sendMessageImpl) throw new Error("sendMessageImpl not configured");
38
+ return sendMessageImpl();
39
+ },
40
+ } as unknown as Provider;
41
+
42
+ mock.module("../../../providers/provider-send-message.js", () => ({
43
+ ...realProviderSend,
44
+ getConfiguredProvider: (
45
+ ...args: Parameters<typeof realProviderSend.getConfiguredProvider>
46
+ ) =>
47
+ selectMockActive
48
+ ? Promise.resolve(mockProvider)
49
+ : realProviderSend.getConfiguredProvider(...args),
50
+ }));
51
+
52
+ const { MemoryV3RetrievalUnavailableError, selectPool } =
53
+ await import("./pool-select.js");
54
+
55
+ function response(content: ContentBlock[]): ProviderResponse {
56
+ return {
57
+ content,
58
+ model: "mock",
59
+ usage: { inputTokens: 0, outputTokens: 0 },
60
+ } as ProviderResponse;
61
+ }
62
+
63
+ const turn: MemoryRoutingTurn = {
64
+ conversationId: "conv-1",
65
+ turnNumber: 0,
66
+ currentMessage: "echo something back",
67
+ recentContext: "",
68
+ };
69
+
70
+ const pool = {
71
+ stable: [],
72
+ finder: [{ slug: "page-a" as Slug, descriptor: "a descriptor" }],
73
+ };
74
+
75
+ describe("selectPool", () => {
76
+ beforeEach(() => {
77
+ selectMockActive = true;
78
+ sendMessageImpl = null;
79
+ });
80
+ afterAll(() => {
81
+ selectMockActive = false;
82
+ });
83
+
84
+ test("a provider throw surfaces the underlying error in the thrown message", async () => {
85
+ const upstream = "Together AI API error (400): 400 status code (no body)";
86
+ sendMessageImpl = async () => {
87
+ throw new Error(upstream);
88
+ };
89
+ let caught: unknown;
90
+ try {
91
+ await selectPool(pool, turn);
92
+ } catch (err) {
93
+ caught = err;
94
+ }
95
+ expect(caught).toBeInstanceOf(MemoryV3RetrievalUnavailableError);
96
+ expect((caught as Error).message).toContain(
97
+ "provider call failed after retries",
98
+ );
99
+ // The real upstream error is no longer hidden behind the generic message.
100
+ expect((caught as Error).message).toContain(upstream);
101
+ });
102
+
103
+ test("a 200 with no usable tool_use throws the generic message", async () => {
104
+ sendMessageImpl = async () =>
105
+ response([{ type: "text", text: "I cannot call the tool." }]);
106
+ let caught: unknown;
107
+ try {
108
+ await selectPool(pool, turn);
109
+ } catch (err) {
110
+ caught = err;
111
+ }
112
+ expect(caught).toBeInstanceOf(MemoryV3RetrievalUnavailableError);
113
+ expect((caught as Error).message).toBe(
114
+ "memory-v3 pool selector returned no usable selection after retries",
115
+ );
116
+ });
117
+
118
+ test("explicit ids select the matching candidates", async () => {
119
+ sendMessageImpl = async () =>
120
+ response([
121
+ {
122
+ type: "tool_use",
123
+ id: "call-1",
124
+ name: "select_pages",
125
+ input: { ids: [1] },
126
+ },
127
+ ]);
128
+ expect(await selectPool(pool, turn)).toEqual([
129
+ { slug: "page-a", pinned: false },
130
+ ]);
131
+ });
132
+
133
+ test("omitted ids keep all candidates (recall-safe)", async () => {
134
+ sendMessageImpl = async () =>
135
+ response([
136
+ { type: "tool_use", id: "call-1", name: "select_pages", input: {} },
137
+ ]);
138
+ expect(await selectPool(pool, turn)).toEqual([
139
+ { slug: "page-a", pinned: false },
140
+ ]);
141
+ });
142
+
143
+ test("an empty candidate pool returns no selections", async () => {
144
+ expect(await selectPool({ stable: [], finder: [] }, turn)).toEqual([]);
145
+ });
146
+ });
@@ -50,6 +50,7 @@
50
50
 
51
51
  import { z } from "zod";
52
52
 
53
+ import { loadPromptOverride } from "../../../memory/prompt-override.js";
53
54
  import { cachedTextBlock } from "../../../providers/cache-control.js";
54
55
  import {
55
56
  extractToolUse,
@@ -201,6 +202,28 @@ A page can be relevant because of the current situation — the date or the live
201
202
 
202
203
  If the conversation is centrally ABOUT a page (rather than only peripherally relevant to it), mark that page as pinned. Call \`select_pages\` with the chosen IDs. Omit \`ids\` only as a recall-safe fallback when you cannot judge the pool (keeps every candidate); return \`[]\` when candidates are present but none are relevant.`;
203
204
 
205
+ /**
206
+ * Resolve the selector system prompt: the file at `overridePath` when it is set
207
+ * and usable, otherwise the bundled {@link SYSTEM_PROMPT}. Path resolution and
208
+ * fallback follow the shared override loader (workspace-relative; a missing,
209
+ * empty, oversized, or unreadable file degrades to the bundled prompt with a
210
+ * warning). The selector prompt takes no placeholders — the candidate pool is
211
+ * the user message — so an override file is used verbatim.
212
+ */
213
+ export function resolveSelectorPrompt(
214
+ overridePath: string | null,
215
+ workspaceDir: string,
216
+ ): string {
217
+ return (
218
+ loadPromptOverride({
219
+ overridePath,
220
+ workspaceDir,
221
+ log,
222
+ label: "memory-v3 selector prompt",
223
+ }) ?? SYSTEM_PROMPT
224
+ );
225
+ }
226
+
204
227
  /** Collapse a descriptor to one line and cap its length for a finder line. */
205
228
  function renderSnippet(descriptor: string): string {
206
229
  return truncate(descriptor.replace(/\s+/g, " ").trim(), SNIPPET_MAX_CHARS);
@@ -315,10 +338,15 @@ function dedupeBySlug(
315
338
  * relevant" signal); an explicit `[]` keeps none; an infrastructure failure
316
339
  * (after a short re-prompt retry) keeps none, degrading to the deterministic
317
340
  * recall lanes the orchestrator unions in.
341
+ *
342
+ * `systemPrompt` is the selector's instruction scaffold; it defaults to the
343
+ * bundled {@link SYSTEM_PROMPT} and is overridable via `memory.v3.selectorPromptPath`
344
+ * (resolved by {@link resolveSelectorPrompt} at the call site).
318
345
  */
319
346
  export async function selectPool(
320
347
  pool: SelectorPool,
321
348
  turn: MemoryRoutingTurn,
349
+ systemPrompt: string = SYSTEM_PROMPT,
322
350
  ): Promise<SelectedPage[]> {
323
351
  // The concatenated numbering: ids 1…m are the stable-prefix cards, ids
324
352
  // m+1… are the finder lines.
@@ -401,7 +429,7 @@ export async function selectPool(
401
429
  try {
402
430
  response = await provider.sendMessage([userMsg], {
403
431
  tools: [SELECT_PAGES_TOOL],
404
- systemPrompt: SYSTEM_PROMPT,
432
+ systemPrompt,
405
433
  config: {
406
434
  callSite: MEMORY_V3_SELECT_CALL_SITE,
407
435
  tool_choice: { type: "tool" as const, name: SELECT_PAGES_TOOL_NAME },
@@ -56,7 +56,10 @@ import { computeHotSet } from "./hot-set.js";
56
56
  import { computeLearnedEdgeGraph } from "./learned-edges.js";
57
57
  import type { OrchestrateResult } from "./orchestrate.js";
58
58
  import { orchestrate } from "./orchestrate.js";
59
- import { MemoryV3RetrievalUnavailableError } from "./pool-select.js";
59
+ import {
60
+ MemoryV3RetrievalUnavailableError,
61
+ resolveSelectorPrompt,
62
+ } from "./pool-select.js";
60
63
  import { ensureSectionCollection } from "./section-dense-store.js";
61
64
  import type { SectionNeedle } from "./section-needle.js";
62
65
  import { buildSectionNeedle } from "./section-needle.js";
@@ -576,6 +579,10 @@ export async function observeTurn(
576
579
  learnedGraph: lanes.learnedGraph,
577
580
  learnedPerSeed: v3.learnedEdges.perSeed,
578
581
  learnedCap: v3.learnedEdges.cap,
582
+ selectorPrompt: resolveSelectorPrompt(
583
+ v3.selectorPromptPath,
584
+ getWorkspaceDir(),
585
+ ),
579
586
  });
580
587
 
581
588
  // A zero-selection turn over a non-trivial pool is unusual enough to be
@@ -18,7 +18,13 @@
18
18
  * cached entry.
19
19
  */
20
20
 
21
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ readdirSync,
25
+ readFileSync,
26
+ statSync,
27
+ } from "node:fs";
22
28
  import { join } from "node:path";
23
29
 
24
30
  import { getConfig } from "../config/loader.js";
@@ -648,7 +654,6 @@ export async function populateCacheAtBoot(
648
654
  try {
649
655
  const initContext: PluginInitContext = {
650
656
  config: getConfig().plugins?.[pluginName],
651
- credentials: {},
652
657
  logger: log.child({ plugin: pluginName }),
653
658
  pluginStorageDir: ensurePluginStorageDir(pluginName),
654
659
  assistantVersion: APP_VERSION,
@@ -47,8 +47,6 @@ export interface PluginManifest {
47
47
  * own version at load time.
48
48
  */
49
49
  version: string;
50
- /** Credential keys the plugin needs resolved before `init()` runs. */
51
- requiresCredential?: string[];
52
50
  /**
53
51
  * Assistant feature-flag keys that must all be enabled for this plugin to
54
52
  * activate. Checked by `bootstrapPlugins` via `isAssistantFeatureFlagEnabled`
@@ -786,6 +786,11 @@ export class AnthropicProvider implements Provider {
786
786
  this.useNativeWebSearch = options.useNativeWebSearch ?? false;
787
787
  }
788
788
 
789
+ /** See {@link Provider.supportsNativeWebSearch}. */
790
+ get supportsNativeWebSearch(): boolean {
791
+ return this.useNativeWebSearch;
792
+ }
793
+
789
794
  async sendMessage(
790
795
  messages: Message[],
791
796
  options?: SendMessageOptions,