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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -26,7 +26,7 @@
26
26
  * and skipped (no silent provider-safe rewrite — operator must rename).
27
27
  * - Multiple workspace tools register in a single batch.
28
28
  */
29
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
29
+ import { mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
30
30
  import { tmpdir } from "node:os";
31
31
  import { join } from "node:path";
32
32
  import { afterAll, beforeEach, describe, expect, test } from "bun:test";
@@ -42,7 +42,10 @@ import {
42
42
  registerTool,
43
43
  } from "../tools/registry.js";
44
44
  import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
45
- import { loadWorkspaceTools } from "../tools/workspace-tools/loader.js";
45
+ import {
46
+ __resetWorkspaceToolCacheForTesting,
47
+ loadWorkspaceTools,
48
+ } from "../tools/workspace-tools/loader.js";
46
49
 
47
50
  // Per-test counter so each writeTool() call lands in a unique tempdir,
48
51
  // defeating bun's per-URL ESM cache between tests. Without this, a
@@ -87,6 +90,23 @@ function writeRemovedSentinel(name: string): void {
87
90
  writeFileSync(join(toolsDir, `${name}.removed`), "");
88
91
  }
89
92
 
93
+ /** Delete `<workspaceDir>/tools/<name><ext>` (defaults to `.ts`). */
94
+ function removeToolFile(name: string, ext = ".ts"): void {
95
+ rmSync(join(currentWorkspaceDir, "tools", `${name}${ext}`), { force: true });
96
+ }
97
+
98
+ /**
99
+ * Overwrite an existing tool file and bump its mtime into the future so the
100
+ * reconcile's mtime gate re-imports it even when the rewrite lands within
101
+ * the same millisecond as the original write.
102
+ */
103
+ function rewriteTool(name: string, body: string, ext = ".ts"): void {
104
+ const path = join(currentWorkspaceDir, "tools", `${name}${ext}`);
105
+ writeFileSync(path, body);
106
+ const future = new Date(Date.now() + 5000);
107
+ utimesSync(path, future, future);
108
+ }
109
+
90
110
  function makeFakeCoreTool(name: string): Tool {
91
111
  return {
92
112
  name,
@@ -94,6 +114,9 @@ function makeFakeCoreTool(name: string): Tool {
94
114
  category: "test",
95
115
  defaultRiskLevel: RiskLevel.Low,
96
116
  executionTarget: "sandbox",
117
+ // Match the finalized shape the registry stores (defaults filled), so
118
+ // `getCoreToolOverride(name)` toEqual comparisons hold after registration.
119
+ exclusive: false,
97
120
  input_schema: { type: "object", properties: {}, required: [] },
98
121
  async execute(
99
122
  _input: Record<string, unknown>,
@@ -133,6 +156,7 @@ export default {
133
156
  describe("workspace tool loader", () => {
134
157
  beforeEach(() => {
135
158
  __clearRegistryForTesting();
159
+ __resetWorkspaceToolCacheForTesting();
136
160
  freshWorkspace();
137
161
  });
138
162
 
@@ -316,4 +340,173 @@ export default 42;
316
340
  const names = getWorkspaceToolNames().sort();
317
341
  expect(names).toEqual(["alpha", "beta", "gamma"]);
318
342
  });
343
+
344
+ // ── Reconcile-on-read behavior ─────────────────────────────────────────
345
+ //
346
+ // loadWorkspaceTools() is idempotent and re-derives registry state from
347
+ // disk on every call. These cases cover the deltas a repeat call applies,
348
+ // which is what replaces the old filesystem watcher.
349
+
350
+ test("repeat call with no disk changes is a no-op (does not throw or duplicate)", async () => {
351
+ writeTool("stable_tool", WELL_FORMED_BODY);
352
+
353
+ await loadWorkspaceTools();
354
+ // A second reconcile must not throw on the already-registered name —
355
+ // the mtime cache recognizes the unchanged file and skips re-import.
356
+ await loadWorkspaceTools();
357
+
358
+ expect(getTool("stable_tool")).toBeDefined();
359
+ expect(getWorkspaceToolNames()).toEqual(["stable_tool"]);
360
+ });
361
+
362
+ test("a file added after the first reconcile registers on the next", async () => {
363
+ writeTool("first", WELL_FORMED_BODY);
364
+ await loadWorkspaceTools();
365
+ expect(getWorkspaceToolNames()).toEqual(["first"]);
366
+
367
+ writeTool("second", WELL_FORMED_BODY);
368
+ await loadWorkspaceTools();
369
+
370
+ expect(getWorkspaceToolNames().sort()).toEqual(["first", "second"]);
371
+ });
372
+
373
+ test("a changed file is re-imported on the next reconcile", async () => {
374
+ writeTool("mutable", WELL_FORMED_BODY);
375
+ await loadWorkspaceTools();
376
+ expect(getTool("mutable")?.description).toBe("from workspace");
377
+
378
+ rewriteTool(
379
+ "mutable",
380
+ `
381
+ export default {
382
+ description: "edited in place",
383
+ defaultRiskLevel: "low",
384
+ input_schema: { type: "object", properties: {}, required: [] },
385
+ async execute() {
386
+ return { content: "edited", isError: false };
387
+ },
388
+ };
389
+ `,
390
+ );
391
+ await loadWorkspaceTools();
392
+
393
+ expect(getTool("mutable")?.description).toBe("edited in place");
394
+ });
395
+
396
+ test("a deleted net-new tool file is unregistered on the next reconcile", async () => {
397
+ writeTool("ephemeral", WELL_FORMED_BODY);
398
+ await loadWorkspaceTools();
399
+ expect(getTool("ephemeral")).toBeDefined();
400
+
401
+ removeToolFile("ephemeral");
402
+ await loadWorkspaceTools();
403
+
404
+ expect(getTool("ephemeral")).toBeUndefined();
405
+ expect(getWorkspaceToolNames()).toEqual([]);
406
+ });
407
+
408
+ test("deleting an override file restores the stashed core tool", async () => {
409
+ const core = makeFakeCoreTool("restore_me");
410
+ registerTool(core);
411
+ writeTool("restore_me", WELL_FORMED_BODY);
412
+
413
+ await loadWorkspaceTools();
414
+ expect(getToolOwner("restore_me")?.kind).toBe("workspace");
415
+
416
+ removeToolFile("restore_me");
417
+ await loadWorkspaceTools();
418
+
419
+ expect(getToolOwner("restore_me")).toBeUndefined();
420
+ expect(getTool("restore_me")).toEqual(core);
421
+ expect(getCoreToolOverride("restore_me")).toBeUndefined();
422
+ });
423
+
424
+ test("deleting a .removed sentinel restores the stripped core tool", async () => {
425
+ const core = makeFakeCoreTool("strip_then_restore");
426
+ registerTool(core);
427
+ writeRemovedSentinel("strip_then_restore");
428
+
429
+ await loadWorkspaceTools();
430
+ expect(getTool("strip_then_restore")).toBeUndefined();
431
+ expect(getStrippedCoreToolNames()).toContain("strip_then_restore");
432
+
433
+ removeToolFile("strip_then_restore", ".removed");
434
+ await loadWorkspaceTools();
435
+
436
+ expect(getTool("strip_then_restore")).toEqual(core);
437
+ expect(getStrippedCoreToolNames()).not.toContain("strip_then_restore");
438
+ });
439
+
440
+ test("the registered name is the filename stem, ignoring the file's own name field", async () => {
441
+ // The default export sets a different `name` — the loader must pin the
442
+ // registered name to the stem ("stem_wins") so the mtime cache and the
443
+ // unregister-on-delete path stay keyed by the same name.
444
+ writeTool(
445
+ "stem_wins",
446
+ `
447
+ export default {
448
+ name: "different_name",
449
+ description: "name field should be ignored",
450
+ defaultRiskLevel: "low",
451
+ input_schema: { type: "object", properties: {}, required: [] },
452
+ async execute() {
453
+ return { content: "ok", isError: false };
454
+ },
455
+ };
456
+ `,
457
+ );
458
+
459
+ await loadWorkspaceTools();
460
+
461
+ expect(getTool("stem_wins")).toBeDefined();
462
+ expect(getTool("different_name")).toBeUndefined();
463
+ expect(getWorkspaceToolNames()).toEqual(["stem_wins"]);
464
+
465
+ // Deleting the file unregisters by stem — no leaked "different_name".
466
+ removeToolFile("stem_wins");
467
+ await loadWorkspaceTools();
468
+ expect(getTool("stem_wins")).toBeUndefined();
469
+ expect(getTool("different_name")).toBeUndefined();
470
+ });
471
+
472
+ test("per-tool isolation on reconcile: a bad file does not drop a valid edited tool", async () => {
473
+ writeTool("good_edit", WELL_FORMED_BODY);
474
+ await loadWorkspaceTools();
475
+ expect(getTool("good_edit")?.description).toBe("from workspace");
476
+
477
+ // Add a file that throws at import, and edit the good tool, in the same
478
+ // reconcile. The broken file must not prevent the edited tool from
479
+ // re-registering.
480
+ writeTool("broken_now", `throw new Error("boom at import");`);
481
+ rewriteTool(
482
+ "good_edit",
483
+ `
484
+ export default {
485
+ description: "edited and still here",
486
+ defaultRiskLevel: "low",
487
+ input_schema: { type: "object", properties: {}, required: [] },
488
+ async execute() {
489
+ return { content: "ok", isError: false };
490
+ },
491
+ };
492
+ `,
493
+ );
494
+ await loadWorkspaceTools();
495
+
496
+ expect(getTool("broken_now")).toBeUndefined();
497
+ expect(getTool("good_edit")?.description).toBe("edited and still here");
498
+ });
499
+
500
+ test("an edit that breaks an existing tool keeps the prior registration", async () => {
501
+ writeTool("was_good", WELL_FORMED_BODY);
502
+ await loadWorkspaceTools();
503
+ expect(getTool("was_good")?.description).toBe("from workspace");
504
+
505
+ // Rewrite the file into something that throws at import. The prior,
506
+ // working registration must stay in place rather than being torn down.
507
+ rewriteTool("was_good", `throw new Error("now broken");`);
508
+ await loadWorkspaceTools();
509
+
510
+ expect(getTool("was_good")?.description).toBe("from workspace");
511
+ });
319
512
  });
@@ -42,6 +42,7 @@ mock.module("../../util/logger.js", () => ({
42
42
  // the a2a.enabled flag. We use the real config system backed by initializeDb's
43
43
  // workspace directory.
44
44
 
45
+ import { seedContactChannel } from "../../__tests__/helpers/seed-contact-channel.js";
45
46
  import {
46
47
  invalidateConfigCache,
47
48
  loadRawConfig,
@@ -80,6 +81,15 @@ const originalFetch = globalThis.fetch;
80
81
  // Helpers
81
82
  // ---------------------------------------------------------------------------
82
83
 
84
+ /** Read a channel's local ACL columns directly to assert the gateway dual-write. */
85
+ function aclColumns(
86
+ channelId: string,
87
+ ): { status: string; policy: string } | null {
88
+ return getSqlite()
89
+ .query("SELECT status, policy FROM contact_channels WHERE id = ?")
90
+ .get(channelId) as { status: string; policy: string } | null;
91
+ }
92
+
83
93
  function resetTables(): void {
84
94
  const sqlite = getSqlite();
85
95
  sqlite.run("DELETE FROM a2a_tasks");
@@ -164,17 +174,16 @@ describe("e2e: trusted contact setup", () => {
164
174
  {
165
175
  type: "a2a",
166
176
  address: "assistant-b",
167
- status: "active",
168
- policy: "allow",
169
177
  },
170
178
  ],
171
179
  });
172
180
 
181
+ // upsertContact persists the a2a channel identity; the gateway owns the ACL
182
+ // status verdict.
173
183
  const contact = findContactByAddress("a2a", "assistant-b");
174
184
  expect(contact).not.toBeNull();
175
185
  expect(contact!.channels.some((ch) => ch.type === "a2a")).toBe(true);
176
186
  const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
177
- expect(a2aChannel!.status).toBe("active");
178
187
  expect(a2aChannel!.address).toBe("assistant-b");
179
188
  });
180
189
  });
@@ -355,21 +364,13 @@ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
355
364
  });
356
365
 
357
366
  test("trusted contact exists with active a2a channel — ACL passes", async () => {
358
- const { upsertContact } = await import("../../contacts/contact-store.js");
359
-
360
367
  // Pre-create a trusted contact for the sender
361
- upsertContact({
368
+ seedContactChannel({
369
+ sourceChannel: "a2a",
370
+ externalUserId: "trusted-assistant",
362
371
  displayName: "Trusted Bot",
363
- contactType: "assistant",
364
- role: "contact",
365
- channels: [
366
- {
367
- type: "a2a",
368
- address: "trusted-assistant",
369
- status: "active",
370
- policy: "allow",
371
- },
372
- ],
372
+ status: "active",
373
+ policy: "allow",
373
374
  });
374
375
 
375
376
  // Verify the contact exists (the ACL check the runtime performs)
@@ -378,8 +379,9 @@ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
378
379
 
379
380
  const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
380
381
  expect(a2aChannel).toBeTruthy();
381
- expect(a2aChannel!.status).toBe("active");
382
- expect(a2aChannel!.policy).toBe("allow");
382
+ const acl = aclColumns(a2aChannel!.id);
383
+ expect(acl!.status).toBe("active");
384
+ expect(acl!.policy).toBe("allow");
383
385
 
384
386
  // A task from this sender would pass the ACL check
385
387
  const msg = makeRequestMessage();
@@ -391,28 +393,21 @@ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
391
393
  });
392
394
 
393
395
  test("contact exists but channel is blocked — ACL would reject", async () => {
394
- const { upsertContact } = await import("../../contacts/contact-store.js");
395
-
396
- upsertContact({
396
+ seedContactChannel({
397
+ sourceChannel: "a2a",
398
+ externalUserId: "blocked-assistant",
397
399
  displayName: "Blocked Bot",
398
- contactType: "assistant",
399
- role: "contact",
400
- channels: [
401
- {
402
- type: "a2a",
403
- address: "blocked-assistant",
404
- status: "blocked",
405
- policy: "deny",
406
- },
407
- ],
400
+ status: "blocked",
401
+ policy: "deny",
408
402
  });
409
403
 
410
404
  const contact = findContactByAddress("a2a", "blocked-assistant");
411
405
  expect(contact).not.toBeNull();
412
406
 
413
407
  const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
414
- expect(a2aChannel!.status).toBe("blocked");
415
- expect(a2aChannel!.policy).toBe("deny");
408
+ const acl = aclColumns(a2aChannel!.id);
409
+ expect(acl!.status).toBe("blocked");
410
+ expect(acl!.policy).toBe("deny");
416
411
  });
417
412
  });
418
413
 
@@ -507,26 +502,19 @@ describe("e2e: full A2A round-trip", () => {
507
502
  setConfigEnabled(true);
508
503
 
509
504
  // Step 1: Create trusted contact for Assistant B (platform-mediated)
510
- upsertContact({
505
+ seedContactChannel({
506
+ sourceChannel: "a2a",
507
+ externalUserId: "assistant-b",
511
508
  displayName: "Assistant B",
512
- contactType: "assistant",
513
- role: "contact",
514
- channels: [
515
- {
516
- type: "a2a",
517
- address: "assistant-b",
518
- status: "active",
519
- policy: "allow",
520
- },
521
- ],
509
+ status: "active",
510
+ policy: "allow",
522
511
  });
523
512
 
524
513
  // Step 2: Verify trusted contact was created
525
514
  const contact = findContactByAddress("a2a", "assistant-b");
526
515
  expect(contact).not.toBeNull();
527
- expect(contact!.channels.find((ch) => ch.type === "a2a")!.status).toBe(
528
- "active",
529
- );
516
+ const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a")!;
517
+ expect(aclColumns(a2aChannel.id)!.status).toBe("active");
530
518
 
531
519
  // Step 3: Simulate inbound A2A message from B (as if B sent us a request)
532
520
  const inboundMsg = makeRequestMessage({
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Verifies the agent loop's exclusive-tool dispatch: when a tool the loop is
3
+ * told is exclusive appears in a multi-call turn, only that tool runs and the
4
+ * siblings are deferred un-run with a benign result — so the model incorporates
5
+ * the exclusive tool's output before acting on anything else. Drives the REAL
6
+ * loop, mocking only the provider boundary.
7
+ */
8
+ import { describe, expect, test } from "bun:test";
9
+
10
+ import { createMockProvider } from "../__tests__/helpers/mock-provider.js";
11
+ import type { ContentBlock, ProviderResponse } from "../providers/types.js";
12
+ import { AgentLoop } from "./loop.js";
13
+
14
+ const endTurn = (text: string): ProviderResponse => ({
15
+ content: [{ type: "text", text }],
16
+ model: "mock-model",
17
+ usage: { inputTokens: 1, outputTokens: 1 },
18
+ stopReason: "end_turn",
19
+ });
20
+
21
+ const toolUseTurn = (
22
+ blocks: Array<{ id: string; name: string }>,
23
+ ): ProviderResponse => ({
24
+ content: [
25
+ { type: "text", text: "working" },
26
+ ...blocks.map((b) => ({
27
+ type: "tool_use" as const,
28
+ id: b.id,
29
+ name: b.name,
30
+ input: {},
31
+ })),
32
+ ],
33
+ model: "mock-model",
34
+ usage: { inputTokens: 1, outputTokens: 1 },
35
+ stopReason: "tool_use",
36
+ });
37
+
38
+ function toolResults(history: { content: ContentBlock[] }[]) {
39
+ return history
40
+ .flatMap((m) => m.content)
41
+ .filter(
42
+ (b): b is Extract<ContentBlock, { type: "tool_result" }> =>
43
+ b.type === "tool_result",
44
+ );
45
+ }
46
+
47
+ const baseRun = {
48
+ requestId: "req-excl",
49
+ onEvent: () => {},
50
+ callSite: "mainAgent" as const,
51
+ trust: { sourceChannel: "vellum" as const, trustClass: "unknown" as const },
52
+ };
53
+
54
+ describe("AgentLoop — exclusive tool deferral", () => {
55
+ test("runs the exclusive tool alone and defers sibling calls un-run", async () => {
56
+ const { provider } = createMockProvider([
57
+ toolUseTurn([
58
+ { id: "call-exclusive", name: "exclusive_tool" },
59
+ { id: "call-edit", name: "write_file" },
60
+ ]),
61
+ endTurn("done"),
62
+ ]);
63
+
64
+ const executed: string[] = [];
65
+ const loop = new AgentLoop({
66
+ provider,
67
+ systemPrompt: "sys",
68
+ conversationId: "excl-1",
69
+ tools: [
70
+ {
71
+ name: "exclusive_tool",
72
+ description: "",
73
+ input_schema: { type: "object" },
74
+ },
75
+ {
76
+ name: "write_file",
77
+ description: "",
78
+ input_schema: { type: "object" },
79
+ },
80
+ ],
81
+ toolExecutor: async (name) => {
82
+ executed.push(name);
83
+ return { content: `ran ${name}`, isError: false };
84
+ },
85
+ isExclusiveTool: (name) => name === "exclusive_tool",
86
+ });
87
+
88
+ const { history } = await loop.run({
89
+ ...baseRun,
90
+ messages: [{ role: "user", content: [{ type: "text", text: "do it" }] }],
91
+ });
92
+
93
+ // Only the exclusive tool actually executed.
94
+ expect(executed).toEqual(["exclusive_tool"]);
95
+
96
+ const results = toolResults(history);
97
+ const exclusiveResult = results.find(
98
+ (b) => b.tool_use_id === "call-exclusive",
99
+ )!;
100
+ const editResult = results.find((b) => b.tool_use_id === "call-edit")!;
101
+
102
+ // The exclusive tool ran; the sibling came back un-run (not an error) so the
103
+ // model can re-issue it after reading the guidance.
104
+ expect(exclusiveResult.content).toBe("ran exclusive_tool");
105
+ expect(editResult.content).toContain("not run");
106
+ expect(editResult.content).toContain("exclusive_tool");
107
+ expect(editResult.is_error).toBe(false);
108
+ });
109
+
110
+ test("runs sibling tools normally when no exclusive tool is present", async () => {
111
+ const { provider } = createMockProvider([
112
+ toolUseTurn([
113
+ { id: "call-read", name: "read_file" },
114
+ { id: "call-write", name: "write_file" },
115
+ ]),
116
+ endTurn("done"),
117
+ ]);
118
+
119
+ const executed: string[] = [];
120
+ const loop = new AgentLoop({
121
+ provider,
122
+ systemPrompt: "sys",
123
+ conversationId: "excl-2",
124
+ tools: [
125
+ {
126
+ name: "read_file",
127
+ description: "",
128
+ input_schema: { type: "object" },
129
+ },
130
+ {
131
+ name: "write_file",
132
+ description: "",
133
+ input_schema: { type: "object" },
134
+ },
135
+ ],
136
+ toolExecutor: async (name) => {
137
+ executed.push(name);
138
+ return { content: `ran ${name}`, isError: false };
139
+ },
140
+ isExclusiveTool: (name) => name === "exclusive_tool",
141
+ });
142
+
143
+ const { history } = await loop.run({
144
+ ...baseRun,
145
+ messages: [{ role: "user", content: [{ type: "text", text: "do it" }] }],
146
+ });
147
+
148
+ // Both non-exclusive tools ran; nothing was deferred.
149
+ expect(executed.sort()).toEqual(["read_file", "write_file"]);
150
+ for (const result of toolResults(history)) {
151
+ expect(result.content).not.toContain("not run");
152
+ }
153
+ });
154
+ });