@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
@@ -30,6 +30,9 @@ import type {
30
30
  * Returns true when a guardian channel was found and revoked, false otherwise.
31
31
  */
32
32
  export function revokeGuardianBinding(channel: string): boolean {
33
+ // Local-store read, not the gateway: this read selects the row that the
34
+ // updateChannelStatus write below mutates, so it must stay transactionally
35
+ // consistent with that write. Leave for Combo 11 / gateway-bootstrap-binding.
33
36
  const guardian = findGuardianForChannel(channel);
34
37
  if (!guardian) return false;
35
38
 
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Gateway-backed guardian binding + delivery reader.
3
+ *
4
+ * Resolves the active guardian binding(s) and their per-channel delivery
5
+ * endpoints from the gateway via the `resolve_guardian_delivery` IPC route,
6
+ * validating the response against {@link ResolveGuardianDeliveryResponseSchema}.
7
+ *
8
+ * Guardian binding is near-static — it only changes on guardian onboarding /
9
+ * verification or revocation — yet this reader sits on many hot paths. To keep
10
+ * those paths off the IPC, results are cached behind a minutes-scale TTL and
11
+ * coalesced single-flight so a cold cache storms the gateway at most once.
12
+ *
13
+ * Freshness comes from two sources: the {@link invalidateGuardianDeliveryCache}
14
+ * subscription to `onContactChange` (contact mutations clear the cache), and
15
+ * {@link getGuardianDeliveryFresh} reads on existence guards (gateway-side
16
+ * binding writes don't invalidate the daemon cache, so those paths read fresh).
17
+ *
18
+ * Returns `null` on ANY failure (transport failure, malformed shape, timeout,
19
+ * or thrown error); failures are NOT cached, so a recovered gateway is retried
20
+ * on the next call.
21
+ */
22
+
23
+ import {
24
+ type GuardianDelivery,
25
+ ResolveGuardianDeliveryResponseSchema,
26
+ } from "@vellumai/gateway-client";
27
+
28
+ import { ipcCall } from "../ipc/gateway-client.js";
29
+ import { onContactChange } from "./contact-events.js";
30
+
31
+ // Short IPC timeout so the read resolves promptly rather than stalling a hot
32
+ // path on a gateway that accepts the socket but hangs.
33
+ const GUARDIAN_DELIVERY_IPC_TIMEOUT_MS = 2_000;
34
+
35
+ // Guardian binding is near-static, so a minutes-scale TTL is safe; freshness is
36
+ // driven primarily by event-based invalidation, not by this backstop expiry.
37
+ const GUARDIAN_DELIVERY_CACHE_TTL_MS = 300_000;
38
+
39
+ interface CacheEntry {
40
+ guardians: GuardianDelivery[];
41
+ fetchedAt: number;
42
+ }
43
+
44
+ const cache = new Map<string, CacheEntry>();
45
+ // Tracks whether the in-flight fetch was a force-refresh, so a fresh read never
46
+ // coalesces with an older non-force fetch that may predate a gateway-side write.
47
+ const inFlight = new Map<
48
+ string,
49
+ { promise: Promise<GuardianDelivery[] | null>; fresh: boolean }
50
+ >();
51
+
52
+ // Bumped on every invalidation. A fetch captures the generation when it starts
53
+ // and only writes its result to the cache if the generation is unchanged on
54
+ // resolve, so an invalidation mid-flight can't repopulate a stale pre-change
55
+ // result and mask a guardian-binding change.
56
+ let cacheGeneration = 0;
57
+
58
+ function cacheKey(channelTypes?: string[]): string {
59
+ if (!channelTypes || channelTypes.length === 0) return "ALL";
60
+ return [...channelTypes].sort().join(",");
61
+ }
62
+
63
+ async function fetchGuardianDelivery(
64
+ input: { channelTypes?: string[] },
65
+ ): Promise<GuardianDelivery[] | null> {
66
+ try {
67
+ const result = await ipcCall(
68
+ "resolve_guardian_delivery",
69
+ input,
70
+ GUARDIAN_DELIVERY_IPC_TIMEOUT_MS,
71
+ );
72
+ if (!result) return null;
73
+
74
+ const parsed = ResolveGuardianDeliveryResponseSchema.safeParse(result);
75
+ return parsed.success ? parsed.data.guardians : null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // Shared fetch path for both the cached and fresh public surfaces. When
82
+ // `forceRefresh` is set the cached entry is bypassed; the read is still
83
+ // single-flight and still populates the cache with the fresh result.
84
+ async function readGuardianDelivery(
85
+ input: { channelTypes?: string[]; forceRefresh?: boolean },
86
+ ): Promise<GuardianDelivery[] | null> {
87
+ const key = cacheKey(input.channelTypes);
88
+
89
+ if (!input.forceRefresh) {
90
+ const cached = cache.get(key);
91
+ if (
92
+ cached &&
93
+ Date.now() - cached.fetchedAt < GUARDIAN_DELIVERY_CACHE_TTL_MS
94
+ ) {
95
+ return cached.guardians;
96
+ }
97
+ }
98
+
99
+ // A non-force read may coalesce with any in-flight fetch. A force read may
100
+ // only coalesce with another force fetch — never with a non-force fetch that
101
+ // could have started before a gateway-side binding write and resolve stale.
102
+ const pending = inFlight.get(key);
103
+ if (pending && (!input.forceRefresh || pending.fresh)) return pending.promise;
104
+
105
+ const startGen = cacheGeneration;
106
+ const promise = fetchGuardianDelivery({ channelTypes: input.channelTypes })
107
+ .then((guardians) => {
108
+ // Skip the write if an invalidation fired during the fetch: the result
109
+ // may predate the change. Return it to this caller (freshest it has) but
110
+ // leave the cache empty so the next call re-fetches.
111
+ if (guardians && cacheGeneration === startGen) {
112
+ cache.set(key, { guardians, fetchedAt: Date.now() });
113
+ }
114
+ return guardians;
115
+ })
116
+ .finally(() => {
117
+ // Only clear the slot if it still holds this fetch — a concurrent force
118
+ // read may have replaced a non-force entry (or vice versa).
119
+ if (inFlight.get(key)?.promise === promise) inFlight.delete(key);
120
+ });
121
+
122
+ inFlight.set(key, { promise, fresh: !!input.forceRefresh });
123
+ return promise;
124
+ }
125
+
126
+ /**
127
+ * Resolve active guardian deliveries, optionally filtered by channel type.
128
+ * Returns the cached list when fresh, otherwise fetches (single-flight) and
129
+ * caches on success. Returns `null` on failure without caching.
130
+ *
131
+ * To force an uncached read, call {@link getGuardianDeliveryFresh} — the only
132
+ * public fresh-read entry point.
133
+ */
134
+ export async function getGuardianDelivery(
135
+ input?: { channelTypes?: string[] },
136
+ ): Promise<GuardianDelivery[] | null> {
137
+ return readGuardianDelivery({ channelTypes: input?.channelTypes });
138
+ }
139
+
140
+ /**
141
+ * Synchronous read of the already-cached guardian deliveries, without any IO.
142
+ *
143
+ * Returns the fresh cached list for the given channel filter, or `undefined`
144
+ * when the cache is cold or expired. Used by sync hot paths (SSE subscribe)
145
+ * that cannot await {@link getGuardianDelivery} but must resolve the SAME
146
+ * gateway-owned principal the async paths land on. A cold/expired return lets
147
+ * the caller fall back to the local store as before.
148
+ */
149
+ export function peekCachedGuardianDelivery(
150
+ input?: { channelTypes?: string[] },
151
+ ): GuardianDelivery[] | undefined {
152
+ const cached = cache.get(cacheKey(input?.channelTypes));
153
+ if (!cached) return undefined;
154
+ if (Date.now() - cached.fetchedAt >= GUARDIAN_DELIVERY_CACHE_TTL_MS) {
155
+ return undefined;
156
+ }
157
+ return cached.guardians;
158
+ }
159
+
160
+ /**
161
+ * Fresh (uncached) variant of {@link getGuardianDelivery}. Existence guards read
162
+ * fresh because gateway-side binding writes don't invalidate the daemon cache.
163
+ * Still single-flight, and still populates the cache with the fresh result.
164
+ */
165
+ export async function getGuardianDeliveryFresh(
166
+ input?: { channelTypes?: string[] },
167
+ ): Promise<GuardianDelivery[] | null> {
168
+ return readGuardianDelivery({
169
+ channelTypes: input?.channelTypes,
170
+ forceRefresh: true,
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Clear ALL cached guardian deliveries. Subscribed to `onContactChange` so
176
+ * contact mutations refetch on the next read; also exported for any caller that
177
+ * wants to invalidate explicitly.
178
+ */
179
+ export function invalidateGuardianDeliveryCache(): void {
180
+ cacheGeneration += 1;
181
+ cache.clear();
182
+ inFlight.clear();
183
+ }
184
+
185
+ onContactChange(invalidateGuardianDeliveryCache);
186
+
187
+ /** First active guardian delivery for the given channel type, if any. */
188
+ export function guardianForChannel(
189
+ list: GuardianDelivery[],
190
+ channelType: string,
191
+ ): GuardianDelivery | undefined {
192
+ return list.find(
193
+ (g) => g.channelType === channelType && g.status === "active",
194
+ );
195
+ }
196
+
197
+ /** First guardian delivery overall — the `listGuardianChannels` fallback. */
198
+ export function anyGuardian(
199
+ list: GuardianDelivery[],
200
+ ): GuardianDelivery | undefined {
201
+ return list[0];
202
+ }
203
+
204
+ /**
205
+ * Resolve a guardian displayName for voice surfaces: prefer the phone-channel
206
+ * guardian, falling back to any guardian. Returns `undefined` when the list is
207
+ * absent or no guardian carries a displayName.
208
+ */
209
+ export function voiceGuardianDisplayName(
210
+ list: GuardianDelivery[] | null,
211
+ ): string | undefined {
212
+ const guardian = list
213
+ ? (guardianForChannel(list, "phone") ?? anyGuardian(list))
214
+ : undefined;
215
+ return guardian?.displayName ?? undefined;
216
+ }
217
+
218
+ /** Test-only: reset cache + in-flight state for deterministic test runs. */
219
+ export function __resetGuardianDeliveryCacheForTest(): void {
220
+ cache.clear();
221
+ inFlight.clear();
222
+ cacheGeneration = 0;
223
+ }
@@ -480,6 +480,11 @@ export async function runAgentLoopImpl(
480
480
  // resolved once here and threaded into every re-injection — including the
481
481
  // post-compaction hook — rather than re-read per assembly call.
482
482
  const isNonInteractive = !isInteractiveResolved;
483
+ // Expose the resolved turn-level interactivity to tool execution so tools
484
+ // (e.g. ask_question) see whether a human is present to answer, rather than
485
+ // re-deriving it from live client state that misclassifies a scheduled turn
486
+ // running on a client-attached conversation.
487
+ ctx.currentTurnIsNonInteractive = isNonInteractive;
483
488
  const diskPressureDecision = classifyDiskPressureTurnPolicy(
484
489
  getDiskPressureStatus(),
485
490
  {
@@ -1515,6 +1520,10 @@ export async function runAgentLoopImpl(
1515
1520
  ctx.diskPressureCleanupModeActive = false;
1516
1521
  ctx.preactivatedSkillIds = undefined;
1517
1522
  ctx.currentTurnOverrideProfile = undefined;
1523
+ // Turn-scoped interactivity. Clear it so paths that bypass this loop (e.g.
1524
+ // opportunity wakes calling `agentLoop.run` directly) don't inherit a stale
1525
+ // value and instead fall back to live client state in the tool context.
1526
+ ctx.currentTurnIsNonInteractive = undefined;
1518
1527
  // Channel command intents (e.g. Telegram /start) are single-turn metadata.
1519
1528
  // Clear at turn end so they never leak into subsequent unrelated messages.
1520
1529
  ctx.commandIntent = undefined;
@@ -60,6 +60,21 @@ import { resolveVerificationSessionIntent } from "./verification-session-intent.
60
60
 
61
61
  const log = getLogger("conversation-process");
62
62
 
63
+ /**
64
+ * Daemon-injected subagent lifecycle notifications carry `subagentNotification`
65
+ * metadata. They are persisted into the parent conversation so the orchestrator
66
+ * wakes and reads the subagent's result, but they are internal scaffolding — the
67
+ * user sees subagent activity through the inline progress card, not a chat turn.
68
+ * Skip the `user_message_echo` broadcast for these so they never render as a live
69
+ * user bubble; the persisted row is filtered from the rendered transcript on the
70
+ * client.
71
+ */
72
+ function isSubagentNotificationMessage(
73
+ metadata: Record<string, unknown> | undefined,
74
+ ): boolean {
75
+ return metadata?.subagentNotification != null;
76
+ }
77
+
63
78
  /** Format the result of a forced compaction into a user-facing message. */
64
79
  export function formatCompactResult(result: ContextWindowResult): string {
65
80
  const fmt = (n: number | undefined) => (n ?? 0).toLocaleString("en-US");
@@ -853,14 +868,16 @@ async function drainSingleMessage(
853
868
 
854
869
  // Broadcast the user message to all hub subscribers so passive devices
855
870
  // see the user turn before the assistant reply starts streaming.
856
- next.onEvent({
857
- type: "user_message_echo",
858
- text: resolvedContent,
859
- conversationId: conversation.conversationId,
860
- messageId: userMessageId,
861
- requestId: next.requestId,
862
- clientMessageId: next.clientMessageId,
863
- });
871
+ if (!isSubagentNotificationMessage(next.metadata)) {
872
+ next.onEvent({
873
+ type: "user_message_echo",
874
+ text: resolvedContent,
875
+ conversationId: conversation.conversationId,
876
+ messageId: userMessageId,
877
+ requestId: next.requestId,
878
+ clientMessageId: next.clientMessageId,
879
+ });
880
+ }
864
881
  publishConversationMessagesChanged(conversation.conversationId);
865
882
 
866
883
  // Set the active surface for the dequeued message so runAgentLoop can inject context
@@ -1204,14 +1221,16 @@ async function drainBatch(
1204
1221
 
1205
1222
  // Broadcast the user message to all hub subscribers so passive devices
1206
1223
  // see each batched user turn before the assistant reply starts streaming.
1207
- qm.onEvent({
1208
- type: "user_message_echo",
1209
- text: qmContent,
1210
- conversationId: conversation.conversationId,
1211
- messageId: lastUserMessageId,
1212
- requestId: qm.requestId,
1213
- clientMessageId: qm.clientMessageId,
1214
- });
1224
+ if (!isSubagentNotificationMessage(qm.metadata)) {
1225
+ qm.onEvent({
1226
+ type: "user_message_echo",
1227
+ text: qmContent,
1228
+ conversationId: conversation.conversationId,
1229
+ messageId: lastUserMessageId,
1230
+ requestId: qm.requestId,
1231
+ clientMessageId: qm.clientMessageId,
1232
+ });
1233
+ }
1215
1234
  publishConversationMessagesChanged(conversation.conversationId);
1216
1235
 
1217
1236
  // Persist succeeded. Update last-successful markers so a later tail
@@ -1367,6 +1386,8 @@ export interface ProcessMessageOptions {
1367
1386
  */
1368
1387
  overrideProfile?: string;
1369
1388
  displayContent?: string;
1389
+ /** JWT-verified committer principal for turn-scoped host-proxy authorization. */
1390
+ sourceActorPrincipalId?: string;
1370
1391
  }
1371
1392
 
1372
1393
  // ── processMessage ───────────────────────────────────────────────────
@@ -1390,6 +1411,7 @@ export async function processMessage(
1390
1411
  callSite,
1391
1412
  overrideProfile,
1392
1413
  displayContent,
1414
+ sourceActorPrincipalId,
1393
1415
  } = options;
1394
1416
  await conversation.ensureActorScopedHistory();
1395
1417
  // Snapshot persona context at turn start so later tool turns can't pick up
@@ -1397,7 +1419,7 @@ export async function processMessage(
1397
1419
  conversation.currentTurnTrustContext = conversation.trustContext;
1398
1420
  conversation.currentTurnAuthContext = conversation.authContext;
1399
1421
  conversation.currentTurnSourceActorPrincipalId =
1400
- conversation.authContext?.actorPrincipalId;
1422
+ sourceActorPrincipalId ?? conversation.authContext?.actorPrincipalId;
1401
1423
  conversation.currentTurnChannelCapabilities =
1402
1424
  conversation.channelCapabilities;
1403
1425
  conversation.currentActiveSurfaceId = activeSurfaceId;
@@ -1695,6 +1695,10 @@ export async function handleSurfaceAction(
1695
1695
  surfaceId: string,
1696
1696
  actionId: string,
1697
1697
  data?: Record<string, unknown>,
1698
+ // JWT-verified committer principal; threaded so enqueued turns can
1699
+ // reconstruct the same-user binding for host proxies (CU / app-control),
1700
+ // mirroring the normal message path.
1701
+ sourceActorPrincipalId?: string,
1698
1702
  ): Promise<SurfaceActionResult> {
1699
1703
  // ── Standalone surface interception ──────────────────────────────
1700
1704
  // Daemon-driven surfaces (from `requestInteractiveUi`) register a
@@ -1987,6 +1991,7 @@ export async function handleSurfaceAction(
1987
1991
  requestId,
1988
1992
  activeSurfaceId: surfaceId,
1989
1993
  displayContent,
1994
+ sourceActorPrincipalId,
1990
1995
  });
1991
1996
 
1992
1997
  if (result.rejected) {
@@ -2043,6 +2048,7 @@ export async function handleSurfaceAction(
2043
2048
  requestId,
2044
2049
  activeSurfaceId: surfaceId,
2045
2050
  displayContent,
2051
+ sourceActorPrincipalId,
2046
2052
  })
2047
2053
  .catch((err) => {
2048
2054
  const message = err instanceof Error ? err.message : String(err);
@@ -2233,6 +2239,7 @@ export async function handleSurfaceAction(
2233
2239
  requestId,
2234
2240
  activeSurfaceId: surfaceId,
2235
2241
  displayContent,
2242
+ sourceActorPrincipalId,
2236
2243
  });
2237
2244
  if (result.rejected) {
2238
2245
  ctx.surfaceActionRequestIds.delete(requestId);
@@ -2336,6 +2343,7 @@ export async function handleSurfaceAction(
2336
2343
  requestId,
2337
2344
  activeSurfaceId: surfaceId,
2338
2345
  displayContent,
2346
+ sourceActorPrincipalId,
2339
2347
  })
2340
2348
  .catch((err) => {
2341
2349
  const message = err instanceof Error ? err.message : String(err);
@@ -22,7 +22,12 @@ import type { Message, ToolDefinition } from "../providers/types.js";
22
22
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
23
23
  import { registerConversationSender } from "../tools/browser/browser-screencast.js";
24
24
  import type { ToolExecutor } from "../tools/executor.js";
25
- import { getMcpToolDefinitions, getTool } from "../tools/registry.js";
25
+ import {
26
+ getMcpToolDefinitions,
27
+ getTool,
28
+ getWorkspaceToolDefinitions,
29
+ getWorkspaceToolNames,
30
+ } from "../tools/registry.js";
26
31
  import {
27
32
  ACTIVITY_SKIP_SET,
28
33
  injectActivityField,
@@ -41,6 +46,7 @@ import {
41
46
  type ToolExecutionResult,
42
47
  type ToolLifecycleEventHandler,
43
48
  } from "../tools/types.js";
49
+ import { loadWorkspaceTools } from "../tools/workspace-tools/loader.js";
44
50
  import {
45
51
  resolveUsageAttribution,
46
52
  type UsageAttributionSnapshot,
@@ -238,7 +244,10 @@ export function createToolExecutor(
238
244
  });
239
245
  }
240
246
  },
241
- isInteractive: !ctx.hasNoClient && !ctx.headlessLock,
247
+ isInteractive:
248
+ ctx.currentTurnIsNonInteractive !== undefined
249
+ ? !ctx.currentTurnIsNonInteractive
250
+ : !ctx.hasNoClient && !ctx.headlessLock,
242
251
  proxyToolResolver: (
243
252
  toolName: string,
244
253
  proxyInput: Record<string, unknown>,
@@ -650,11 +659,13 @@ export function isToolActiveForContext(
650
659
  * allowedToolNames so newly-activated skill tools aren't blocked by
651
660
  * the executor's stale gate.
652
661
  *
653
- * Core (non-MCP) tool definitions are captured at conversation creation and
654
- * reused on each turn. MCP tool definitions are re-read from the global
655
- * registry on each turn so that tools registered after conversation creation
656
- * (e.g. via `vellum mcp reload`) are automatically picked up without
657
- * requiring conversation disposal or app restart.
662
+ * Core (non-MCP, non-workspace) tool definitions are captured at conversation
663
+ * creation and reused on each turn. MCP and workspace tool definitions are
664
+ * re-read from the global registry on each turn so that tools registered or
665
+ * changed after conversation creation are automatically picked up without
666
+ * requiring conversation disposal or app restart — MCP via `vellum mcp
667
+ * reload`, workspace tools via edits under `<workspaceDir>/tools/` that the
668
+ * per-turn reconcile (kicked below) folds into the registry.
658
669
  */
659
670
  export function createResolveToolsCallback(
660
671
  toolDefs: ToolDefinition[],
@@ -662,16 +673,21 @@ export function createResolveToolsCallback(
662
673
  ): ((history: Message[]) => ToolDefinition[]) | undefined {
663
674
  if (toolDefs.length === 0) return undefined;
664
675
 
665
- // Separate the initial tool defs into core (stable) and MCP (dynamic).
666
- // We keep core tools from the snapshot and re-read MCP tools each turn.
676
+ // Separate the initial tool defs into core (stable) and the two dynamic
677
+ // categories (MCP, workspace). We keep core tools from the snapshot and
678
+ // re-read MCP + workspace tools from the registry each turn.
667
679
  const initialMcpDefs = getMcpToolDefinitions();
668
680
  const initialMcpNames = new Set(initialMcpDefs.map((d) => d.name));
669
- const coreToolDefs = toolDefs.filter((d) => !initialMcpNames.has(d.name));
681
+ const initialWorkspaceNames = new Set(getWorkspaceToolNames());
682
+ const coreToolDefs = toolDefs.filter(
683
+ (d) => !initialMcpNames.has(d.name) && !initialWorkspaceNames.has(d.name),
684
+ );
670
685
  log.debug(
671
686
  {
672
687
  coreCount: coreToolDefs.length,
673
688
  mcpCount: initialMcpDefs.length,
674
689
  mcpTools: initialMcpDefs.map((d) => d.name),
690
+ workspaceCount: initialWorkspaceNames.size,
675
691
  },
676
692
  "Conversation tool resolver initialized",
677
693
  );
@@ -685,6 +701,13 @@ export function createResolveToolsCallback(
685
701
  return [];
686
702
  }
687
703
 
704
+ // Reconcile workspace tool overrides under `<workspaceDir>/tools/` into
705
+ // the registry, then re-read them below — the on-read replacement for a
706
+ // filesystem watcher. Fire-and-forget: the reconcile is idempotent,
707
+ // mtime-cached (a no-op costs one readdir + a stat per file) and
708
+ // serialized, so the registry settles for a subsequent turn to read.
709
+ void loadWorkspaceTools();
710
+
688
711
  // Filter core tools based on current conversation context so that tools
689
712
  // irrelevant to this turn (e.g. UI tools when no client is connected)
690
713
  // are omitted from the definitions sent to the provider.
@@ -705,24 +728,34 @@ export function createResolveToolsCallback(
705
728
  ? filteredCoreDefs.filter((d) => wireAllowlist.has(d.name))
706
729
  : filteredCoreDefs;
707
730
 
708
- // Re-read MCP tool definitions from the registry each turn so conversations
709
- // automatically pick up tools added/removed by `vellum mcp reload`.
731
+ // Re-read MCP and workspace tool definitions from the registry each turn
732
+ // so conversations automatically pick up tools added/removed by `vellum
733
+ // mcp reload` and workspace-tool edits reconciled from disk, without
734
+ // recreating the conversation.
710
735
  const currentMcpDefs = getMcpToolDefinitions();
736
+ const currentWorkspaceDefs = getWorkspaceToolDefinitions();
711
737
  log.debug(
712
738
  {
713
739
  coreCount: scopedCoreDefs.length,
714
740
  mcpCount: currentMcpDefs.length,
715
741
  mcpTools: currentMcpDefs.map((d) => d.name),
742
+ workspaceCount: currentWorkspaceDefs.length,
743
+ workspaceTools: currentWorkspaceDefs.map((d) => d.name),
716
744
  },
717
- "MCP tools resolved for turn",
745
+ "MCP and workspace tools resolved for turn",
718
746
  );
719
747
  const scopedMcpDefs = wireAllowlist
720
748
  ? currentMcpDefs.filter((d) => wireAllowlist.has(d.name))
721
749
  : currentMcpDefs;
750
+ const scopedWorkspaceDefs = wireAllowlist
751
+ ? currentWorkspaceDefs.filter((d) => wireAllowlist.has(d.name))
752
+ : currentWorkspaceDefs;
722
753
  const excluded = new Set(getConfig().tools.exclude);
723
- const allBaseDefs = [...scopedCoreDefs, ...scopedMcpDefs].filter(
724
- (d) => !excluded.has(d.name),
725
- );
754
+ const allBaseDefs = [
755
+ ...scopedCoreDefs,
756
+ ...scopedWorkspaceDefs,
757
+ ...scopedMcpDefs,
758
+ ].filter((d) => !excluded.has(d.name));
726
759
 
727
760
  const effectivePreactivated = [
728
761
  ...DEFAULT_PREACTIVATED_SKILL_IDS,
@@ -50,6 +50,7 @@ import {
50
50
  getMessages,
51
51
  resolveOverrideProfile,
52
52
  setConversationHistoryStrippedAt,
53
+ setConversationProcessingStartedAt,
53
54
  } from "../memory/conversation-crud.js";
54
55
  import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
55
56
  import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
@@ -91,7 +92,7 @@ import {
91
92
  isActivationMomentParam,
92
93
  } from "../telemetry/activation-funnel.js";
93
94
  import { ToolExecutor } from "../tools/executor.js";
94
- import { getAllToolDefinitions } from "../tools/registry.js";
95
+ import { getAllToolDefinitions, getTool } from "../tools/registry.js";
95
96
  import type { ToolLifecycleEvent } from "../tools/types.js";
96
97
  import type { OnboardingContext } from "../types/onboarding-context.js";
97
98
  import type { AbortReason } from "../util/abort-reasons.js";
@@ -376,6 +377,7 @@ export class Conversation {
376
377
  */
377
378
  wakePersonaOverride?: SystemPromptPersonaOverride;
378
379
  /** @internal */ currentTurnOverrideProfile?: string;
380
+ /** @internal */ currentTurnIsNonInteractive?: boolean;
379
381
  /** @internal */ authContext?: AuthContext;
380
382
  /** @internal */ currentTurnAuthContext?: AuthContext;
381
383
  /** @internal */ currentTurnSourceActorPrincipalId?: string;
@@ -701,6 +703,9 @@ export class Conversation {
701
703
  tools: toolDefs.length > 0 ? toolDefs : undefined,
702
704
  toolExecutor: toolDefs.length > 0 ? toolExecutor : undefined,
703
705
  resolveTools,
706
+ // A tool the registry marks exclusive (e.g. `advisor`) runs alone in its
707
+ // turn; the loop defers any sibling calls until the next turn.
708
+ isExclusiveTool: (name) => getTool(name)?.exclusive === true,
704
709
  resolveConversationDir: () => {
705
710
  const conv = getConversation(this.conversationId);
706
711
  if (!conv) return null;
@@ -1335,6 +1340,13 @@ export class Conversation {
1335
1340
  setProcessing(value: boolean): void {
1336
1341
  const wasProcessing = this._processing;
1337
1342
  this._processing = value;
1343
+ // Persist the cross-process source of truth so out-of-process callers
1344
+ // (retrospective CLI, future detached workers) can detect mid-turn state
1345
+ // by reading the conversations row directly.
1346
+ setConversationProcessingStartedAt(
1347
+ this.conversationId,
1348
+ value ? Date.now() : null,
1349
+ );
1338
1350
  if (wasProcessing && !value) {
1339
1351
  void publishSyncInvalidation([
1340
1352
  conversationMetadataSyncTag(this.conversationId),
@@ -2048,8 +2060,15 @@ export class Conversation {
2048
2060
  surfaceId: string,
2049
2061
  actionId: string,
2050
2062
  data?: Record<string, unknown>,
2063
+ sourceActorPrincipalId?: string,
2051
2064
  ): Promise<SurfaceActionResult> {
2052
- return handleSurfaceActionImpl(this, surfaceId, actionId, data);
2065
+ return handleSurfaceActionImpl(
2066
+ this,
2067
+ surfaceId,
2068
+ actionId,
2069
+ data,
2070
+ sourceActorPrincipalId,
2071
+ );
2053
2072
  }
2054
2073
 
2055
2074
  handleSurfaceUndo(surfaceId: string): void {
@@ -24,6 +24,12 @@ export const DISK_PRESSURE_CLEAR_THRESHOLD_PERCENT = 90;
24
24
  // clears the warning state, which discards the banner's (state-scoped) dismissal
25
25
  // so it re-appears the moment usage ticks back up.
26
26
  export const DISK_PRESSURE_WARNING_CLEAR_THRESHOLD_PERCENT = 77;
27
+ // Absolute free-space floor (MiB). Regardless of usage percentage, never enter
28
+ // the warning or critical state while at least this much space remains free. A
29
+ // high usage percentage on a large disk can still leave many gigabytes
30
+ // available, where locking is pointless. Small volumes (where a high percentage
31
+ // genuinely means near-full) drop below the floor and remain protected.
32
+ export const DISK_PRESSURE_MIN_FREE_FLOOR_MB = 2048;
27
33
  export const DISK_PRESSURE_CHECK_INTERVAL_MS = 60_000;
28
34
  export const DISK_PRESSURE_OVERRIDE_CONFIRMATION = "I understand the risks";
29
35
  export const DISK_PRESSURE_BLOCKED_CAPABILITIES = [
@@ -219,7 +225,10 @@ export function evaluateDiskPressureNow(): DiskPressureStatus {
219
225
  const criticalThreshold = state.status.locked
220
226
  ? DISK_PRESSURE_CLEAR_THRESHOLD_PERCENT
221
227
  : DISK_PRESSURE_THRESHOLD_PERCENT;
222
- const isCritical = usagePercent >= criticalThreshold;
228
+ // Absolute free-space floor overrides the percentage thresholds: while ample
229
+ // space remains free, report "ok" no matter how full the volume is by percent.
230
+ const hasAmpleFreeSpace = usageInfo.freeMb >= DISK_PRESSURE_MIN_FREE_FLOOR_MB;
231
+ const isCritical = !hasAmpleFreeSpace && usagePercent >= criticalThreshold;
223
232
  // Mirror the critical deadband for the warning band: once in an active
224
233
  // pressure state (warning or critical), hold warning until usage clears the
225
234
  // lower warning-clear threshold. Treating "critical" as active here matters
@@ -235,7 +244,8 @@ export function evaluateDiskPressureNow(): DiskPressureStatus {
235
244
  const warningThreshold = inActivePressureState
236
245
  ? DISK_PRESSURE_WARNING_CLEAR_THRESHOLD_PERCENT
237
246
  : DISK_PRESSURE_WARNING_THRESHOLD_PERCENT;
238
- const isWarning = !isCritical && usagePercent >= warningThreshold;
247
+ const isWarning =
248
+ !hasAmpleFreeSpace && !isCritical && usagePercent >= warningThreshold;
239
249
  const lastCheckedAt = new Date().toISOString();
240
250
 
241
251
  if (!isCritical && !isWarning) {