@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
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Verifies the agent loop's exclusive-tool dispatch: when a tool the loop is
3
+ * told is exclusive (e.g. the advisor) appears in a multi-call turn, only that
4
+ * tool runs and the siblings are deferred un-run with a benign result — so the
5
+ * model incorporates the exclusive tool's output before acting on anything
6
+ * else. Drives the REAL 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-advisor", name: "advisor" },
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
+ { name: "advisor", description: "", input_schema: { type: "object" } },
71
+ {
72
+ name: "write_file",
73
+ description: "",
74
+ input_schema: { type: "object" },
75
+ },
76
+ ],
77
+ toolExecutor: async (name) => {
78
+ executed.push(name);
79
+ return { content: `ran ${name}`, isError: false };
80
+ },
81
+ isExclusiveTool: (name) => name === "advisor",
82
+ });
83
+
84
+ const { history } = await loop.run({
85
+ ...baseRun,
86
+ messages: [{ role: "user", content: [{ type: "text", text: "do it" }] }],
87
+ });
88
+
89
+ // Only the exclusive tool actually executed.
90
+ expect(executed).toEqual(["advisor"]);
91
+
92
+ const results = toolResults(history);
93
+ const advisorResult = results.find(
94
+ (b) => b.tool_use_id === "call-advisor",
95
+ )!;
96
+ const editResult = results.find((b) => b.tool_use_id === "call-edit")!;
97
+
98
+ // The advisor ran; the sibling came back un-run (not an error) so the model
99
+ // can re-issue it after reading the guidance.
100
+ expect(advisorResult.content).toBe("ran advisor");
101
+ expect(editResult.content).toContain("not run");
102
+ expect(editResult.content).toContain("advisor");
103
+ expect(editResult.is_error).toBe(false);
104
+ });
105
+
106
+ test("runs sibling tools normally when no exclusive tool is present", async () => {
107
+ const { provider } = createMockProvider([
108
+ toolUseTurn([
109
+ { id: "call-read", name: "read_file" },
110
+ { id: "call-write", name: "write_file" },
111
+ ]),
112
+ endTurn("done"),
113
+ ]);
114
+
115
+ const executed: string[] = [];
116
+ const loop = new AgentLoop({
117
+ provider,
118
+ systemPrompt: "sys",
119
+ conversationId: "excl-2",
120
+ tools: [
121
+ {
122
+ name: "read_file",
123
+ description: "",
124
+ input_schema: { type: "object" },
125
+ },
126
+ {
127
+ name: "write_file",
128
+ description: "",
129
+ input_schema: { type: "object" },
130
+ },
131
+ ],
132
+ toolExecutor: async (name) => {
133
+ executed.push(name);
134
+ return { content: `ran ${name}`, isError: false };
135
+ },
136
+ isExclusiveTool: (name) => name === "advisor",
137
+ });
138
+
139
+ const { history } = await loop.run({
140
+ ...baseRun,
141
+ messages: [{ role: "user", content: [{ type: "text", text: "do it" }] }],
142
+ });
143
+
144
+ // Both non-exclusive tools ran; nothing was deferred.
145
+ expect(executed.sort()).toEqual(["read_file", "write_file"]);
146
+ for (const result of toolResults(history)) {
147
+ expect(result.content).not.toContain("not run");
148
+ }
149
+ });
150
+ });
package/src/agent/loop.ts CHANGED
@@ -625,6 +625,20 @@ export type LoopToolExecutor = (
625
625
  activityMetadata?: ToolActivityMetadata;
626
626
  }>;
627
627
 
