@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,256 @@
1
+ /**
2
+ * Verifies resolveDestinations resolves guardian delivery endpoints from the
3
+ * gateway-provided guardian list, with shapes identical to the local read, and
4
+ * falls back to the local contacts read when the list is null.
5
+ */
6
+
7
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
8
+
9
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
10
+
11
+ mock.module("../../util/logger.js", () => ({
12
+ getLogger: () =>
13
+ new Proxy({} as Record<string, unknown>, {
14
+ get: () => () => {},
15
+ }),
16
+ }));
17
+
18
+ // Local fallback read; mocked so the null-list path is deterministic.
19
+ let localGuardian:
20
+ | { contact: { principalId?: string }; channel: { address: string; externalChatId?: string } }
21
+ | null = null;
22
+
23
+ mock.module("../../contacts/contact-store.js", () => ({
24
+ findGuardianForChannel: (_channelType: string) => localGuardian,
25
+ }));
26
+
27
+ const { resolveDestinations } = await import("../destination-resolver.js");
28
+
29
+ function guardian(
30
+ overrides: Partial<GuardianDelivery> & Pick<GuardianDelivery, "channelType" | "address">,
31
+ ): GuardianDelivery {
32
+ return {
33
+ contactId: "contact-1",
34
+ status: "active",
35
+ ...overrides,
36
+ } as GuardianDelivery;
37
+ }
38
+
39
+ describe("resolveDestinations — gateway guardian list", () => {
40
+ beforeEach(() => {
41
+ localGuardian = null;
42
+ });
43
+
44
+ test("vellum carries guardianPrincipalId from the gateway list", () => {
45
+ const list = [
46
+ guardian({ channelType: "vellum", address: "user@example.com", principalId: "prin-1" }),
47
+ ];
48
+ const result = resolveDestinations(["vellum"], list);
49
+ expect(result.get("vellum")).toEqual({
50
+ channel: "vellum",
51
+ metadata: { guardianPrincipalId: "prin-1" },
52
+ });
53
+ });
54
+
55
+ test("platform carries guardianPrincipalId from the vellum guardian", () => {
56
+ const list = [
57
+ guardian({ channelType: "vellum", address: "user@example.com", principalId: "prin-1" }),
58
+ ];
59
+ const result = resolveDestinations(["platform"], list);
60
+ expect(result.get("platform")).toEqual({
61
+ channel: "platform",
62
+ metadata: { guardianPrincipalId: "prin-1" },
63
+ });
64
+ });
65
+
66
+ test("telegram resolves endpoint and binding context", () => {
67
+ const list = [
68
+ guardian({
69
+ channelType: "telegram",
70
+ address: "tg-user",
71
+ externalChatId: "12345",
72
+ }),
73
+ ];
74
+ const result = resolveDestinations(["telegram"], list);
75
+ expect(result.get("telegram")).toEqual({
76
+ channel: "telegram",
77
+ endpoint: "12345",
78
+ metadata: { externalUserId: "tg-user" },
79
+ bindingContext: {
80
+ sourceChannel: "telegram",
81
+ externalChatId: "12345",
82
+ externalUserId: "tg-user",
83
+ },
84
+ });
85
+ });
86
+
87
+ test("telegram without externalChatId is omitted", () => {
88
+ const list = [guardian({ channelType: "telegram", address: "tg-user" })];
89
+ const result = resolveDestinations(["telegram"], list);
90
+ expect(result.has("telegram")).toBe(false);
91
+ });
92
+
93
+ test("slack resolves DM endpoint and binding context", () => {
94
+ const list = [
95
+ guardian({
96
+ channelType: "slack",
97
+ address: "slack-user",
98
+ externalChatId: "D123",
99
+ }),
100
+ ];
101
+ const result = resolveDestinations(["slack"], list);
102
+ expect(result.get("slack")).toEqual({
103
+ channel: "slack",
104
+ endpoint: "D123",
105
+ metadata: { externalUserId: "slack-user" },
106
+ bindingContext: {
107
+ sourceChannel: "slack",
108
+ externalChatId: "D123",
109
+ externalUserId: "slack-user",
110
+ },
111
+ });
112
+ });
113
+
114
+ test("slack non-DM channel is dropped", () => {
115
+ const list = [
116
+ guardian({
117
+ channelType: "slack",
118
+ address: "slack-user",
119
+ externalChatId: "C123",
120
+ }),
121
+ ];
122
+ const result = resolveDestinations(["slack"], list);
123
+ expect(result.has("slack")).toBe(false);
124
+ });
125
+
126
+ test("inactive guardian is ignored", () => {
127
+ const list = [
128
+ guardian({
129
+ channelType: "telegram",
130
+ address: "tg-user",
131
+ externalChatId: "12345",
132
+ status: "revoked",
133
+ }),
134
+ ];
135
+ const result = resolveDestinations(["telegram"], list);
136
+ expect(result.has("telegram")).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("resolveDestinations — null list falls back to local read", () => {
141
+ beforeEach(() => {
142
+ localGuardian = null;
143
+ });
144
+
145
+ test("telegram resolves from the local contacts read", () => {
146
+ localGuardian = {
147
+ contact: {},
148
+ channel: { address: "tg-user", externalChatId: "12345" },
149
+ };
150
+ const result = resolveDestinations(["telegram"], null);
151
+ expect(result.get("telegram")).toEqual({
152
+ channel: "telegram",
153
+ endpoint: "12345",
154
+ metadata: { externalUserId: "tg-user" },
155
+ bindingContext: {
156
+ sourceChannel: "telegram",
157
+ externalChatId: "12345",
158
+ externalUserId: "tg-user",
159
+ },
160
+ });
161
+ });
162
+
163
+ test("slack DM resolves from the local contacts read", () => {
164
+ localGuardian = {
165
+ contact: {},
166
+ channel: { address: "slack-user", externalChatId: "D123" },
167
+ };
168
+ const result = resolveDestinations(["slack"], null);
169
+ expect(result.get("slack")).toEqual({
170
+ channel: "slack",
171
+ endpoint: "D123",
172
+ metadata: { externalUserId: "slack-user" },
173
+ bindingContext: {
174
+ sourceChannel: "slack",
175
+ externalChatId: "D123",
176
+ externalUserId: "slack-user",
177
+ },
178
+ });
179
+ });
180
+
181
+ test("vellum carries principalId from the local contacts read", () => {
182
+ localGuardian = {
183
+ contact: { principalId: "prin-1" },
184
+ channel: { address: "user@example.com" },
185
+ };
186
+ const result = resolveDestinations(["vellum"], null);
187
+ expect(result.get("vellum")).toEqual({
188
+ channel: "vellum",
189
+ metadata: { guardianPrincipalId: "prin-1" },
190
+ });
191
+ });
192
+ });
193
+
194
+ describe("resolveDestinations — gateway yields no channel match falls back to local", () => {
195
+ beforeEach(() => {
196
+ localGuardian = null;
197
+ });
198
+
199
+ test("empty gateway list falls back to local telegram binding", () => {
200
+ localGuardian = {
201
+ contact: {},
202
+ channel: { address: "tg-user", externalChatId: "12345" },
203
+ };
204
+ const result = resolveDestinations(["telegram"], []);
205
+ expect(result.get("telegram")).toEqual({
206
+ channel: "telegram",
207
+ endpoint: "12345",
208
+ metadata: { externalUserId: "tg-user" },
209
+ bindingContext: {
210
+ sourceChannel: "telegram",
211
+ externalChatId: "12345",
212
+ externalUserId: "tg-user",
213
+ },
214
+ });
215
+ });
216
+
217
+ test("gateway list missing the channel falls back to local slack DM", () => {
218
+ localGuardian = {
219
+ contact: {},
220
+ channel: { address: "slack-user", externalChatId: "D123" },
221
+ };
222
+ // Gateway returns a telegram guardian but no slack entry.
223
+ const list = [
224
+ guardian({ channelType: "telegram", address: "tg", externalChatId: "999" }),
225
+ ];
226
+ const result = resolveDestinations(["slack"], list);
227
+ expect(result.get("slack")).toEqual({
228
+ channel: "slack",
229
+ endpoint: "D123",
230
+ metadata: { externalUserId: "slack-user" },
231
+ bindingContext: {
232
+ sourceChannel: "slack",
233
+ externalChatId: "D123",
234
+ externalUserId: "slack-user",
235
+ },
236
+ });
237
+ });
238
+
239
+ test("empty gateway list falls back to local vellum principalId", () => {
240
+ localGuardian = {
241
+ contact: { principalId: "prin-1" },
242
+ channel: { address: "user@example.com" },
243
+ };
244
+ const result = resolveDestinations(["vellum"], []);
245
+ expect(result.get("vellum")).toEqual({
246
+ channel: "vellum",
247
+ metadata: { guardianPrincipalId: "prin-1" },
248
+ });
249
+ });
250
+
251
+ test("empty gateway list with no local binding omits telegram", () => {
252
+ localGuardian = null;
253
+ const result = resolveDestinations(["telegram"], []);
254
+ expect(result.has("telegram")).toBe(false);
255
+ });
256
+ });
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { v4 as uuid } from "uuid";
13
13
 
14
+ import { getGuardianDelivery } from "../contacts/guardian-delivery-reader.js";
14
15
  import { getConversation } from "../memory/conversation-crud.js";
15
16
  import type { ApprovalUIMetadata } from "../runtime/channel-approval-types.js";
16
17
  import { getLogger } from "../util/logger.js";
@@ -171,7 +172,13 @@ export class NotificationBroadcaster {
171
172
  decision: NotificationDecision,
172
173
  options?: BroadcastDecisionOptions,
173
174
  ): Promise<NotificationDeliveryResult[]> {
174
- const destinations = resolveDestinations(decision.selectedChannels);
175
+ // Pull the guardian list once so the resolver stays pure. A null list
176
+ // (gateway unreachable) falls back to the local contacts read.
177
+ const guardians = await getGuardianDelivery();
178
+ const destinations = resolveDestinations(
179
+ decision.selectedChannels,
180
+ guardians,
181
+ );
175
182
 
176
183
  // Ensure vellum is processed first so the notification_conversation_created
177
184
  // event fires immediately, before slower channel sends (e.g. Telegram 30s
@@ -12,7 +12,11 @@
12
12
  import { v4 as uuid } from "uuid";
13
13
 
14
14
  import { getDeliverableChannels } from "../channels/config.js";
15
- import { listGuardianChannels } from "../contacts/contact-store.js";
15
+ import { findContactInfoById } from "../contacts/contact-store.js";
16
+ import {
17
+ anyGuardian,
18
+ getGuardianDelivery,
19
+ } from "../contacts/guardian-delivery-reader.js";
16
20
  import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
17
21
  import {
18
22
  createTimeout,
@@ -963,15 +967,19 @@ async function classifyWithLLM(
963
967
  ? truncate(rawIdentityContext, MAX_IDENTITY_CONTEXT_CHARS, "\n…[truncated]")
964
968
  : undefined;
965
969
 
966
- // Resolve guardian contact notes for recipient context. Use the channel-
967
- // agnostic guardian lookup so notes are available even when the only
968
- // deliverable channel is "vellum" (which has no contact channel type).
970
+ // Resolve guardian contact notes for recipient context. The guardian's
971
+ // identity (ACL) comes from the gateway pull, channel-agnostic so notes are
972
+ // available even when the only deliverable channel is "vellum". Notes (INFO)
973
+ // stay local and are joined by contactId.
969
974
  let recipientNotes: string | undefined;
970
975
  try {
971
- const guardianResult = listGuardianChannels();
972
- if (guardianResult?.contact.notes) {
976
+ const guardian = anyGuardian((await getGuardianDelivery()) ?? []);
977
+ const notes = guardian
978
+ ? findContactInfoById(guardian.contactId)?.notes
979
+ : undefined;
980
+ if (notes) {
973
981
  recipientNotes = truncate(
974
- guardianResult.contact.notes,
982
+ notes,
975
983
  MAX_IDENTITY_CONTEXT_CHARS,
976
984
  "\n…[truncated]",
977
985
  );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Resolves per-channel destination endpoints for notification delivery.
3
3
  *
4
- * Reads guardian delivery info from the contacts table.
4
+ * Reads guardian delivery info from the gateway-backed guardian list.
5
5
  *
6
6
  * - Vellum: no external endpoint needed — delivery goes through the event
7
7
  * broadcast mechanism to connected desktop/mobile clients. The
@@ -11,14 +11,54 @@
11
11
  * sourced from the guardian contact's channel record.
12
12
  */
13
13
 
14
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
15
+
14
16
  import { isNotificationDeliverable } from "../channels/config.js";
15
17
  import type { ChannelId } from "../channels/types.js";
16
18
  import { findGuardianForChannel } from "../contacts/contact-store.js";
19
+ import { guardianForChannel } from "../contacts/guardian-delivery-reader.js";
17
20
  import { getLogger } from "../util/logger.js";
18
21
  import type { ChannelDestination, NotificationChannel } from "./types.js";
19
22
 
20
23
  const log = getLogger("destination-resolver");
21
24
 
25
+ /** Guardian delivery endpoint for a channel, flattened from either source. */
26
+ interface ResolvedGuardian {
27
+ principalId?: string;
28
+ address: string;
29
+ externalChatId?: string;
30
+ }
31
+
32
+ /**
33
+ * Resolve the guardian delivery endpoint for a channel: gateway list first,
34
+ * else the local contacts read. The local read is the transitional
35
+ * dual-written mirror and covers a transient gateway failure (null list) or a
36
+ * gateway list missing this channel, so a soft-failed gateway read does not
37
+ * drop a binding the local store still holds. Removed in Combo 11.
38
+ */
39
+ function resolveGuardian(
40
+ guardians: GuardianDelivery[] | null,
41
+ channelType: string,
42
+ ): ResolvedGuardian | undefined {
43
+ const g = guardians
44
+ ? guardianForChannel(guardians, channelType)
45
+ : undefined;
46
+ if (g) {
47
+ return {
48
+ principalId: g.principalId ?? undefined,
49
+ address: g.address,
50
+ externalChatId: g.externalChatId ?? undefined,
51
+ };
52
+ }
53
+ const local = findGuardianForChannel(channelType);
54
+ if (!local) return undefined;
55
+ return {
56
+ principalId: local.contact.principalId ?? undefined,
57
+ address: local.channel.address,
58
+ externalChatId: local.channel.externalChatId ?? undefined,
59
+ };
60
+ }
61
+
22
62
  /**
23
63
  * Resolve destination information for each requested channel.
24
64
  *
@@ -26,9 +66,14 @@ const log = getLogger("destination-resolver");
26
66
  * the function skips non-deliverable channels via `isNotificationDeliverable`.
27
67
  * Returns a map keyed by `NotificationChannel`. Channels that cannot be
28
68
  * resolved (e.g. no Telegram binding configured) are omitted from the result.
69
+ *
70
+ * `guardians` is the gateway-resolved guardian list; per channel, a missing
71
+ * match (null list or no entry for the channel) falls back to the local
72
+ * contacts read for this release.
29
73
  */
30
74
  export function resolveDestinations(
31
75
  channels: readonly (ChannelId | NotificationChannel)[],
76
+ guardians: GuardianDelivery[] | null,
32
77
  ): Map<NotificationChannel, ChannelDestination> {
33
78
  const result = new Map<NotificationChannel, ChannelDestination>();
34
79
 
@@ -43,10 +88,10 @@ export function resolveDestinations(
43
88
  // Vellum delivery is local — no external endpoint required.
44
89
  // Include the guardianPrincipalId so the adapter can annotate
45
90
  // guardian-sensitive notifications for scoped delivery.
46
- const guardianResult = findGuardianForChannel("vellum");
91
+ const guardian = resolveGuardian(guardians, "vellum");
47
92
  const metadata: Record<string, unknown> = {};
48
- if (guardianResult) {
49
- metadata.guardianPrincipalId = guardianResult.contact.principalId;
93
+ if (guardian?.principalId) {
94
+ metadata.guardianPrincipalId = guardian.principalId;
50
95
  }
51
96
  result.set("vellum", {
52
97
  channel: "vellum",
@@ -55,7 +100,7 @@ export function resolveDestinations(
55
100
  log.debug(
56
101
  {
57
102
  channel: "vellum",
58
- source: "contacts",
103
+ source: "guardian-delivery",
59
104
  hasEndpoint: false,
60
105
  },
61
106
  "destination resolved",
@@ -63,52 +108,52 @@ export function resolveDestinations(
63
108
  break;
64
109
  }
65
110
  case "telegram": {
66
- const guardianResult = findGuardianForChannel(channel);
67
- if (guardianResult && guardianResult.channel.externalChatId) {
68
- const externalChatId = guardianResult.channel.externalChatId;
111
+ const guardian = resolveGuardian(guardians, channel);
112
+ if (guardian?.externalChatId) {
113
+ const externalChatId = guardian.externalChatId;
69
114
  result.set(channel as NotificationChannel, {
70
115
  channel: channel as NotificationChannel,
71
116
  endpoint: externalChatId,
72
117
  metadata: {
73
- externalUserId: guardianResult.channel.address,
118
+ externalUserId: guardian.address,
74
119
  },
75
120
  bindingContext: {
76
121
  sourceChannel: channel as NotificationChannel,
77
122
  externalChatId,
78
- externalUserId: guardianResult.channel.address,
123
+ externalUserId: guardian.address,
79
124
  },
80
125
  });
81
126
  }
82
127
  log.debug(
83
128
  {
84
129
  channel,
85
- source: "contacts",
86
- hasEndpoint: !!guardianResult?.channel.externalChatId,
130
+ source: "guardian-delivery",
131
+ hasEndpoint: !!guardian?.externalChatId,
87
132
  },
88
133
  "destination resolved",
89
134
  );
90
135
  break;
91
136
  }
92
137
  case "slack": {
93
- const guardianResult = findGuardianForChannel("slack");
94
- const chatId = guardianResult?.channel.externalChatId;
138
+ const guardian = resolveGuardian(guardians, "slack");
139
+ const chatId = guardian?.externalChatId;
95
140
  // Slack bindings can originate from app_mention in shared channels.
96
141
  // Only route notifications to DM channels (IDs starting with "D")
97
142
  // to prevent leaking notifications into shared workspaces.
98
- if (guardianResult && chatId && isSlackDmChannel(chatId)) {
143
+ if (guardian && chatId && isSlackDmChannel(chatId)) {
99
144
  result.set("slack", {
100
145
  channel: "slack",
101
146
  endpoint: chatId,
102
147
  metadata: {
103
- externalUserId: guardianResult.channel.address,
148
+ externalUserId: guardian.address,
104
149
  },
105
150
  bindingContext: {
106
151
  sourceChannel: "slack",
107
152
  externalChatId: chatId,
108
- externalUserId: guardianResult.channel.address,
153
+ externalUserId: guardian.address,
109
154
  },
110
155
  });
111
- } else if (guardianResult && chatId) {
156
+ } else if (guardian && chatId) {
112
157
  log.warn(
113
158
  { channel: "slack", chatId },
114
159
  "skipping non-DM Slack channel for notification delivery",
@@ -117,7 +162,7 @@ export function resolveDestinations(
117
162
  log.debug(
118
163
  {
119
164
  channel: "slack",
120
- source: "contacts",
165
+ source: "guardian-delivery",
121
166
  hasEndpoint: !!(chatId && isSlackDmChannel(chatId)),
122
167
  },
123
168
  "destination resolved",
@@ -128,11 +173,10 @@ export function resolveDestinations(
128
173
  // Platform delivery goes through the daemon's VellumPlatformClient —
129
174
  // no external binding needed. Include guardianPrincipalId so the
130
175
  // adapter can scope guardian-sensitive notifications.
131
- const platformGuardian = findGuardianForChannel("vellum");
176
+ const platformGuardian = resolveGuardian(guardians, "vellum");
132
177
  const platformMeta: Record<string, unknown> = {};
133
- if (platformGuardian) {
134
- platformMeta.guardianPrincipalId =
135
- platformGuardian.contact.principalId;
178
+ if (platformGuardian?.principalId) {
179
+ platformMeta.guardianPrincipalId = platformGuardian.principalId;
136
180
  }
137
181
  result.set("platform", {
138
182
  channel: "platform",
@@ -140,7 +184,7 @@ export function resolveDestinations(
140
184
  Object.keys(platformMeta).length > 0 ? platformMeta : undefined,
141
185
  });
142
186
  log.debug(
143
- { channel: "platform", source: "contacts", hasEndpoint: false },
187
+ { channel: "platform", source: "guardian-delivery", hasEndpoint: false },
144
188
  "destination resolved",
145
189
  );
146
190
  break;
@@ -9,10 +9,15 @@
9
9
  * propagated unless `throwOnError` is enabled.
10
10
  */
11
11
 
12
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
12
13
  import { v4 as uuid } from "uuid";
13
14
 
14
15
  import { getDeliverableChannels } from "../channels/config.js";
15
16
  import { findGuardianForChannel } from "../contacts/contact-store.js";
17
+ import {
18
+ getGuardianDelivery,
19
+ guardianForChannel,
20
+ } from "../contacts/guardian-delivery-reader.js";
16
21
  import type { ConversationCreateType } from "../memory/conversation-crud.js";
17
22
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
18
23
  import { getLogger } from "../util/logger.js";
@@ -89,9 +94,33 @@ export function getBroadcaster(): NotificationBroadcaster {
89
94
 
90
95
  // ── Connected channels resolution ──────────────────────────────────────
91
96
 
92
- function getConnectedChannels(): NotificationChannel[] {
97
+ /**
98
+ * Resolve a binding-based channel's delivery endpoint (externalChatId) the
99
+ * SAME way destination-resolver's `resolveGuardian` does: gateway match first,
100
+ * falling back to the LOCAL contacts read on ANY per-channel no-match — gateway
101
+ * list null (unreachable) OR no active gateway entry for this channel. The
102
+ * local read is the transitional dual-written mirror, removed in Combo 11.
103
+ * Keeping connectivity aligned with delivery prevents a channel being marked
104
+ * connected but then skipped with no destination (or vice-versa).
105
+ */
106
+ function resolveChannelChatId(
107
+ guardians: GuardianDelivery[] | null,
108
+ channelType: string,
109
+ ): string | undefined {
110
+ const g = guardians ? guardianForChannel(guardians, channelType) : undefined;
111
+ if (g) {
112
+ return g.externalChatId ?? undefined;
113
+ }
114
+ return findGuardianForChannel(channelType)?.channel.externalChatId ?? undefined;
115
+ }
116
+
117
+ export async function getConnectedChannels(): Promise<NotificationChannel[]> {
93
118
  const channels: NotificationChannel[] = [];
94
119
 
120
+ // Guardian bindings (ACL) come from the gateway pull; null ⇒ gateway
121
+ // unreachable, so binding-based connectivity falls back to the local read.
122
+ const guardians = await getGuardianDelivery();
123
+
95
124
  // getDeliverableChannels() returns ChannelId[] but every returned channel
96
125
  // has deliveryEnabled: true, making it a valid NotificationChannel at
97
126
  // runtime. We iterate over the broad type and narrow via the switch.
@@ -110,24 +139,20 @@ function getConnectedChannels(): NotificationChannel[] {
110
139
  channels.push(channel);
111
140
  break;
112
141
  case "telegram": {
113
- // A binding-based channel is connected when the guardian has an
114
- // active channel entry with a valid delivery endpoint. The
115
- // externalChatId check ensures we don't report a channel as
116
- // connected when the contacts record exists but lacks the
117
- // delivery address the destination-resolver needs.
118
- const guardian = findGuardianForChannel(channel);
119
- if (guardian && guardian.channel.externalChatId) {
142
+ // Connected when the resolved guardian has a delivery endpoint —
143
+ // mirroring destination-resolver so we never mark connected what
144
+ // can't be delivered.
145
+ if (resolveChannelChatId(guardians, channel)) {
120
146
  channels.push(channel);
121
147
  }
122
148
  break;
123
149
  }
124
150
  case "slack": {
125
151
  // Slack bindings can originate from shared channels (app_mention).
126
- // Only consider Slack connected when the stored chat ID is a DM
127
- // channel (D-prefixed) to prevent leaking notifications.
128
- const slackGuardian = findGuardianForChannel("slack");
129
- const chatId = slackGuardian?.channel.externalChatId;
130
- if (slackGuardian && chatId && chatId.startsWith("D")) {
152
+ // Only consider Slack connected when the resolved chat ID is a DM
153
+ // channel (D-prefixed), matching destination-resolver's DM gate.
154
+ const chatId = resolveChannelChatId(guardians, "slack");
155
+ if (chatId && chatId.startsWith("D")) {
131
156
  channels.push(channel);
132
157
  }
133
158
  break;
@@ -290,7 +315,7 @@ export async function emitNotificationSignal<TEventName extends string>(
290
315
  }
291
316
 
292
317
  // Step 2: Evaluate the signal through the decision engine
293
- const connectedChannels = getConnectedChannels();
318
+ const connectedChannels = await getConnectedChannels();
294
319
 
295
320
  log.debug(
296
321
  {