@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
@@ -19,14 +19,28 @@ mock.module("../../../util/logger.js", () => ({
19
19
  }));
20
20
 
21
21
  // Track contact-store reads to prove findContactChannel is NOT used on the
22
- // verdict path. findGuardianForChannel is still called by resolveGuardianLabel.
22
+ // verdict path.
23
23
  const findContactChannelCalls: unknown[] = [];
24
24
  mock.module("../../../contacts/contact-store.js", () => ({
25
25
  findContactChannel: (params: unknown) => {
26
26
  findContactChannelCalls.push(params);
27
27
  return null;
28
28
  },
29
- findGuardianForChannel: () => null,
29
+ }));
30
+
31
+ // resolveGuardianLabel resolves the guardian via the gateway delivery reader.
32
+ let guardianDeliveryList: Array<Record<string, unknown>> = [];
33
+ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
34
+ getGuardianDelivery: async () => guardianDeliveryList,
35
+ guardianForChannel: (
36
+ list: Array<Record<string, unknown>>,
37
+ channelType: string,
38
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
39
+ }));
40
+
41
+ mock.module("../../../prompts/user-reference.js", () => ({
42
+ resolveGuardianName: (displayName?: string | null) =>
43
+ displayName && displayName.trim().length > 0 ? displayName.trim() : "my human",
30
44
  }));
31
45
 
32
46
  const deliverReplyCalls: Array<{ url: string; payload: unknown }> = [];
@@ -126,6 +140,7 @@ beforeEach(() => {
126
140
  deliverReplyCalls.length = 0;
127
141
  accessRequestCalls.length = 0;
128
142
  inviteTokenForTest = undefined;
143
+ guardianDeliveryList = [];
129
144
  });
130
145
 
131
146
  afterEach(() => {
@@ -140,8 +155,8 @@ describe("enforceIngressAcl — verdict-sourced member resolution", () => {
140
155
 
141
156
  expect(result.earlyResponse).toBeUndefined();
142
157
  expect(result.resolvedMember).not.toBeNull();
143
- expect(result.resolvedMember!.channel.status).toBe("active");
144
- expect(result.resolvedMember!.channel.policy).toBe("allow");
158
+ expect(result.resolvedMember!.status).toBe("active");
159
+ expect(result.resolvedMember!.policy).toBe("allow");
145
160
  // Member came from the verdict, never the local contact store.
146
161
  expect(findContactChannelCalls.length).toBe(0);
147
162
  });
@@ -156,7 +171,8 @@ describe("enforceIngressAcl — verdict-sourced member resolution", () => {
156
171
  );
157
172
 
158
173
  expect(result.earlyResponse).toBeUndefined();
159
- expect(result.resolvedMember!.contact.role).toBe("guardian");
174
+ expect(result.resolvedMember).not.toBeNull();
175
+ expect(result.resolvedMember!.status).toBe("active");
160
176
  expect(findContactChannelCalls.length).toBe(0);
161
177
  });
162
178
  });
@@ -249,6 +265,82 @@ describe("enforceIngressAcl — fail-closed on absent verdict", () => {
249
265
  });
250
266
  });
251
267
 
