@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
@@ -33,6 +33,7 @@
33
33
 
34
34
  import * as Sentry from "@sentry/node";
35
35
 
36
+ import { recordWatchdogEvent } from "../memory/watchdog-events-store.js";
36
37
  import { getLogger } from "../util/logger.js";
37
38
 
38
39
  const log = getLogger("event-loop-watchdog");
@@ -74,11 +75,37 @@ export function evaluateTick(
74
75
  return { blockedMs, exceeded: blockedMs >= thresholdMs };
75
76
  }
76
77
 
78
+ /**
79
+ * Check name emitted for event-loop block events. The platform's
80
+ * `watchdog__event_loop_blocking_daily` admin query filters `check_name` to
81
+ * this exact string, so it is the primary group-by dimension downstream —
82
+ * keep it stable.
83
+ */
84
+ export const EVENT_LOOP_BLOCKED_CHECK_NAME = "event_loop_blocked";
85
+
77
86
  function reportBlock(blockedMs: number, thresholdMs: number): void {
78
87
  log.warn(
79
88
  { blockedMs, thresholdMs, tickIntervalMs: TICK_INTERVAL_MS },
80
89
  "event loop blocked",
81
90
  );
91
+ // Persist a `watchdog` telemetry event so the platform can surface
92
+ // event-loop blocking in the infrastructure admin chart. `recordWatchdogEvent`
93
+ // no-ops when usage-data collection is disabled (the event is dropped to
94
+ // honor the opt-out), so the watchdog runs unconditionally without leaking
95
+ // health data for opted-out owners. Never let a telemetry failure escape
96
+ // the timer callback — wrap it alongside the Sentry capture below.
97
+ try {
98
+ recordWatchdogEvent({
99
+ checkName: EVENT_LOOP_BLOCKED_CHECK_NAME,
100
+ value: blockedMs,
101
+ detail: {
102
+ threshold_ms: thresholdMs,
103
+ tick_interval_ms: TICK_INTERVAL_MS,
104
+ },
105
+ });
106
+ } catch {
107
+ // Never let a telemetry failure escape the timer callback.
108
+ }
82
109
  try {
83
110
  Sentry.withScope((scope) => {
84
111
  scope.setLevel("warning");
@@ -88,7 +115,7 @@ function reportBlock(blockedMs: number, thresholdMs: number): void {
88
115
  threshold_ms: thresholdMs,
89
116
  tick_interval_ms: TICK_INTERVAL_MS,
90
117
  });
91
- Sentry.captureMessage("event_loop_blocked");
118
+ Sentry.captureMessage(EVENT_LOOP_BLOCKED_CHECK_NAME);
92
119
  });
93
120
  } catch {
94
121
  // Never let a telemetry failure escape the timer callback.
@@ -19,24 +19,20 @@
19
19
  * dropped from the registry via {@link unregisterPlugin} so none of its
20
20
  * hooks participate in the turn lifecycle. This is the primary mechanism for
21
21
  * shipping experimental plugins behind a feature flag.
22
- * 4. Resolves the plugin's `manifest.requiresCredential` entries via the
23
- * credential store helper ({@link getSecureKeyAsync}). In Docker mode
24
- * that helper goes through the CES HTTP API transparently; in local mode
25
- * it hits the encrypted file store / CES RPC backend.
26
- * 5. Validates the config block under `plugins.<name>` against
22
+ * 4. Validates the config block under `plugins.<name>` against
27
23
  * `manifest.config` if the manifest supplies a parser-like validator
28
24
  * (Zod schemas with `.parse()` are supported; anything else is passed
29
25
  * through untouched).
30
- * 6. Creates `<workspaceDir>/plugins-data/<plugin>/` on demand for per-plugin
26
+ * 5. Creates `<workspaceDir>/plugins-data/<plugin>/` on demand for per-plugin
31
27
  * writable state and exposes it via {@link PluginInitContext.pluginStorageDir}.
32
- * 7. For each surviving plugin, registers its contributed tools and routes
28
+ * 6. For each surviving plugin, registers its contributed tools and routes
33
29
  * into their global registries via {@link registerPluginTools} and
34
30
  * {@link registerSkillRoute}. Contributions land BEFORE `init()` so
35
31
  * the plugin's hook can observe a registry where its own model-visible
36
32
  * surface is already wired — useful for plugins that want to attach
37
33
  * metadata, warm caches, or otherwise interact with their own
38
34
  * contributions during initialization.
39
- * 8. Awaits `plugin.init(ctx)` sequentially. An init failure is contained to
35
+ * 7. Awaits `plugin.init(ctx)` sequentially. An init failure is contained to
40
36
  * the offending plugin: its already-registered tools and routes are rolled
41
37
  * back, it is dropped from the registry, the failure is logged, and
42
38
  * bootstrap continues with the remaining plugins. A single plugin's failure
@@ -76,7 +72,6 @@ import {
76
72
  type SkillRouteHandle,
77
73
  unregisterSkillRoute,
78
74
  } from "../runtime/skill-route-registry.js";
79
- import { getSecureKeyAsync } from "../security/secure-keys.js";
80
75
  import {
81
76
  registerPluginTools,
82
77
  unregisterPluginTools,
@@ -88,25 +83,6 @@ import { registerShutdownHook } from "./shutdown-registry.js";
88
83
 
89
84
  const log = getLogger("plugins-bootstrap");
90
85
 
91
- /**
92
- * Resolve one credential value. Returns the raw secret string or throws a
93
- * {@link PluginExecutionError} tagged with the plugin name so the caller can
94
- * fail startup with clear attribution.
95
- */
96
- async function resolveCredentialOrThrow(
97
- pluginName: string,
98
- credentialKey: string,
99
- ): Promise<string> {
100
- const value = await getSecureKeyAsync(credentialKey);
101
- if (value === undefined || value === "") {
102
- throw new PluginExecutionError(
103
- `plugin ${pluginName} requires credential "${credentialKey}" but the credential store returned no value`,
104
- pluginName,
105
- );
106
- }
107
- return value;
108
- }
109
-
110
86
  /**
111
87
  * Validate a plugin config block. If the manifest supplies a parser-like
112
88
  * validator (Zod schemas expose `.parse()`), use it. Otherwise pass the
@@ -346,11 +322,6 @@ async function initializePlugin(
346
322
  let initCompleted = false;
347
323
 
348
324
  try {
349
- const credentials: Record<string, string> = {};
350
- for (const key of plugin.manifest.requiresCredential ?? []) {
351
- credentials[key] = await resolveCredentialOrThrow(name, key);
352
- }
353
-
354
325
  const config = validatePluginConfig(
355
326
  name,
356
327
  plugin.manifest.config,
@@ -359,7 +330,6 @@ async function initializePlugin(
359
330
 
360
331
  const initContext = {
361
332
  config,
362
- credentials,
363
333
  logger: log.child({ plugin: name }),
364
334
  pluginStorageDir: ensurePluginStorageDir(name),
365
335
  assistantVersion: APP_VERSION,
@@ -129,4 +129,29 @@ describe("redeemA2AInvite", () => {
129
129
  const result = redeemA2AInvite({ sender: SENDER });
130
130
  expect(result.success).toBe(true);
131
131
  });
132
+
133
+ test("activeConnections counts a2a channel existence, not status", () => {
134
+ const result = redeemA2AInvite({ sender: SENDER });
135
+ expect(result.success).toBe(true);
136
+
137
+ expect(getA2AConfig().activeConnections).toBe(1);
138
+
139
+ // Readiness is existence-based: a non-active stored status still counts.
140
+ const sqlite = getSqlite();
141
+ sqlite.run("UPDATE contact_channels SET status = 'unverified'");
142
+ invalidateConfigCache();
143
+ expect(getA2AConfig().activeConnections).toBe(1);
144
+ });
145
+
146
+ test("already-connected guard fires regardless of stored channel status", () => {
147
+ const first = redeemA2AInvite({ sender: SENDER });
148
+ expect(first.success).toBe(true);
149
+
150
+ const sqlite = getSqlite();
151
+ sqlite.run("UPDATE contact_channels SET status = 'revoked'");
152
+
153
+ const second = redeemA2AInvite({ sender: SENDER });
154
+ expect(second.alreadyConnected).toBe(true);
155
+ expect(second.contactId).toBe(first.contactId);
156
+ });
132
157
  });
@@ -0,0 +1,225 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
4
+
5
+ import type { ContactChannel } from "../../../contacts/types.js";
6
+
7
+ let mockGuardians: GuardianDelivery[] | null = null;
8
+ let mockBinding: { guardianExternalUserId: string; guardianDeliveryChatId: string } | null = null;
9
+ let mockContactChannel: { channel: ContactChannel } | null = null;
10
+ let mockChannel: ContactChannel | null = null;
11
+ let mockGwContactChannels: Array<{ id: string; status: string; verifiedAt: number | null }> | null =
12
+ null;
13
+ let ipcCalls: Array<{ method: string; payload: unknown }> = [];
14
+
15
+ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
16
+ getGuardianDelivery: async (input?: { channelTypes?: string[] }) => {
17
+ if (mockGuardians == null) return null;
18
+ if (!input?.channelTypes) return mockGuardians;
19
+ return mockGuardians.filter((g) =>
20
+ input.channelTypes!.includes(g.channelType),
21
+ );
22
+ },
23
+ }));
24
+
25
+ mock.module("../../../contacts/contact-store.js", () => ({
26
+ findContactChannel: () => mockContactChannel,
27
+ findGuardianForChannel: () => null,
28
+ getChannelById: () => mockChannel,
29
+ getContact: () => ({ id: "contact-1", displayName: "Pat" }),
30
+ }));
31
+
32
+ mock.module("../../../contacts/contact-events.js", () => ({
33
+ emitContactChange: () => {},
34
+ onContactChange: () => {},
35
+ }));
36
+
37
+ mock.module("../../../ipc/gateway-client.js", () => ({
38
+ ipcCallPersistent: async (method: string, payload: unknown) => {
39
+ ipcCalls.push({ method, payload });
40
+ if (method === "contacts_get_rich") {
41
+ if (mockGwContactChannels == null) return { ok: true, contact: null };
42
+ return {
43
+ ok: true,
44
+ contact: {
45
+ id: "contact-1",
46
+ displayName: "Pat",
47
+ role: "contact",
48
+ interactionCount: 0,
49
+ createdAt: 0,
50
+ updatedAt: 0,
51
+ channels: mockGwContactChannels.map((c) => ({
52
+ id: c.id,
53
+ contactId: "contact-1",
54
+ type: "telegram",
55
+ address: "user-123",
56
+ isPrimary: true,
57
+ externalUserId: null,
58
+ status: c.status,
59
+ policy: "allow",
60
+ verifiedAt: c.verifiedAt,
61
+ verifiedVia: null,
62
+ lastSeenAt: null,
63
+ interactionCount: 0,
64
+ lastInteraction: null,
65
+ revokedReason: null,
66
+ blockedReason: null,
67
+ })),
68
+ },
69
+ };
70
+ }
71
+ return {
72
+ ok: true,
73
+ didWrite: true,
74
+ channel: {
75
+ id: "ch-1",
76
+ contactId: "contact-1",
77
+ type: "telegram",
78
+ address: "user-123",
79
+ status: "revoked",
80
+ revokedReason: "guardian_binding_revoked",
81
+ },
82
+ };
83
+ },
84
+ ipcCall: async () => null,
85
+ }));
86
+
87
+ mock.module("../../../runtime/channel-verification-service.js", () => ({
88
+ getGuardianBinding: () => mockBinding,
89
+ revokeBinding: () => true,
90
+ revokePendingSessions: () => {},
91
+ createOutboundSession: () => ({
92
+ sessionId: "sess",
93
+ secret: "code",
94
+ expiresAt: Date.now() + 1000,
95
+ }),
96
+ countRecentSendsToDestination: () => 0,
97
+ isGuardianBoundForChannel: async () => false,
98
+ updateSessionDelivery: () => {},
99
+ }));
100
+
101
+ mock.module("../../../runtime/verification-outbound-actions.js", () => ({
102
+ cancelOutbound: () => {},
103
+ deliverVerificationEmail: () => {},
104
+ deliverVerificationSlack: () => {},
105
+ deliverVerificationTelegram: () => {},
106
+ DESTINATION_RATE_WINDOW_MS: 1000,
107
+ MAX_SENDS_PER_DESTINATION_WINDOW: 5,
108
+ normalizeTelegramDestination: (d: string) => d,
109
+ resendOutbound: () => ({}),
110
+ startOutbound: async () => ({}),
111
+ }));
112
+
113
+ import {
114
+ revokeVerificationForChannel,
115
+ verifyTrustedContact,
116
+ } from "../config-channels.js";
117
+
118
+ function channel(overrides: Partial<ContactChannel> = {}): ContactChannel {
119
+ return {
120
+ id: "ch-1",
121
+ contactId: "contact-1",
122
+ type: "telegram",
123
+ address: "user-123",
124
+ isPrimary: true,
125
+ externalChatId: "chat-123",
126
+ // DB columns are intentionally a terminal state to prove the gates ignore
127
+ // them and read from the gateway delivery instead.
128
+ status: "revoked",
129
+ policy: {} as ContactChannel["policy"],
130
+ verifiedAt: null,
131
+ verifiedVia: null,
132
+ inviteId: null,
133
+ revokedReason: null,
134
+ blockedReason: null,
135
+ lastSeenAt: null,
136
+ interactionCount: 0,
137
+ lastInteraction: null,
138
+ updatedAt: null,
139
+ createdAt: 0,
140
+ ...overrides,
141
+ };
142
+ }
143
+
144
+ function delivery(overrides: Partial<GuardianDelivery> = {}): GuardianDelivery {
145
+ return {
146
+ channelType: "telegram",
147
+ contactId: "contact-1",
148
+ address: "user-123",
149
+ externalChatId: "chat-123",
150
+ status: "active",
151
+ verifiedAt: 1700000000,
152
+ ...overrides,
153
+ };
154
+ }
155
+
156
+ describe("revokeVerificationForChannel", () => {
157
+ beforeEach(() => {
158
+ mockGuardians = null;
159
+ mockBinding = { guardianExternalUserId: "user-123", guardianDeliveryChatId: "chat-123" };
160
+ mockContactChannel = { channel: channel() };
161
+ ipcCalls = [];
162
+ });
163
+
164
+ test("relays mark_channel_revoked when the gateway delivery is live", async () => {
165
+ mockGuardians = [delivery({ status: "active" })];
166
+ await revokeVerificationForChannel("telegram");
167
+ expect(ipcCalls.map((c) => c.method)).toContain("mark_channel_revoked");
168
+ });
169
+
170
+ test("skips a redundant revoke when the gateway delivery is already revoked", async () => {
171
+ // Local DB status is the live "active" here, but the gateway (SoT) says
172
+ // revoked — the gate must follow the gateway and not relay.
173
+ mockContactChannel = { channel: channel({ status: "active" }) };
174
+ mockGuardians = [delivery({ status: "revoked" })];
175
+ await revokeVerificationForChannel("telegram");
176
+ expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
177
+ });
178
+
179
+ test("skips the relay when the gateway has no delivery for the channel", async () => {
180
+ mockGuardians = [];
181
+ await revokeVerificationForChannel("telegram");
182
+ expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
183
+ });
184
+
185
+ test("skips the relay when the gateway is unreachable", async () => {
186
+ mockGuardians = null;
187
+ await revokeVerificationForChannel("telegram");
188
+ expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
189
+ });
190
+ });
191
+
192
+ describe("verifyTrustedContact already-verified gate", () => {
193
+ beforeEach(() => {
194
+ mockGuardians = null;
195
+ mockGwContactChannels = null;
196
+ mockChannel = channel();
197
+ });
198
+
199
+ test("short-circuits when the gateway contact channel is active and verified", async () => {
200
+ // Arbitrary trusted contact (non-guardian) — read from the contact-channel
201
+ // gateway read, which covers all contacts.
202
+ mockGwContactChannels = [
203
+ { id: "ch-1", status: "active", verifiedAt: 1700000000 },
204
+ ];
205
+ const result = await verifyTrustedContact("ch-1", "assistant-1");
206
+ expect(result.success).toBe(false);
207
+ expect(result.error).toBe("already_verified");
208
+ });
209
+
210
+ test("does not short-circuit when the gateway channel has no verifiedAt", async () => {
211
+ // DB column says verified, but the gateway channel is unverified — proceed.
212
+ mockChannel = channel({ status: "active", verifiedAt: 1700000000 });
213
+ mockGwContactChannels = [
214
+ { id: "ch-1", status: "pending", verifiedAt: null },
215
+ ];
216
+ const result = await verifyTrustedContact("ch-1", "assistant-1");
217
+ expect(result.error).not.toBe("already_verified");
218
+ });
219
+
220
+ test("does not short-circuit when the gateway has no matching channel", async () => {
221
+ mockGwContactChannels = [];
222
+ const result = await verifyTrustedContact("ch-1", "assistant-1");
223
+ expect(result.error).not.toBe("already_verified");
224
+ });
225
+ });
@@ -96,13 +96,11 @@ export function getA2AConfig(): A2AConfigResult {
96
96
  const config = getConfig();
97
97
  const enabled = config.a2a?.enabled ?? false;
98
98
 
99
+ // a2a is peer binding outside the human-trust ACL model — the gateway has no
100
+ // canonical a2a channel status, so channel existence is the readiness signal.
99
101
  const contacts = searchContacts({ channelType: "a2a" });
100
102
  const activeConnections = contacts.reduce((count, c) => {
101
- return (
102
- count +
103
- c.channels.filter((ch) => ch.type === "a2a" && ch.status === "active")
104
- .length
105
- );
103
+ return count + c.channels.filter((ch) => ch.type === "a2a").length;
106
104
  }, 0);
107
105
 
108
106
  return { success: true, enabled, activeConnections };
@@ -270,12 +268,9 @@ export function redeemA2AInvite(params: {
270
268
  setA2AConfig();
271
269
  }
272
270
 
273
- // 2. Check for existing active contact with this sender
271
+ // 2. Check for existing contact with this sender (a2a binding existence)
274
272
  const existing = findContactByAddress("a2a", params.sender.assistantId);
275
- if (
276
- existing &&
277
- existing.channels.some((ch) => ch.type === "a2a" && ch.status === "active")
278
- ) {
273
+ if (existing && existing.channels.some((ch) => ch.type === "a2a")) {
279
274
  return { success: true, alreadyConnected: true, contactId: existing.id };
280
275
  }
281
276
 
@@ -373,10 +368,7 @@ export async function acceptA2AInvite(params: {
373
368
  // 2. Short-circuit if already connected — avoids a network round-trip
374
369
  // and consuming a token on the sender side.
375
370
  const existing = findContactByAddress("a2a", params.senderAssistantId);
376
- if (
377
- existing &&
378
- existing.channels.some((ch) => ch.type === "a2a" && ch.status === "active")
379
- ) {
371
+ if (existing && existing.channels.some((ch) => ch.type === "a2a")) {
380
372
  return { success: true, alreadyConnected: true, contactId: existing.id };
381
373
  }
382
374
 
@@ -1,6 +1,10 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
 
3
- import { MarkChannelRevokedIpcResponseSchema } from "@vellumai/gateway-client/gateway-ipc-contracts";
3
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
4
+ import {
5
+ GetContactIpcResponseSchema,
6
+ MarkChannelRevokedIpcResponseSchema,
7
+ } from "@vellumai/gateway-client/gateway-ipc-contracts";
4
8
 
5
9
  import { startVerificationCall } from "../../calls/call-domain.js";
6
10
  import type { ChannelId } from "../../channels/types.js";
@@ -11,7 +15,8 @@ import {
11
15
  getChannelById,
12
16
  getContact,
13
17
  } from "../../contacts/contact-store.js";
14
- import type { ChannelStatus } from "../../contacts/types.js";
18
+ import { getGuardianDelivery } from "../../contacts/guardian-delivery-reader.js";
19
+ import type { ContactChannel } from "../../contacts/types.js";
15
20
  import { ipcCallPersistent } from "../../ipc/gateway-client.js";
16
21
  import { getBindingByChannelChat } from "../../memory/external-conversation-store.js";
17
22
  import { resolveGuardianName } from "../../prompts/user-reference.js";
@@ -28,6 +33,7 @@ import {
28
33
  findActiveSession,
29
34
  getGuardianBinding,
30
35
  getPendingSession,
36
+ isGuardianBoundForChannel,
31
37
  revokeBinding,
32
38
  revokePendingSessions,
33
39
  updateSessionDelivery,
@@ -76,23 +82,64 @@ export function getReadinessService(): ChannelReadinessService {
76
82
  return _readinessService;
77
83
  }
78
84
 
85
+ // ---------------------------------------------------------------------------
86
+ // Gateway delivery lookup
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Resolve the gateway-owned delivery (ACL source of truth) for a contact
91
+ * channel, matching on type and either address or externalChatId. Returns
92
+ * `undefined` when the gateway is unreachable or has no binding for it.
93
+ */
94
+ async function deliveryForChannel(
95
+ channel: Pick<ContactChannel, "type" | "address" | "externalChatId">,
96
+ ): Promise<GuardianDelivery | undefined> {
97
+ const guardians = await getGuardianDelivery({ channelTypes: [channel.type] });
98
+ if (!guardians) return undefined;
99
+ return guardians.find(
100
+ (g) =>
101
+ g.channelType === channel.type &&
102
+ ((channel.address && g.address === channel.address) ||
103
+ (channel.externalChatId != null &&
104
+ g.externalChatId === channel.externalChatId)),
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Read a contact channel's verified state from the gateway contact-channel read
110
+ * (ACL source of truth). Covers all contacts, not just guardian deliveries.
111
+ * Returns `undefined` when the gateway is unreachable or has no such channel.
112
+ */
113
+ async function gatewayContactChannelState(
114
+ channel: Pick<ContactChannel, "id" | "contactId">,
115
+ ): Promise<{ status: string; verifiedAt: number | null } | undefined> {
116
+ const result = await ipcCallPersistent("contacts_get_rich", {
117
+ contactId: channel.contactId,
118
+ });
119
+ if (!result || (result as { contact?: unknown }).contact == null) {
120
+ return undefined;
121
+ }
122
+ const { contact } = GetContactIpcResponseSchema.parse(result);
123
+ const ch = contact.channels.find((c) => c.id === channel.id);
124
+ return ch ? { status: ch.status, verifiedAt: ch.verifiedAt } : undefined;
125
+ }
126
+
79
127
  // ---------------------------------------------------------------------------
80
128
  // Extracted business logic functions
81
129
  // ---------------------------------------------------------------------------
82
130
 
83
- export function createInboundChallenge(
131
+ export async function createInboundChallenge(
84
132
  channel?: ChannelId,
85
133
  rebind?: boolean,
86
134
  conversationId?: string,
87
- ): ChannelVerificationSessionResult {
88
- const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
135
+ ): Promise<ChannelVerificationSessionResult> {
89
136
  const resolvedChannel = channel ?? "telegram";
90
137
 
91
- const existingBinding = getGuardianBinding(
92
- resolvedAssistantId,
93
- resolvedChannel,
94
- );
95
- if (existingBinding && !rebind) {
138
+ // Gateway-backed presence guard: block re-binding when a guardian is already
139
+ // bound. Null-list (gateway unreachable) is treated as bound, so a transient
140
+ // miss blocks rather than letting a second binding through.
141
+ const alreadyBound = await isGuardianBoundForChannel(resolvedChannel);
142
+ if (alreadyBound && !rebind) {
96
143
  return {
97
144
  success: false,
98
145
  error: "already_bound",
@@ -115,13 +162,13 @@ export function createInboundChallenge(
115
162
  };
116
163
  }
117
164
 
118
- export function getVerificationStatus(
165
+ export async function getVerificationStatus(
119
166
  channel?: ChannelId,
120
- ): ChannelVerificationSessionResult {
167
+ ): Promise<ChannelVerificationSessionResult> {
121
168
  const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
122
169
  const resolvedChannel = channel ?? "telegram";
123
170
 
124
- const binding = getGuardianBinding(resolvedAssistantId, resolvedChannel);
171
+ const binding = await getGuardianBinding(resolvedAssistantId, resolvedChannel);
125
172
 
126
173
  // Read the contact directly to get displayName — getGuardianBinding is a
127
174
  // compatibility shim that doesn't carry metadataJson.
@@ -189,7 +236,10 @@ export async function revokeVerificationForChannel(
189
236
 
190
237
  // Capture binding before revoking so we can downgrade the guardian's
191
238
  // channel — without this, the guardian would still pass the ACL check.
192
- const bindingBeforeRevoke = getGuardianBinding(assistantId, resolvedChannel);
239
+ const bindingBeforeRevoke = await getGuardianBinding(
240
+ assistantId,
241
+ resolvedChannel,
242
+ );
193
243
  if (!bindingBeforeRevoke) {
194
244
  return {
195
245
  success: true,
@@ -206,13 +256,16 @@ export async function revokeVerificationForChannel(
206
256
 
207
257
  // Relay the ACL downgrade to the gateway (source of truth). The gateway's
208
258
  // mark_channel_revoked enforces the guardian guard and dual-writes the
209
- // contact-channel status back to the assistant DB.
259
+ // contact-channel status back to the assistant DB. Gate on the gateway
260
+ // delivery's live status, not the assistant DB column, so a redundant revoke
261
+ // is still skipped for an already-revoked binding.
210
262
  if (contactResult) {
211
- const channelStatus: ChannelStatus = contactResult.channel.status;
263
+ const delivery = await deliveryForChannel(contactResult.channel);
264
+ const deliveryStatus = delivery?.status;
212
265
  if (
213
- channelStatus === "active" ||
214
- channelStatus === "pending" ||
215
- channelStatus === "unverified"
266
+ deliveryStatus === "active" ||
267
+ deliveryStatus === "pending" ||
268
+ deliveryStatus === "unverified"
216
269
  ) {
217
270
  const result = await ipcCallPersistent("mark_channel_revoked", {
218
271
  contactChannelId: contactResult.channel.id,
@@ -294,7 +347,10 @@ export async function verifyTrustedContact(
294
347
  };
295
348
  }
296
349
 
297
- if (channel.status === "active" && channel.verifiedAt != null) {
350
+ // Already-verified short-circuit derived from the gateway contact-channel read
351
+ // (ACL SoT), which covers all contacts — not just guardian deliveries.
352
+ const gwState = await gatewayContactChannelState(channel);
353
+ if (gwState?.status === "active" && gwState.verifiedAt != null) {
298
354
  return {
299
355
  success: false,
300
356
  error: "already_verified",
@@ -579,7 +635,7 @@ export async function handleChannelVerificationSession(
579
635
  ...publicResult,
580
636
  });
581
637
  } else {
582
- const result = createInboundChallenge(
638
+ const result = await createInboundChallenge(
583
639
  channel,
584
640
  msg.rebind,
585
641
  msg.conversationId,
@@ -590,7 +646,7 @@ export async function handleChannelVerificationSession(
590
646
  });
591
647
  }
592
648
  } else if (msg.action === "status") {
593
- const result = getVerificationStatus(channel);
649
+ const result = await getVerificationStatus(channel);
594
650
  broadcastMessage({
595
651
  type: "channel_verification_session_response",
596
652
  ...result,