628
+ /**
629
+ * The benign result returned for a sibling tool call that was deferred because
630
+ * an exclusive tool ran in the same turn. Phrased so the model treats it as a
631
+ * "not run yet" signal — read the exclusive tool's output, then re-issue this
632
+ * call if it is still the right next step.
633
+ */
634
+ function deferredForExclusiveMessage(exclusiveToolName: string): string {
635
+ return (
636
+ `(not run: \`${exclusiveToolName}\` was called this turn and runs first, on its own, ` +
637
+ `so the rest of your tool calls were held back. Read its output, then call this tool ` +
638
+ `again if it is still the right next step.)`
639
+ );
640
+ }
641
+
628
642
  export interface AgentLoopConstructorOptions {
629
643
  /** LLM provider the loop issues every call through. */
630
644
  provider: Provider;
@@ -634,6 +648,14 @@ export interface AgentLoopConstructorOptions {
634
648
  tools?: ToolDefinition[];
635
649
  toolExecutor?: LoopToolExecutor;
636
650
  resolveTools?: (history: Message[]) => ToolDefinition[];
651
+ /**
652
+ * Decide whether a tool runs exclusively in its turn (see
653
+ * {@link ToolDefinition.exclusive}). When it returns true for a tool present
654
+ * in a multi-call turn, the loop runs only that tool and defers the siblings
655
+ * un-run. Injected by the conversation wiring, which can read the tool
656
+ * registry; lightweight loops that omit it never defer.
657
+ */
658
+ isExclusiveTool?: (toolName: string) => boolean;
637
659
  /**
638
660
  * Conversation this loop drives. Scopes the loop-held compaction circuit
639
661
  * breaker and is the source of truth the loop's pipeline contexts and
@@ -659,6 +681,7 @@ export class AgentLoop {
659
681
  private tools: ToolDefinition[];
660
682
  private resolveTools: ((history: Message[]) => ToolDefinition[]) | null;
661
683
  private toolExecutor: LoopToolExecutor | null;
684
+ private isExclusiveTool: ((toolName: string) => boolean) | null;
662
685
 
663
686
  /**
664
687
  * Conversation this loop drives. Source of truth for the `conversationId`
@@ -688,6 +711,7 @@ export class AgentLoop {
688
711
  tools,
689
712
  toolExecutor,
690
713
  resolveTools,
714
+ isExclusiveTool,
691
715
  conversationId,
692
716
  resolveConversationDir,
693
717
  } = options;
@@ -697,6 +721,7 @@ export class AgentLoop {
697
721
  this.tools = tools ?? [];
698
722
  this.resolveTools = resolveTools ?? null;
699
723
  this.toolExecutor = toolExecutor ?? null;
724
+ this.isExclusiveTool = isExclusiveTool ?? null;
700
725
  this.conversationId = conversationId;
701
726
  this.resolveConversationDir = resolveConversationDir ?? null;
702
727
  this.compactionCircuit = new CompactionCircuit(this.conversationId);
@@ -1883,8 +1908,39 @@ export class AgentLoop {
1883
1908
  "Tool execution start",
1884
1909
  );
1885
1910
 
1911
+ // When an exclusive tool (e.g. the advisor) is among this turn's calls,
1912
+ // it must run alone: the model should incorporate its output before
1913
+ // acting on anything else. Run only the first exclusive call and defer
1914
+ // the siblings with a benign, un-run result so the model re-issues them
1915
+ // next turn if still needed. Every tool_use still gets a matching
1916
+ // tool_result, so history stays well-formed.
1917
+ const exclusiveBlock = this.isExclusiveTool
1918
+ ? toolUseBlocks.find((block) => this.isExclusiveTool!(block.name))
1919
+ : undefined;
1920
+ const deferSiblings =
1921
+ exclusiveBlock !== undefined && toolUseBlocks.length > 1;
1922
+ if (deferSiblings) {
1923
+ rlog.info(
1924
+ {
1925
+ turn: toolUseTurns,
1926
+ exclusiveTool: exclusiveBlock!.name,
1927
+ deferred: toolUseBlocks
1928
+ .filter((block) => block !== exclusiveBlock)
1929
+ .map((block) => block.name),
1930
+ },
1931
+ "Exclusive tool present — running it alone and deferring sibling tool calls this turn",
1932
+ );
1933
+ }
1934
+
1886
1935
  const toolExecutionPromise = Promise.all(
1887
1936
  toolUseBlocks.map(async (toolUse) => {
1937
+ if (deferSiblings && toolUse !== exclusiveBlock) {
1938
+ const result: Awaited<ReturnType<LoopToolExecutor>> = {
1939
+ content: deferredForExclusiveMessage(exclusiveBlock!.name),
1940
+ isError: false,
1941
+ };
1942
+ return { toolUse, result };
1943
+ }
1888
1944
  const result = await this.toolExecutor!(
1889
1945
  toolUse.name,
1890
1946
  toolUse.input,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Maximum number of events the daemon's per-process SSE replay ring
3
+ * retains for `Last-Event-ID` resume. This is the ring's count bound; the
4
+ * ring is also bounded by total bytes and entry age (whichever limit is
5
+ * hit first wins), so the live ring can hold *fewer* than this many
6
+ * events, never more. The daemon-side definition and eviction live in
7
+ * `assistant/src/runtime/assistant-stream-state.ts`.
8
+ *
9
+ * Exposed on the API surface so the web client's SSE consumer can size
10
+ * its seq-gap tolerance against the same number the daemon buffers
11
+ * against, instead of hard-coding a duplicate.
12
+ *
13
+ * A live seq gap smaller than this is benign: the global per-assistant
14
+ * `seq` counter is stamped before fanout, but the hub deliberately
15
+ * withholds some events from a given subscriber — self-echo-suppressed
16
+ * `sync_changed` (a client's own mutation echo) and capability-targeted
17
+ * host-proxy events — so a subscriber legitimately sees its cursor skip a
18
+ * few seqs it was never going to receive. Such a hole is not data loss
19
+ * and must not trigger a destructive authoritative snapshot heal. Only a
20
+ * gap that meets or exceeds this count proves the live suffix fell
21
+ * outside the ring entirely and is genuinely non-contiguous.
22
+ */
23
+ export const SSE_REPLAY_RING_COUNT_LIMIT = 200;
24
+
25
+ /**
26
+ * Maximum age (in milliseconds) an event may reach in the daemon's SSE
27
+ * replay ring before it is evicted, regardless of count or byte usage.
28
+ * The daemon-side definition and eviction live in
29
+ * `assistant/src/runtime/assistant-stream-state.ts`.
30
+ *
31
+ * Exposed alongside the count bound so the web client can reason about
32
+ * the age dimension of the ring too. A small live seq gap is only safe to
33
+ * treat as benign when the client has been continuously receiving events
34
+ * — live delivery is concurrent with stamping, so a connected subscriber
35
+ * never relies on the ring. Once the connection has been quiet for longer
36
+ * than this window (a disconnect/resume), events the client missed may
37
+ * have aged out of the ring and become unrecoverable by replay, so even a
38
+ * small seq gap must trigger an authoritative reconcile rather than be
39
+ * waved through.
40
+ */
41
+ export const SSE_REPLAY_RING_AGE_LIMIT_MS = 30_000;
package/src/api/index.ts CHANGED
@@ -61,6 +61,10 @@ export {
61
61
  CALL_SITE_COMPACTION_AGENT,
62
62
  CALL_SITE_SYNTHETIC_AGENT_ERROR_MESSAGE,
63
63
  } from "./constants/call-sites.js";
64
+ export {
65
+ SSE_REPLAY_RING_AGE_LIMIT_MS,
66
+ SSE_REPLAY_RING_COUNT_LIMIT,
67
+ } from "./constants/sse-replay.js";
64
68
  export { DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC } from "./constants/tool-execution.js";
65
69
  export {
66
70
  type AssistantActivityAnchor,
@@ -430,6 +434,8 @@ export {
430
434
  LlmContextResponseSchema,
431
435
  } from "./responses/llm-context-response.js";
432
436
  export {
437
+ type LLMCallError,
438
+ LLMCallErrorSchema,
433
439
  type LLMCallSummary,
434
440
  LLMCallSummarySchema,
435
441
  type LLMContextSection,
@@ -67,6 +67,30 @@ export const LLMContextSectionSchema = z.object({
67
67
 
68
68
  export type LLMContextSection = z.infer<typeof LLMContextSectionSchema>;
69
69
 
70
+ /**
71
+ * Structured provider/transport error recorded when an LLM call was
72
+ * rejected before producing a response. Mirrors the on-disk
73
+ * `responsePayload.error` shape written by
74
+ * `buildProviderErrorResponsePayload` — the inspector branches on the
75
+ * presence of this field to render a failed call distinctly (failure
76
+ * banner in the Response tab, $0.00 cost in the rail, etc.) instead of
77
+ * the generic "section rendering unavailable" fallback.
78
+ *
79
+ * Every field is optional because the serializer degrades a plain
80
+ * `Error` down to just `{ name, message }`; only the wrapper object is
81
+ * guaranteed.
82
+ */
83
+ export const LLMCallErrorSchema = z.object({
84
+ name: z.string().nullish(),
85
+ message: z.string().nullish(),
86
+ code: z.string().nullish(),
87
+ provider: z.string().nullish(),
88
+ statusCode: z.number().nullish(),
89
+ retryAfterMs: z.number().nullish(),
90
+ });
91
+
92
+ export type LLMCallError = z.infer<typeof LLMCallErrorSchema>;
93
+
70
94
  /**
71
95
  * One LLM request log row.
72
96
  *
@@ -88,6 +112,7 @@ export const LLMRequestLogEntrySchema = z.object({
88
112
  responseSections: z.array(LLMContextSectionSchema).nullish(),
89
113
  agentLoopExitReason: z.string().nullish(),
90
114
  callSite: z.string().nullish(),
115
+ error: LLMCallErrorSchema.nullish(),
91
116
  });
92
117
 
93
118
  export type LLMRequestLogEntry = z.infer<typeof LLMRequestLogEntrySchema>;
@@ -31,6 +31,23 @@ export const SubagentDetailEventSchema = z.object({
31
31
  toolName: z.string().optional(),
32
32
  isError: z.boolean().optional(),
33
33
  messageId: z.string().optional(),
34
+ /**
35
+ * Tool-call id — the `tool_use.id` on a tool-call event and the referencing
36
+ * `tool_use_id` on its tool-result event, in the daemon's canonical
37
+ * content-block format. That format is provider-agnostic: every provider
38
+ * (Anthropic, OpenAI, Gemini, …) normalizes its native tool calls into these
39
+ * `tool_use`/`tool_result` blocks (see `providers/types.ts`), so this id is
40
+ * present regardless of which model produced the call. Lets the web client
41
+ * pair a result with its call and key the nested tool-detail view, so tool
42
+ * pills on reloaded/history subagents are clickable (not just live ones).
43
+ */
44
+ toolUseId: z.string().optional(),
45
+ /**
46
+ * Raw tool input object on tool-call events. (`content` also carries a
47
+ * JSON-stringified copy for back-compat / label derivation.) Surfaced in the
48
+ * tool-detail view's input section.
49
+ */
50
+ input: z.record(z.string(), z.unknown()).optional(),
34
51
  });
35
52
 
36
53
  export type SubagentDetailEvent = z.infer<typeof SubagentDetailEventSchema>;
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { beforeEach, describe, expect, mock, test } from "bun:test";
17
17
 
18
- import type { AdmissionPolicy } from "@vellumai/gateway-client";
18
+ import type { AdmissionPolicy, TrustVerdict } from "@vellumai/gateway-client";
19
19
 
20
20
  import type {
21
21
  ChannelPolicy,
@@ -38,10 +38,21 @@ mock.module("../../config/loader.js", () => ({
38
38
  getConfig: () => ({ calls: { verification: { enabled: false } } }),
39
39
  }));
40
40
 
41
- // Controllable resolved trust context.
41
+ // Controllable resolved trust context. `resolveActorTrust` is a tracked mock
42
+ // so the verdict-source tests can assert the local fallback fires (or not).
43
+ // The verdict path uses the REAL, pure `actorTrustContextFromVerdict` /
44
+ // `verdictMemberUnresolvable` — no module mock — so this file leaks nothing
45
+ // into sibling test files sharing the bun process.
42
46
  let nextTrust: ActorTrustContext;
47
+ const resolveActorTrustMock = mock(() => nextTrust);
48
+ // Override only `resolveActorTrust`; the real `trust-verdict-consumer` imports
49
+ // `toTrustContext` from this module, so the rest must pass through untouched.
50
+ const actorTrustResolverModule = await import(
51
+ "../../runtime/actor-trust-resolver.js"
52
+ );
43
53
  mock.module("../../runtime/actor-trust-resolver.js", () => ({
44
- resolveActorTrust: () => nextTrust,
54
+ ...actorTrustResolverModule,
55
+ resolveActorTrust: resolveActorTrustMock,
45
56
  }));
46
57
 
47
58
  // Controllable pending verification challenge.
@@ -151,13 +162,17 @@ function makeTrust(
151
162
  };
152
163
  }
153
164
 
154
- function route(admissionPolicy?: AdmissionPolicy | null) {
165
+ function route(
166
+ admissionPolicy?: AdmissionPolicy | null,
167
+ verdict?: TrustVerdict | null,
168
+ ) {
155
169
  return routeSetup({
156
170
  callSessionId: "cs_1",
157
171
  session: null, // inbound
158
172
  from: "+12025550142",
159
173
  to: "+12025550199",
160
174
  admissionPolicy,
175
+ verdict,
161
176
  });
162
177
  }
163
178
 
@@ -165,6 +180,7 @@ beforeEach(() => {
165
180
  pendingChallenge = null;
166
181
  activeInvites = [];
167
182
  boundContact = null;
183
+ resolveActorTrustMock.mockClear();
168
184
  });
169
185
 
170
186
  // ---------------------------------------------------------------------------
@@ -374,3 +390,245 @@ describe("routeSetup — floor bypasses", () => {
374
390
  expect(outcome.action).toBe("verification");
375
391
  });
376
392
  });
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // Caller-trust source: gateway verdict first, local fallback
396
+ // ---------------------------------------------------------------------------
397
+
398
+ function makeVerdict(overrides: Partial<TrustVerdict> = {}): TrustVerdict {
399
+ return {
400
+ trustClass: "guardian",
401
+ canonicalSenderId: "+12025550142",
402
+ ...overrides,
403
+ };
404
+ }
405
+
406
+ // A verdict carrying a fully-resolvable member ACL (contactId/channelId + valid
407
+ // known status·policy enums). The REAL `resolvedMemberFromVerdict` synthesizes
408
+ // a memberRecord from these, so the verdict path enforces blocked/revoked/deny.
409
+ function makeMemberVerdict(
410
+ trustClass: TrustVerdict["trustClass"],
411
+ channel: { status: string; policy?: string },
412
+ overrides: Partial<TrustVerdict> = {},
413
+ ): TrustVerdict {
414
+ return makeVerdict({
415
+ trustClass,
416
+ contactId: "ct_1",
417
+ channelId: "ch_1",
418
+ status: channel.status,
419
+ policy: channel.policy ?? "allow",
420
+ ...overrides,
421
+ });
422
+ }
423
+
424
+ describe("routeSetup — caller-trust source", () => {
425
+ test("present verdict builds trust from the verdict (no local resolve)", () => {
426
+ const { resolved, outcome } = route(
427
+ null,
428
+ makeMemberVerdict("guardian", { status: "active" }),
429
+ );
430
+
431
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
432
+ expect(resolved.actorTrust.trustClass).toBe("guardian");
433
+ expect(outcome.action).toBe("normal_call");
434
+ });
435
+
436
+ test("resolutionFailed verdict falls back to local resolveActorTrust", () => {
437
+ nextTrust = makeTrust("guardian", { status: "active", role: "guardian" });
438
+ const { resolved } = route(null, makeVerdict({ resolutionFailed: true }));
439
+
440
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
441
+ expect(resolved.actorTrust.trustClass).toBe("guardian");
442
+ });
443
+
444
+ test("null verdict falls back to local resolveActorTrust", () => {
445
+ nextTrust = makeTrust("trusted_contact", { status: "active" });
446
+ const { resolved } = route(null, null);
447
+
448
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
449
+ expect(resolved.actorTrust.trustClass).toBe("trusted_contact");
450
+ });
451
+
452
+ test("absent verdict falls back to local resolveActorTrust", () => {
453
+ nextTrust = makeTrust("guardian", { status: "active", role: "guardian" });
454
+ route(null);
455
+
456
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
457
+ });
458
+
459
+ test("admission floor still applies on the verdict path (guardian_only denies trusted_contact)", () => {
460
+ const { outcome } = route(
461
+ "guardian_only",
462
+ makeMemberVerdict("trusted_contact", { status: "active" }),
463
+ );
464
+
465
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
466
+ expect(outcome.action).toBe("deny");
467
+ });
468
+
469
+ test("admission floor still applies on the fallback path (guardian_only denies trusted_contact)", () => {
470
+ nextTrust = makeTrust("trusted_contact", { status: "active" });
471
+ const { outcome } = route(
472
+ "guardian_only",
473
+ makeVerdict({ resolutionFailed: true }),
474
+ );
475
+
476
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
477
+ expect(outcome.action).toBe("deny");
478
+ });
479
+ });
480
+
481
+ // ---------------------------------------------------------------------------
482
+ // Verdict-path ACL: blocked / revoked / deny enforced from the verdict-derived
483
+ // memberRecord (no local fallback). Guards the P1 where a verdict member with
484
+ // no memberRecord bypassed these gates.
485
+ // ---------------------------------------------------------------------------
486
+
487
+ describe("routeSetup — verdict path enforces member ACL", () => {
488
+ test("blocked member via verdict is denied (not normal_call) under permissive floor", () => {
489
+ const { outcome } = route(
490
+ "strangers",
491
+ makeMemberVerdict("unknown", { status: "blocked" }),
492
+ );
493
+
494
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
495
+ expect(outcome.action).toBe("deny");
496
+ });
497
+
498
+ test("revoked member via verdict is denied under permissive floor", () => {
499
+ const { outcome } = route(
500
+ "strangers",
501
+ makeMemberVerdict("unknown", { status: "revoked" }),
502
+ );
503
+
504
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
505
+ expect(outcome.action).toBe("deny");
506
+ });
507
+
508
+ test("policy deny member via verdict is denied (not normal_call)", () => {
509
+ const { outcome } = route(
510
+ null,
511
+ makeMemberVerdict("trusted_contact", {
512
+ status: "active",
513
+ policy: "deny",
514
+ }),
515
+ );
516
+
517
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
518
+ expect(outcome.action).toBe("deny");
519
+ });
520
+
521
+ test("policy escalate member via verdict is denied (live call can't await approval)", () => {
522
+ const { outcome } = route(
523
+ null,
524
+ makeMemberVerdict("trusted_contact", {
525
+ status: "active",
526
+ policy: "escalate",
527
+ }),
528
+ );
529
+
530
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
531
+ expect(outcome.action).toBe("deny");
532
+ });
533
+
534
+ test("trusted/active member via verdict still admits to normal_call", () => {
535
+ const { outcome } = route(
536
+ null,
537
+ makeMemberVerdict("trusted_contact", {
538
+ status: "active",
539
+ policy: "allow",
540
+ }),
541
+ );
542
+
543
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
544
+ expect(outcome.action).toBe("normal_call");
545
+ });
546
+
547
+ test("guardian via verdict still admits to normal_call", () => {
548
+ const { outcome } = route(
549
+ null,
550
+ makeMemberVerdict("guardian", { status: "active" }),
551
+ );
552
+
553
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
554
+ expect(outcome.action).toBe("normal_call");
555
+ });
556
+ });
557
+
558
+ // ---------------------------------------------------------------------------
559
+ // Unresolvable member verdict → local fallback (never trust an un-ACL-checkable
560
+ // member). A verdict claiming a member (contactId/channelId) whose ACL can't be
561
+ // reassembled (missing/unknown status·policy) must take the local resolveActorTrust
562
+ // path so the member is ACL-checked locally, not trusted by trustClass.
563
+ // ---------------------------------------------------------------------------
564
+
565
+ describe("routeSetup — unresolvable member verdict falls back to local", () => {
566
+ test("member identity with missing status falls back to local resolveActorTrust", () => {
567
+ nextTrust = makeTrust("trusted_contact", { status: "active" });
568
+ const { resolved } = route(
569
+ null,
570
+ makeVerdict({
571
+ trustClass: "trusted_contact",
572
+ contactId: "ct_1",
573
+ channelId: "ch_1",
574
+ policy: "allow",
575
+ // status absent → unresolvable
576
+ }),
577
+ );
578
+
579
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
580
+ expect(resolved.actorTrust.trustClass).toBe("trusted_contact");
581
+ });
582
+
583
+ test("member identity with unknown status falls back to local resolveActorTrust", () => {
584
+ nextTrust = makeTrust("trusted_contact", { status: "active" });
585
+ route(
586
+ null,
587
+ makeVerdict({
588
+ trustClass: "trusted_contact",
589
+ contactId: "ct_1",
590
+ channelId: "ch_1",
591
+ status: "bogus",
592
+ policy: "allow",
593
+ }),
594
+ );
595
+
596
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
597
+ });
598
+
599
+ test("member identity with unknown policy falls back to local resolveActorTrust", () => {
600
+ nextTrust = makeTrust("trusted_contact", { status: "active" });
601
+ route(
602
+ null,
603
+ makeVerdict({
604
+ trustClass: "trusted_contact",
605
+ contactId: "ct_1",
606
+ channelId: "ch_1",
607
+ status: "active",
608
+ policy: "bogus",
609
+ }),
610
+ );
611
+
612
+ expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
613
+ });
614
+
615
+ test("real stranger verdict (no member identity) still takes the verdict path", () => {
616
+ const { resolved } = route(null, makeVerdict({ trustClass: "unknown" }));
617
+
618
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
619
+ expect(resolved.actorTrust.trustClass).toBe("unknown");
620
+ });
621
+
622
+ test("valid member verdict (good status+policy) still takes the verdict path", () => {
623
+ const { outcome } = route(
624
+ null,
625
+ makeMemberVerdict("trusted_contact", {
626
+ status: "active",
627
+ policy: "allow",
628
+ }),
629
+ );
630
+
631
+ expect(resolveActorTrustMock).not.toHaveBeenCalled();
632
+ expect(outcome.action).toBe("normal_call");
633
+ });
634
+ });