@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
@@ -43,6 +43,9 @@ import type {
43
43
 
44
44
  export class CallSiteRoutingProvider implements Provider {
45
45
  public readonly tokenEstimationProvider?: string;
46
+ // Forward native web-search capability so it survives the wrapper chain
47
+ // (callers like the advisor consult gate on it). Fixed at construction.
48
+ public readonly supportsNativeWebSearch?: boolean;
46
49
 
47
50
  // Per-call async context that tracks which provider is currently executing.
48
51
  // Using AsyncLocalStorage instead of a plain instance field means concurrent
@@ -94,6 +97,7 @@ export class CallSiteRoutingProvider implements Provider {
94
97
  ) => Promise<Provider | null>,
95
98
  ) {
96
99
  this.tokenEstimationProvider = defaultProvider.tokenEstimationProvider;
100
+ this.supportsNativeWebSearch = defaultProvider.supportsNativeWebSearch;
97
101
  if (defaultProvider.countInputTokens) {
98
102
  this.countInputTokens =
99
103
  defaultProvider.countInputTokens.bind(defaultProvider);
@@ -760,6 +760,22 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
760
760
  maxEffort: "max",
761
761
  pricing: { inputPer1mTokens: 1.74, outputPer1mTokens: 3.48 },
762
762
  },
763
+ {
764
+ id: "accounts/fireworks/models/deepseek-v4-flash",
765
+ displayName: "DeepSeek V4 Flash",
766
+ contextWindowTokens: 1040000,
767
+ maxOutputTokens: 131072,
768
+ supportsThinking: true,
769
+ supportsCaching: true,
770
+ supportsVision: false,
771
+ supportsToolUse: true,
772
+ maxEffort: "max",
773
+ pricing: {
774
+ inputPer1mTokens: 0.14,
775
+ outputPer1mTokens: 0.28,
776
+ cacheReadPer1mTokens: 0.03,
777
+ },
778
+ },
763
779
  ],
764
780
  defaultModel: "accounts/fireworks/models/kimi-k2p5",
765
781
  apiKeyUrl: "https://fireworks.ai/account/api-keys",
@@ -194,6 +194,11 @@ export class OpenAIResponsesProvider implements Provider {
194
194
  this.useNativeWebSearch = options.useNativeWebSearch ?? false;
195
195
  }
196
196
 
197
+ /** See {@link Provider.supportsNativeWebSearch}. */
198
+ get supportsNativeWebSearch(): boolean {
199
+ return this.useNativeWebSearch;
200
+ }
201
+
197
202
  async sendMessage(
198
203
  messages: Message[],
199
204
  options?: SendMessageOptions,
@@ -140,6 +140,11 @@ export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
140
140
  return isAnthropicModel(this.defaultModel) ? "anthropic" : this.name;
141
141
  }
142
142
 
143
+ /** See {@link Provider.supportsNativeWebSearch}. Set per model at construction. */
144
+ get supportsNativeWebSearch(): boolean {
145
+ return this.useNativeWebSearch;
146
+ }
147
+
143
148
  override async sendMessage(
144
149
  messages: Message[],
145
150
  options?: SendMessageOptions,
@@ -43,6 +43,9 @@ let lazyInitPromise: Promise<void> | null = null;
43
43
  export class CallSiteConfiguredProvider implements Provider {
44
44
  public readonly name: string;
45
45
  public readonly tokenEstimationProvider?: string;
46
+ // Forward native web-search capability so it survives the wrapper chain
47
+ // (callers like the advisor consult gate on it). Fixed at construction.
48
+ public readonly supportsNativeWebSearch?: boolean;
46
49
 
47
50
  constructor(
48
51
  private readonly inner: Provider,
@@ -52,6 +55,7 @@ export class CallSiteConfiguredProvider implements Provider {
52
55
  ) {
53
56
  this.name = inner.name;
54
57
  this.tokenEstimationProvider = inner.tokenEstimationProvider;
58
+ this.supportsNativeWebSearch = inner.supportsNativeWebSearch;
55
59
  }
56
60
 
57
61
  sendMessage(
@@ -23,6 +23,10 @@ export class RateLimitProvider implements Provider {
23
23
  return this.inner.tokenEstimationProvider;
24
24
  }
25
25
 
26
+ get supportsNativeWebSearch(): boolean | undefined {
27
+ return this.inner.supportsNativeWebSearch;
28
+ }
29
+
26
30
  private requestTimestamps: number[];
27
31
 
28
32
  // Forward the optional token-counting endpoint so the capability survives
@@ -618,6 +618,10 @@ export class RetryProvider implements Provider {
618
618
  return this.inner.tokenEstimationProvider;
619
619
  }
620
620
 
621
+ get supportsNativeWebSearch(): boolean | undefined {
622
+ return this.inner.supportsNativeWebSearch;
623
+ }
624
+
621
625
  // Forward the optional token-counting endpoint so the capability survives
622
626
  // the wrapper chain (callers gate on its presence). Bound straight to the
623
627
  // inner provider — count_tokens is a cheap separate endpoint and its caller
@@ -265,6 +265,15 @@ export interface Provider {
265
265
  * Falls back to `name` when unset.
266
266
  */
267
267
  tokenEstimationProvider?: string;
268
+ /**
269
+ * True when this provider instance was constructed to run web search
270
+ * server-side (provider-native). The native search only activates when a
271
+ * `web_search`-named tool is passed in the request, so callers that want to
272
+ * enable web search on a one-shot completion (e.g. the advisor consult) check
273
+ * this first — passing the tool to a non-native instance would surface an
274
+ * unexecutable client tool call. Absent/false on providers without it.
275
+ */
276
+ supportsNativeWebSearch?: boolean;
268
277
  sendMessage(
269
278
  messages: Message[],
270
279
  options?: SendMessageOptions,
@@ -18,6 +18,9 @@ const log = getLogger("provider-usage-tracking");
18
18
  export class UsageTrackingProvider implements Provider {
19
19
  public readonly name: string;
20
20
  public readonly tokenEstimationProvider?: string;
21
+ // Forward native web-search capability so it survives the wrapper chain
22
+ // (callers like the advisor consult gate on it). Fixed at construction.
23
+ public readonly supportsNativeWebSearch?: boolean;
21
24
  // Forward the optional token-counting endpoint so the capability survives
22
25
  // the wrapper chain. Bound straight to the inner provider — count_tokens is
23
26
  // not billed, so there's no usage to track.
@@ -26,6 +29,7 @@ export class UsageTrackingProvider implements Provider {
26
29
  constructor(private readonly inner: Provider) {
27
30
  this.name = inner.name;
28
31
  this.tokenEstimationProvider = inner.tokenEstimationProvider;
32
+ this.supportsNativeWebSearch = inner.supportsNativeWebSearch;
29
33
  if (inner.countInputTokens) {
30
34
  this.countInputTokens = inner.countInputTokens.bind(inner);
31
35
  }
@@ -0,0 +1,133 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Gateway guardian-delivery list drives both getGuardianBinding and isGuardian:
4
+ // null = couldn't determine, [] = authoritative unbound, one active entry =
5
+ // bound. Tests set this to mirror the gateway-owned ACL state.
6
+ let mockGuardianList: Array<Record<string, unknown>> | null = [];
7
+ const cachedCalls: Array<{ channelTypes?: string[] } | undefined> = [];
8
+
9
+ const resolveList = (input?: { channelTypes?: string[] }) => {
10
+ cachedCalls.push(input);
11
+ return Promise.resolve(mockGuardianList);
12
+ };
13
+
14
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
15
+ getGuardianDelivery: resolveList,
16
+ getGuardianDeliveryFresh: resolveList,
17
+ guardianForChannel: (
18
+ list: Array<{ channelType: string; status: string }>,
19
+ channelType: string,
20
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
21
+ }));
22
+
23
+ const { getGuardianBinding, isGuardian } = await import(
24
+ "../channel-verification-service.js"
25
+ );
26
+
27
+ const TELEGRAM_DELIVERY = {
28
+ channelType: "telegram",
29
+ contactId: "contact-1",
30
+ principalId: "principal-1",
31
+ displayName: "Guardian",
32
+ address: "guardian-handle",
33
+ externalChatId: "chat-1",
34
+ status: "active",
35
+ verifiedAt: 1700,
36
+ };
37
+
38
+ describe("getGuardianBinding", () => {
39
+ beforeEach(() => {
40
+ mockGuardianList = [];
41
+ cachedCalls.length = 0;
42
+ });
43
+
44
+ test("filters delivery by the requested channel type", async () => {
45
+ await getGuardianBinding("asst-1", "telegram");
46
+ expect(cachedCalls).toEqual([{ channelTypes: ["telegram"] }]);
47
+ });
48
+
49
+ test("returns null when no guardian is bound", async () => {
50
+ mockGuardianList = [];
51
+ expect(await getGuardianBinding("asst-1", "telegram")).toBeNull();
52
+ });
53
+
54
+ test("returns null when the gateway is unreachable", async () => {
55
+ mockGuardianList = null;
56
+ expect(await getGuardianBinding("asst-1", "telegram")).toBeNull();
57
+ });
58
+
59
+ test("synthesizes the binding from the gateway delivery", async () => {
60
+ mockGuardianList = [TELEGRAM_DELIVERY];
61
+
62
+ const binding = await getGuardianBinding("asst-1", "telegram");
63
+
64
+ expect(binding).not.toBeNull();
65
+ expect(binding?.assistantId).toBe("asst-1");
66
+ expect(binding?.channel).toBe("telegram");
67
+ expect(binding?.id).toBe("contact-1");
68
+ expect(binding?.guardianPrincipalId).toBe("principal-1");
69
+ expect(binding?.guardianExternalUserId).toBe("guardian-handle");
70
+ expect(binding?.guardianDeliveryChatId).toBe("chat-1");
71
+ expect(binding?.verifiedAt).toBe(1700);
72
+ expect(binding?.status).toBe("active");
73
+ expect(binding?.verifiedVia).toBe("verified");
74
+ });
75
+
76
+ test("falls back to empty strings for absent optional delivery fields", async () => {
77
+ mockGuardianList = [
78
+ {
79
+ channelType: "telegram",
80
+ contactId: "contact-2",
81
+ address: "addr",
82
+ status: "active",
83
+ },
84
+ ];
85
+
86
+ const binding = await getGuardianBinding("asst-1", "telegram");
87
+
88
+ expect(binding?.guardianPrincipalId).toBe("");
89
+ expect(binding?.guardianDeliveryChatId).toBe("");
90
+ expect(binding?.verifiedAt).toBe(0);
91
+ });
92
+
93
+ test("ignores deliveries for a different channel", async () => {
94
+ mockGuardianList = [TELEGRAM_DELIVERY];
95
+ expect(await getGuardianBinding("asst-1", "phone")).toBeNull();
96
+ });
97
+ });
98
+
99
+ describe("isGuardian", () => {
100
+ beforeEach(() => {
101
+ mockGuardianList = [];
102
+ cachedCalls.length = 0;
103
+ });
104
+
105
+ test("returns true when the address matches the gateway guardian", async () => {
106
+ mockGuardianList = [TELEGRAM_DELIVERY];
107
+ expect(await isGuardian("asst-1", "telegram", "guardian-handle")).toBe(true);
108
+ });
109
+
110
+ test("compares case-insensitively", async () => {
111
+ mockGuardianList = [TELEGRAM_DELIVERY];
112
+ expect(await isGuardian("asst-1", "telegram", "GUARDIAN-HANDLE")).toBe(true);
113
+ });
114
+
115
+ test("returns false for a non-matching address", async () => {
116
+ mockGuardianList = [TELEGRAM_DELIVERY];
117
+ expect(await isGuardian("asst-1", "telegram", "someone-else")).toBe(false);
118
+ });
119
+
120
+ test("returns false when no guardian is bound", async () => {
121
+ mockGuardianList = [];
122
+ expect(await isGuardian("asst-1", "telegram", "guardian-handle")).toBe(
123
+ false,
124
+ );
125
+ });
126
+
127
+ test("returns false when the gateway is unreachable", async () => {
128
+ mockGuardianList = null;
129
+ expect(await isGuardian("asst-1", "telegram", "guardian-handle")).toBe(
130
+ false,
131
+ );
132
+ });
133
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Unit tests for the narrow reset-drift trust-recovery helper
3
+ * `reResolveTrustOnResetDrift`.
4
+ *
5
+ * The real helper runs against mocked leaf deps: the gateway guardian read
6
+ * (`getGuardianDelivery`/`guardianForChannel`), the local-mirror heal
7
+ * (`findGuardianForChannel`/`updateContactPrincipalAndChannel`, which the real
8
+ * `healGuardianBindingDrift` drives), and the local trust resolver
9
+ * (`resolveTrustContext`). Heal invocations are observed via the contact-store
10
+ * write mock.
11
+ */
12
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ mock.module("../../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
17
+ }));
18
+
19
+ let mockGuardianList: Array<Record<string, unknown>> | null = [];
20
+
21
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
22
+ getGuardianDelivery: async () => mockGuardianList,
23
+ guardianForChannel: (
24
+ list: Array<Record<string, unknown>>,
25
+ channelType: string,
26
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
27
+ }));
28
+
29
+ // Local mirror the real heal reads/writes. `findGuardianForChannel` returns the
30
+ // stored guardian; `updateContactPrincipalAndChannel` records heal writes.
31
+ let mockLocalGuardian: {
32
+ contact: { id: string; principalId: string };
33
+ channel: { id: string };
34
+ } | null = null;
35
+ const healWrites: Array<{ principalId: string }> = [];
36
+
37
+ mock.module("../../contacts/contact-store.js", () => ({
38
+ findGuardianForChannel: () => mockLocalGuardian,
39
+ updateContactPrincipalAndChannel: (
40
+ _contactId: string,
41
+ _channelId: string,
42
+ principalId: string,
43
+ ) => {
44
+ healWrites.push({ principalId });
45
+ return true;
46
+ },
47
+ }));
48
+
49
+ // The local trust resolver returns guardian for the actor; the gate threads
50
+ // sourceChannel via the real withSourceChannel wrapper.
51
+ mock.module("../../runtime/trust-context-resolver.js", () => ({
52
+ resolveTrustContext: (input: { actorExternalId?: string }) => ({
53
+ trustClass: "guardian",
54
+ sourceChannel: "vellum",
55
+ resolvedActor: input.actorExternalId,
56
+ }),
57
+ withSourceChannel: (sourceChannel: unknown, ctx: Record<string, unknown>) => ({
58
+ ...ctx,
59
+ sourceChannel,
60
+ }),
61
+ }));
62
+
63
+ const { reResolveTrustOnResetDrift } = await import(
64
+ "../guardian-vellum-migration.js"
65
+ );
66
+
67
+ function gatewayGuardian(principalId: string): Record<string, unknown> {
68
+ return {
69
+ channelType: "vellum",
70
+ contactId: "guardian-contact",
71
+ principalId,
72
+ address: principalId,
73
+ status: "active",
74
+ };
75
+ }
76
+
77
+ function localGuardian(principalId: string) {
78
+ return {
79
+ contact: { id: "contact-1", principalId },
80
+ channel: { id: "channel-1" },
81
+ };
82
+ }
83
+
84
+ describe("reResolveTrustOnResetDrift", () => {
85
+ beforeEach(() => {
86
+ mockGuardianList = [];
87
+ mockLocalGuardian = null;
88
+ healWrites.length = 0;
89
+ });
90
+
91
+ test("reset drift: heals and returns the re-resolved guardian ctx", async () => {
92
+ // Stale local mirror still holds the pre-reset principal; the incoming JWT
93
+ // carries the old one. Heal repairs the mirror toward the incoming actor.
94
+ mockGuardianList = [gatewayGuardian("vellum-principal-new")];
95
+ mockLocalGuardian = localGuardian("vellum-principal-stale");
96
+
97
+ const ctx = await reResolveTrustOnResetDrift(
98
+ "vellum-principal-old",
99
+ "vellum",
100
+ );
101
+
102
+ expect(ctx?.trustClass).toBe("guardian");
103
+ expect(healWrites).toEqual([{ principalId: "vellum-principal-old" }]);
104
+ });
105
+
106
+ test("repeat drift where heal no-ops still returns the guardian ctx", async () => {
107
+ // Local mirror already matches the incoming principal, so heal's write is
108
+ // skipped, but the gate still passes and the re-resolve yields guardian.
109
+ mockGuardianList = [gatewayGuardian("vellum-principal-new")];
110
+ mockLocalGuardian = localGuardian("vellum-principal-old");
111
+
112
+ const ctx = await reResolveTrustOnResetDrift(
113
+ "vellum-principal-old",
114
+ "vellum",
115
+ );
116
+
117
+ expect(ctx?.trustClass).toBe("guardian");
118
+ expect(healWrites).toEqual([]);
119
+ });
120
+
121
+ test("gateway unreachable (null): returns null, heal not called", async () => {
122
+ mockGuardianList = null;
123
+ mockLocalGuardian = localGuardian("vellum-principal-old");
124
+
125
+ const ctx = await reResolveTrustOnResetDrift(
126
+ "vellum-principal-old",
127
+ "vellum",
128
+ );
129
+
130
+ expect(ctx).toBeNull();
131
+ expect(healWrites).toEqual([]);
132
+ });
133
+
134
+ test("empty/revoked gateway (no active guardian): returns null, heal not called", async () => {
135
+ mockGuardianList = [];
136
+ mockLocalGuardian = localGuardian("vellum-principal-old");
137
+
138
+ const ctx = await reResolveTrustOnResetDrift(
139
+ "vellum-principal-old",
140
+ "vellum",
141
+ );
142
+
143
+ expect(ctx).toBeNull();
144
+ expect(healWrites).toEqual([]);
145
+ });
146
+
147
+ test("gateway guardian is a real (non vellum-principal-*) id: returns null", async () => {
148
+ mockGuardianList = [gatewayGuardian("user@example.com")];
149
+ mockLocalGuardian = localGuardian("vellum-principal-old");
150
+
151
+ const ctx = await reResolveTrustOnResetDrift(
152
+ "vellum-principal-old",
153
+ "vellum",
154
+ );
155
+
156
+ expect(ctx).toBeNull();
157
+ expect(healWrites).toEqual([]);
158
+ });
159
+
160
+ test("incoming principal is not vellum-principal-*: returns null", async () => {
161
+ mockGuardianList = [gatewayGuardian("vellum-principal-new")];
162
+ mockLocalGuardian = localGuardian("vellum-principal-old");
163
+
164
+ const ctx = await reResolveTrustOnResetDrift("user@example.com", "vellum");
165
+
166
+ expect(ctx).toBeNull();
167
+ expect(healWrites).toEqual([]);
168
+ });
169
+
170
+ test("threads sourceChannel into the returned ctx", async () => {
171
+ mockGuardianList = [gatewayGuardian("vellum-principal-new")];
172
+ mockLocalGuardian = localGuardian("vellum-principal-old");
173
+
174
+ const ctx = await reResolveTrustOnResetDrift(
175
+ "vellum-principal-old",
176
+ "telegram",
177
+ );
178
+
179
+ expect(ctx?.sourceChannel).toBe("telegram");
180
+ });
181
+ });
@@ -0,0 +1,66 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Gateway guardian-delivery list: null = couldn't determine (transport failure
4
+ // OR gateway-side resolver/DB error), [] = authoritative unbound, one active
5
+ // entry = bound.
6
+ let mockGuardianList: Array<Record<string, unknown>> | null = [];
7
+ const freshCalls: Array<{ channelTypes?: string[] } | undefined> = [];
8
+
9
+ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
10
+ // Existence guard reads fresh (uncached); the binding/identity reads use the
11
+ // cached variant. The service imports both, so both must be stubbed.
12
+ getGuardianDeliveryFresh: (input?: { channelTypes?: string[] }) => {
13
+ freshCalls.push(input);
14
+ return Promise.resolve(mockGuardianList);
15
+ },
16
+ getGuardianDelivery: () => Promise.resolve(mockGuardianList),
17
+ guardianForChannel: (
18
+ list: Array<{ channelType: string; status: string }>,
19
+ channelType: string,
20
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
21
+ }));
22
+
23
+ const { isGuardianBoundForChannel } = await import(
24
+ "../channel-verification-service.js"
25
+ );
26
+
27
+ describe("isGuardianBoundForChannel", () => {
28
+ beforeEach(() => {
29
+ mockGuardianList = [];
30
+ freshCalls.length = 0;
31
+ });
32
+
33
+ test("reads fresh so a stale cached empty list can't mask a present guardian", async () => {
34
+ await isGuardianBoundForChannel("telegram");
35
+ expect(freshCalls).toEqual([{ channelTypes: ["telegram"] }]);
36
+ });
37
+
38
+ test("returns false when no guardian is bound", async () => {
39
+ mockGuardianList = [];
40
+ expect(await isGuardianBoundForChannel("telegram")).toBe(false);
41
+ });
42
+
43
+ test("returns true when a guardian is bound", async () => {
44
+ mockGuardianList = [{ channelType: "telegram", status: "active" }];
45
+ expect(await isGuardianBoundForChannel("telegram")).toBe(true);
46
+ });
47
+
48
+ test("null list (gateway unreachable) is treated as bound", async () => {
49
+ mockGuardianList = null;
50
+ expect(await isGuardianBoundForChannel("telegram")).toBe(true);
51
+ });
52
+
53
+ test("gateway resolver error (null, not []) is treated as bound — no duplicate", async () => {
54
+ // A gateway-side DB/resolver error now reaches the reader as null (the
55
+ // handler no longer swallows it into an empty list), so the guard's
56
+ // null fail-safe applies and reports bound instead of mis-reading the
57
+ // error as "no guardian" and allowing a duplicate binding.
58
+ mockGuardianList = null;
59
+ expect(await isGuardianBoundForChannel("telegram")).toBe(true);
60
+ });
61
+
62
+ test("genuine empty ([], not null) reports unbound so first-bind is allowed", async () => {
63
+ mockGuardianList = [];
64
+ expect(await isGuardianBoundForChannel("telegram")).toBe(false);
65
+ });
66
+ });