268
+ describe("enforceIngressAcl — fail-closed on resolutionFailed verdict", () => {
269
+ test("resolutionFailed verdict → not_a_member deny, does not flow to intercepts", async () => {
270
+ inviteTokenForTest = "iv_token123";
271
+ const result = await enforceIngressAcl(
272
+ makeParams({
273
+ sourceMetadata: withVerdict({
274
+ trustClass: "unknown",
275
+ canonicalSenderId: "sender-1",
276
+ resolutionFailed: true,
277
+ }),
278
+ effectiveAdmissionPolicy: "strangers",
279
+ }),
280
+ );
281
+
282
+ expect(result.earlyResponse).toBeDefined();
283
+ expect(result.earlyResponse!.reason).toBe("not_a_member");
284
+ expect(result.resolvedMember).toBeNull();
285
+ // Distinct from a stranger: no invite redemption, onboarding, or
286
+ // guardian notification fires.
287
+ expect(result.earlyResponse!.inviteRedemption).toBeUndefined();
288
+ expect(deliverReplyCalls.length).toBe(0);
289
+ expect(accessRequestCalls.length).toBe(0);
290
+ expect(findContactChannelCalls.length).toBe(0);
291
+ });
292
+
293
+ test("real unknown stranger (no resolutionFailed) still redeems via intercept", async () => {
294
+ inviteTokenForTest = "iv_token123";
295
+ const result = await enforceIngressAcl(
296
+ makeParams({
297
+ sourceMetadata: withVerdict({
298
+ trustClass: "unknown",
299
+ canonicalSenderId: "sender-1",
300
+ }),
301
+ }),
302
+ );
303
+
304
+ expect(result.earlyResponse!.inviteRedemption).toBe("redeemed");
305
+ expect(result.resolvedMember).toBeNull();
306
+ });
307
+ });
308
+
309
+ describe("enforceIngressAcl — deny copy names the gateway guardian", () => {
310
+ test("non-member deny reply uses the guardian displayName from the gateway list", async () => {
311
+ guardianDeliveryList = [
312
+ {
313
+ channelType: "vellum",
314
+ contactId: "c-1",
315
+ principalId: "p-anchor",
316
+ displayName: "Alice Guardian",
317
+ address: "p-anchor",
318
+ status: "active",
319
+ },
320
+ ];
321
+
322
+ const result = await enforceIngressAcl(
323
+ makeParams({
324
+ sourceMetadata: withVerdict({
325
+ trustClass: "unknown",
326
+ canonicalSenderId: "stranger-1",
327
+ }),
328
+ }),
329
+ );
330
+
331
+ expect(result.earlyResponse!.reason).toBe("not_a_member");
332
+ const denyReply = deliverReplyCalls.find((c) =>
333
+ String((c.payload as { text?: string }).text ?? "").includes(
334
+ "tried talking to me",
335
+ ),
336
+ );
337
+ expect(denyReply).toBeDefined();
338
+ expect((denyReply!.payload as { text: string }).text).toContain(
339
+ "Alice Guardian",
340
+ );
341
+ });
342
+ });
343
+
252
344
  describe("enforceIngressAcl — fail-closed on malformed member verdict", () => {
253
345
  test("member identity + unknown policy → not_a_member deny even under strangers", async () => {
254
346
  const result = await enforceIngressAcl(
@@ -7,7 +7,7 @@ import type { AdmissionPolicy, SourceMetadata } from "@vellumai/gateway-client";
7
7
 
8
8
  import { isInviteCodeRedemptionEnabled } from "../../../channels/config.js";
9
9
  import type { ChannelId } from "../../../channels/types.js";
10
- import { findGuardianForChannel } from "../../../contacts/contact-store.js";
10
+ import { getGuardianDelivery } from "../../../contacts/guardian-delivery-reader.js";
11
11
  import { channelStatusToMemberStatus } from "../../../contacts/member-status.js";
12
12
  import type {
13
13
  ContactChannel,
@@ -25,6 +25,7 @@ import { getLogger } from "../../../util/logger.js";
25
25
  import { truncate } from "../../../util/truncate.js";
26
26
  import { hashVoiceCode } from "../../../util/voice-code.js";
27
27
  import { notifyGuardianOfAccessRequest } from "../../access-request-helper.js";
28
+ import { resolveAnchoredGuardian } from "../../anchored-guardian.js";
28
29
  import { getInviteAdapterRegistry } from "../../channel-invite-transport.js";
29
30
  import {
30
31
  createOutboundSession,
@@ -38,7 +39,8 @@ import {
38
39
  redeemInviteByCode,
39
40
  } from "../../invite-redemption-service.js";
40
41
  import { getInviteRedemptionReply } from "../../invite-redemption-templates.js";
41
- import { resolvedMemberFromVerdict } from "../../trust-verdict-consumer.js";
42
+ import type { VerdictMember } from "../../trust-verdict-consumer.js";
43
+ import { verdictMemberFromVerdict } from "../../trust-verdict-consumer.js";
42
44
 
43
45
  const log = getLogger("runtime-http");
44
46
 
@@ -46,28 +48,20 @@ const log = getLogger("runtime-http");
46
48
  * Resolve the guardian's display name for use in requester-facing messages.
47
49
  *
48
50
  * Uses the assistant's anchored vellum principal to validate the guardian
49
- * contact, matching the same strategy used by `notifyGuardianOfAccessRequest`.
50
- * This prevents stale or cross-assistant contacts from leaking a wrong name.
51
+ * binding, matching the same strategy used by `notifyGuardianOfAccessRequest`.
52
+ * This prevents stale or cross-assistant bindings from leaking a wrong name.
53
+ * Cosmetic copy, not an admission decision, so a null gateway list degrades
54
+ * gracefully to the default reference.
51
55
  */
52
- function resolveGuardianLabel(sourceChannel: ChannelId): string {
53
- const vellumGuardian = findGuardianForChannel("vellum");
54
- const anchoredPrincipalId = vellumGuardian?.contact.principalId;
55
-
56
- if (!anchoredPrincipalId) {
57
- return resolveGuardianName(undefined);
58
- }
59
-
60
- // Try source-channel guardian, but only accept it when the principal
61
- // matches the assistant's anchor.
62
- const sourceGuardian = findGuardianForChannel(sourceChannel);
63
- if (
64
- sourceGuardian &&
65
- sourceGuardian.contact.principalId === anchoredPrincipalId
66
- ) {
67
- return resolveGuardianName(sourceGuardian.contact.displayName);
68
- }
69
-
70
- return resolveGuardianName(vellumGuardian.contact.displayName);
56
+ async function resolveGuardianLabel(sourceChannel: ChannelId): Promise<string> {
57
+ // Cosmetic copy, not an admission decision: no local-store fallback, and a
58
+ // missing anchor principal degrades to the default reference.
59
+ const anchored = resolveAnchoredGuardian({
60
+ guardians: await getGuardianDelivery(),
61
+ sourceChannel,
62
+ requireAnchorPrincipal: true,
63
+ });
64
+ return resolveGuardianName(anchored?.displayName);
71
65
  }
72
66
 
73
67
  // ---------------------------------------------------------------------------
@@ -109,7 +103,7 @@ export type ResolvedMember = {
109
103
  };
110
104
 
111
105
  export interface AclResult {
112
- resolvedMember: ResolvedMember | null;
106
+ resolvedMember: VerdictMember | null;
113
107
  /** When set, the caller must return this response immediately. */
114
108
  earlyResponse?: Record<string, unknown>;
115
109
  /**
@@ -173,9 +167,27 @@ export async function enforceIngressAcl(
173
167
  };
174
168
  }
175
169
 
170
+ // Gateway attempted resolution but failed (DB error) → fail-closed deny,
171
+ // distinct from an absent verdict and from a real stranger. TEXT does not
172
+ // fall back to local ACL reads; the sender can retry.
173
+ if (verdict.resolutionFailed === true) {
174
+ log.warn(
175
+ { sourceChannel, externalUserId: canonicalSenderId },
176
+ "Ingress ACL: gateway trust resolution failed, denying fail-closed",
177
+ );
178
+ return {
179
+ resolvedMember: null,
180
+ earlyResponse: {
181
+ accepted: true,
182
+ denied: true,
183
+ reason: "not_a_member",
184
+ },
185
+ };
186
+ }
187
+
176
188
  // Member resolved from the gateway verdict (ACL + identity only); null for a
177
189
  // stranger verdict, which falls through to the non-member intercepts.
178
- const resolvedMember: ResolvedMember | null = resolvedMemberFromVerdict(verdict);
190
+ const resolvedMember: VerdictMember | null = verdictMemberFromVerdict(verdict);
179
191
 
180
192
  // A verdict carrying member identity but no resolvable member
181
193
  // (malformed/unknown ACL) fails closed, not treated as a stranger.
@@ -329,7 +341,7 @@ export async function enforceIngressAcl(
329
341
  if (slackVerifyResult.initiated) {
330
342
  // Still notify the guardian about the access attempt
331
343
  try {
332
- notifyGuardianOfAccessRequest({
344
+ await notifyGuardianOfAccessRequest({
333
345
  canonicalAssistantId,
334
346
  sourceChannel,
335
347
  conversationExternalId,
@@ -369,7 +381,7 @@ export async function enforceIngressAcl(
369
381
  try {
370
382
  await deliverChannelReply(dmCallbackUrl, {
371
383
  chatId: senderUserId,
372
- text: `I don't recognize you yet! I've let ${resolveGuardianLabel(sourceChannel)} know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.`,
384
+ text: `I don't recognize you yet! I've let ${await resolveGuardianLabel(sourceChannel)} know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.`,
373
385
  assistantId,
374
386
  });
375
387
  } catch (err) {
@@ -404,7 +416,7 @@ export async function enforceIngressAcl(
404
416
 
405
417
  if (emailVerifyResult.initiated) {
406
418
  try {
407
- notifyGuardianOfAccessRequest({
419
+ await notifyGuardianOfAccessRequest({
408
420
  canonicalAssistantId,
409
421
  sourceChannel,
410
422
  conversationExternalId,
@@ -443,7 +455,7 @@ export async function enforceIngressAcl(
443
455
  // deduplication, canonical request creation, and notification emission.
444
456
  let guardianNotified = false;
445
457
  try {
446
- const accessResult = notifyGuardianOfAccessRequest({
458
+ const accessResult = await notifyGuardianOfAccessRequest({
447
459
  canonicalAssistantId,
448
460
  sourceChannel,
449
461
  conversationExternalId,
@@ -467,7 +479,7 @@ export async function enforceIngressAcl(
467
479
  }
468
480
 
469
481
  const replyText = guardianNotified
470
- ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel)} know you tried talking to me and get back to you.`
482
+ ? `Hmm looks like you don't have access to talk to me. I'll let ${await resolveGuardianLabel(sourceChannel)} know you tried talking to me and get back to you.`
471
483
  : "Sorry, you haven't been approved to message this assistant.";
472
484
  let replyDelivered = false;
473
485
  if (replyCallbackUrl) {
@@ -507,8 +519,8 @@ export async function enforceIngressAcl(
507
519
  }
508
520
 
509
521
  if (resolvedMember) {
510
- if (resolvedMember.channel.status !== "active") {
511
- const isBlockedMember = resolvedMember.channel.status === "blocked";
522
+ if (resolvedMember.status !== "active") {
523
+ const isBlockedMember = resolvedMember.status === "blocked";
512
524
  // Bootstrap commands must pass through for re-verifiable states
513
525
  // (pending/revoked), but never for blocked members.
514
526
  let denyInactiveMember = true;
@@ -529,7 +541,7 @@ export async function enforceIngressAcl(
529
541
  log.info(
530
542
  {
531
543
  sourceChannel,
532
- channelId: resolvedMember.channel.id,
544
+ channelId: resolvedMember.channelId,
533
545
  hasValidBootstrapSession: false,
534
546
  },
535
547
  "Ingress ACL: inactive member bootstrap bypass denied",
@@ -614,11 +626,11 @@ export async function enforceIngressAcl(
614
626
  if (!isBlockedMember && denyInactiveMember) {
615
627
  if (
616
628
  (effectiveAdmissionPolicy === "strangers" &&
617
- resolvedMember.channel.status !== "revoked") ||
629
+ resolvedMember.status !== "revoked") ||
618
630
  ((effectiveAdmissionPolicy === "any_contact" ||
619
631
  effectiveAdmissionPolicy === "guardian_only") &&
620
- (resolvedMember.channel.status === "pending" ||
621
- resolvedMember.channel.status === "unverified"))
632
+ (resolvedMember.status === "pending" ||
633
+ resolvedMember.status === "unverified"))
622
634
  ) {
623
635
  denyInactiveMember = false;
624
636
  }
@@ -628,8 +640,8 @@ export async function enforceIngressAcl(
628
640
  log.info(
629
641
  {
630
642
  sourceChannel,
631
- channelId: resolvedMember.channel.id,
632
- status: resolvedMember.channel.status,
643
+ channelId: resolvedMember.channelId,
644
+ status: resolvedMember.status,
633
645
  },
634
646
  "Ingress ACL: member not active, denying",
635
647
  );
@@ -639,7 +651,7 @@ export async function enforceIngressAcl(
639
651
  // the guardian made an explicit decision to block them.
640
652
  if (
641
653
  sourceChannel === "slack" &&
642
- resolvedMember.channel.status !== "blocked" &&
654
+ resolvedMember.status !== "blocked" &&
643
655
  (canonicalSenderId ?? rawSenderId)
644
656
  ) {
645
657
  const slackVerifyResult = initiateSlackVerificationChallenge({
@@ -649,7 +661,7 @@ export async function enforceIngressAcl(
649
661
 
650
662
  if (slackVerifyResult.initiated) {
651
663
  try {
652
- notifyGuardianOfAccessRequest({
664
+ await notifyGuardianOfAccessRequest({
653
665
  canonicalAssistantId,
654
666
  sourceChannel,
655
667
  conversationExternalId,
@@ -657,7 +669,7 @@ export async function enforceIngressAcl(
657
669
  actorDisplayName,
658
670
  actorUsername,
659
671
  previousMemberStatus: channelStatusToMemberStatus(
660
- resolvedMember.channel.status,
672
+ resolvedMember.status,
661
673
  ),
662
674
  messagePreview: truncate(
663
675
  trimmedContent,
@@ -688,7 +700,7 @@ export async function enforceIngressAcl(
688
700
  try {
689
701
  await deliverChannelReply(dmCallbackUrl, {
690
702
  chatId: senderUserId,
691
- text: `I don't recognize you yet! I've let ${resolveGuardianLabel(sourceChannel)} know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.`,
703
+ text: `I don't recognize you yet! I've let ${await resolveGuardianLabel(sourceChannel)} know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.`,
692
704
  assistantId,
693
705
  });
694
706
  } catch (err) {
@@ -715,9 +727,9 @@ export async function enforceIngressAcl(
715
727
  // re-approve. Blocked members are intentionally excluded — the
716
728
  // guardian already made an explicit decision to block them.
717
729
  let guardianNotified = false;
718
- if (resolvedMember.channel.status !== "blocked") {
730
+ if (resolvedMember.status !== "blocked") {
719
731
  try {
720
- const accessResult = notifyGuardianOfAccessRequest({
732
+ const accessResult = await notifyGuardianOfAccessRequest({
721
733
  canonicalAssistantId,
722
734
  sourceChannel,
723
735
  conversationExternalId,
@@ -725,7 +737,7 @@ export async function enforceIngressAcl(
725
737
  actorDisplayName,
726
738
  actorUsername,
727
739
  previousMemberStatus: channelStatusToMemberStatus(
728
- resolvedMember.channel.status,
740
+ resolvedMember.status,
729
741
  ),
730
742
  messagePreview: truncate(
731
743
  trimmedContent,
@@ -745,7 +757,7 @@ export async function enforceIngressAcl(
745
757
  }
746
758
 
747
759
  const inactiveReplyText = guardianNotified
748
- ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel)} know you tried talking to me and get back to you.`
760
+ ? `Hmm looks like you don't have access to talk to me. I'll let ${await resolveGuardianLabel(sourceChannel)} know you tried talking to me and get back to you.`
749
761
  : "Sorry, you haven't been approved to message this assistant.";
750
762
  let inactiveReplyDelivered = false;
751
763
  if (replyCallbackUrl) {
@@ -779,16 +791,16 @@ export async function enforceIngressAcl(
779
791
  earlyResponse: {
780
792
  accepted: true,
781
793
  denied: true,
782
- reason: `member_${channelStatusToMemberStatus(resolvedMember.channel.status)}`,
794
+ reason: `member_${channelStatusToMemberStatus(resolvedMember.status)}`,
783
795
  ...(!inactiveReplyDelivered && { replyText: inactiveReplyText }),
784
796
  },
785
797
  };
786
798
  }
787
799
  }
788
800
 
789
- if (resolvedMember.channel.policy === "deny") {
801
+ if (resolvedMember.policy === "deny") {
790
802
  log.info(
791
- { sourceChannel, channelId: resolvedMember.channel.id },
803
+ { sourceChannel, channelId: resolvedMember.channelId },
792
804
  "Ingress ACL: member policy deny",
793
805
  );
794
806
  const denyReplyText =
@@ -9,6 +9,10 @@
9
9
  */
10
10
  import type { ChannelId, InterfaceId } from "../../../channels/types.js";
11
11
  import { findGuardianForChannel } from "../../../contacts/contact-store.js";
12
+ import {
13
+ getGuardianDelivery,
14
+ guardianForChannel,
15
+ } from "../../../contacts/guardian-delivery-reader.js";
12
16
  import type { ServerMessage } from "../../../daemon/message-protocol.js";
13
17
  import type { TrustContext } from "../../../daemon/trust-context.js";
14
18
  import {
@@ -960,10 +964,18 @@ function startTrustedContactApprovalNotifier(params: {
960
964
 
961
965
  if (info && !globalNotifiedApprovalRequestIds.has(info.requestId)) {
962
966
  globalNotifiedApprovalRequestIds.set(info.requestId, conversationId);
963
- const guardian = findGuardianForChannel(sourceChannel);
964
- const guardianName = resolveGuardianName(
965
- guardian?.contact.displayName,
966
- );
967
+ // Gateway-resolved guardian display name with the transitional
968
+ // local fallback on null/no-match (display-only). Removed in Combo 11.
969
+ const guardians = await getGuardianDelivery({
970
+ channelTypes: [sourceChannel],
971
+ });
972
+ const gatewayDisplayName = guardians
973
+ ? guardianForChannel(guardians, sourceChannel)?.displayName
974
+ : undefined;
975
+ const displayName =
976
+ gatewayDisplayName ??
977
+ findGuardianForChannel(sourceChannel)?.contact.displayName;
978
+ const guardianName = resolveGuardianName(displayName);
967
979
  const waitingText = `Waiting for ${guardianName}'s approval...`;
968
980
  try {
969
981
  await deliverChannelReply(replyCallbackUrl, {
@@ -13,8 +13,8 @@ import { storePayload } from "../../../memory/delivery-crud.js";
13
13
  import { emitNotificationSignal } from "../../../notifications/emit-signal.js";
14
14
  import { getLogger } from "../../../util/logger.js";
15
15
  import { getGuardianBinding } from "../../channel-verification-service.js";
16
+ import type { VerdictMember } from "../../trust-verdict-consumer.js";
16
17
  import { GUARDIAN_APPROVAL_TTL_MS } from "../channel-route-shared.js";
17
- import type { ResolvedMember } from "./acl-enforcement.js";
18
18
 
19
19
  const log = getLogger("runtime-http");
20
20
 
@@ -23,7 +23,7 @@ const log = getLogger("runtime-http");
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
25
  export interface EscalationInterceptParams {
26
- resolvedMember: ResolvedMember | null;
26
+ resolvedMember: VerdictMember | null;
27
27
  canonicalAssistantId: string;
28
28
  sourceChannel: ChannelId;
29
29
  sourceInterface: InterfaceId;
@@ -49,9 +49,9 @@ export interface EscalationInterceptParams {
49
49
  * Returns a Response if the escalation was handled (the pipeline should
50
50
  * short-circuit), or null to continue the pipeline.
51
51
  */
52
- export function handleEscalationIntercept(
52
+ export async function handleEscalationIntercept(
53
53
  params: EscalationInterceptParams,
54
- ): Record<string, unknown> | null {
54
+ ): Promise<Record<string, unknown> | null> {
55
55
  const {
56
56
  resolvedMember,
57
57
  canonicalAssistantId,
@@ -72,15 +72,15 @@ export function handleEscalationIntercept(
72
72
  rawSenderId,
73
73
  } = params;
74
74
 
75
- if (resolvedMember?.channel.policy !== "escalate") {
75
+ if (resolvedMember?.policy !== "escalate") {
76
76
  return null;
77
77
  }
78
78
 
79
- const binding = getGuardianBinding(canonicalAssistantId, sourceChannel);
79
+ const binding = await getGuardianBinding(canonicalAssistantId, sourceChannel);
80
80
  if (!binding) {
81
81
  // Fail-closed: can't escalate without a guardian to route to
82
82
  log.info(
83
- { sourceChannel, channelId: resolvedMember.channel.id },
83
+ { sourceChannel, channelId: resolvedMember.channelId },
84
84
  "Ingress ACL: escalate policy but no guardian binding, denying",
85
85
  );
86
86
  return {
@@ -4,7 +4,9 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
4
  // Mocks — must be set up before importing the module under test
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
- let mockGuardian: { contact: unknown; channel: unknown } | null = null;
7
+ // Gateway guardian-delivery list: empty = unbound, one entry = bound,
8
+ // null = gateway unreachable.
9
+ let mockGuardianList: Array<Record<string, unknown>> | null = [];
8
10
  let mockActiveSession: Record<string, unknown> | null = null;
9
11
  let mockSessionResult = {
10
12
  sessionId: "sess-1",
@@ -20,8 +22,14 @@ let deliverChannelReplyCalls: unknown[][] = [];
20
22
  let emitNotificationSignalCalls: unknown[] = [];
21
23
  let messageIdCounter = 0;
22
24
 
23
- mock.module("../../../contacts/contact-store.js", () => ({
24
- findGuardianForChannel: () => mockGuardian,
25
+ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
26
+ // Existence guard reads fresh (uncached) only this variant is stubbed.
27
+ getGuardianDeliveryFresh: () => Promise.resolve(mockGuardianList),
28
+ guardianForChannel: (
29
+ list: Array<{ channelType: string; status: string }>,
30
+ channelType: string,
31
+ ) =>
32
+ list.find((g) => g.channelType === channelType && g.status === "active"),
25
33
  }));
26
34
 
27
35
  mock.module("../../channel-verification-service.js", () => ({
@@ -96,7 +104,7 @@ function makeParams(
96
104
 
97
105
  describe("handleGuardianActivationIntercept", () => {
98
106
  beforeEach(() => {
99
- mockGuardian = null;
107
+ mockGuardianList = [];
100
108
  mockActiveSession = null;
101
109
  mockSessionResult = {
102
110
  sessionId: "sess-1",
@@ -155,10 +163,15 @@ describe("handleGuardianActivationIntercept", () => {
155
163
  });
156
164
 
157
165
  test("bare /start with existing guardian returns null", async () => {
158
- mockGuardian = {
159
- contact: { id: "contact-1", role: "guardian" },
160
- channel: { id: "ch-1", type: "telegram" },
161
- };
166
+ mockGuardianList = [{ channelType: "telegram", status: "active" }];
167
+
168
+ const result = await handleGuardianActivationIntercept(makeParams());
169
+ expect(result).toBeNull();
170
+ expect(createOutboundSessionCalls).toHaveLength(0);
171
+ });
172
+
173
+ test("null guardian list (gateway unreachable) does NOT auto-start", async () => {
174
+ mockGuardianList = null;
162
175
 
163
176
  const result = await handleGuardianActivationIntercept(makeParams());
164
177
  expect(result).toBeNull();
@@ -9,7 +9,10 @@
9
9
  * the guardian binding, and sends a success reply.
10
10
  */
11
11
  import type { ChannelId } from "../../../channels/types.js";
12
- import { findGuardianForChannel } from "../../../contacts/contact-store.js";
12
+ import {
13
+ getGuardianDeliveryFresh,
14
+ guardianForChannel,
15
+ } from "../../../contacts/guardian-delivery-reader.js";
13
16
  import { emitNotificationSignal } from "../../../notifications/emit-signal.js";
14
17
  import { getLogger } from "../../../util/logger.js";
15
18
  import {
@@ -88,8 +91,16 @@ export async function handleGuardianActivationIntercept(
88
91
  // Only proceed for Telegram (can be extended later)
89
92
  if (sourceChannel !== "telegram") return null;
90
93
 
91
- // If a guardian already exists for this channel, continue to normal flow
92
- if (findGuardianForChannel(sourceChannel)) return null;
94
+ // If a guardian already exists for this channel, continue to normal flow.
95
+ // Null-list (gateway unreachable) is treated as guardian-present so a
96
+ // transient miss does NOT spuriously auto-start verification.
97
+ // Read fresh: gateway-side binding writes don't invalidate the daemon cache.
98
+ const guardianList = await getGuardianDeliveryFresh({
99
+ channelTypes: [sourceChannel],
100
+ });
101
+ if (guardianList === null || guardianForChannel(guardianList, sourceChannel)) {
102
+ return null;
103
+ }
93
104
 
94
105
  // Can't bind a session without sender identity
95
106
  if (!rawSenderId) return null;
@@ -102,6 +102,7 @@ import { ROUTES as OAUTH_COMMANDS_ROUTES } from "./oauth-commands-routes.js";
102
102
  import { ROUTES as OAUTH_CONNECT_ROUTES } from "./oauth-connect-routes.js";
103
103
  import { ROUTES as OAUTH_LIFECYCLE_ROUTES } from "./oauth-lifecycle-routes.js";
104
104
  import { ROUTES as OAUTH_PROVIDERS_ROUTES } from "./oauth-providers.js";
105
+ import { ROUTES as ONBOARDING_CHECKIN_ROUTES } from "./onboarding-checkin-routes.js";
105
106
  import { ROUTES as PLATFORM_ROUTES } from "./platform-routes.js";
106
107
  import { ROUTES as PLAYGROUND_ROUTES } from "./playground/index.js";
107
108
  import { ROUTES as PLUGINS_ROUTES } from "./plugins-routes.js";
@@ -234,6 +235,7 @@ export const ROUTES: RouteDefinition[] = [
234
235
  ...OAUTH_LIFECYCLE_ROUTES,
235
236
  ...OAUTH_COMMANDS_ROUTES,
236
237
  ...OAUTH_PROVIDERS_ROUTES,
238
+ ...ONBOARDING_CHECKIN_ROUTES,
237
239
  ...PLATFORM_ROUTES,
238
240
  ...PLAYGROUND_ROUTES,
239
241
  ...PLUGINS_ROUTES,