@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
@@ -779,6 +779,31 @@ describe("getUsageDayBuckets", () => {
779
779
  expect(buckets[0].totalEstimatedCostUsd).toBeCloseTo(0.05);
780
780
  expect(buckets[0].eventCount).toBe(2);
781
781
  });
782
+
783
+ test("collapses many events within a sub-bucket span without losing totals", () => {
784
+ // The read path pre-aggregates events into 15-minute UTC buckets in SQL
785
+ // before local-time bucketing. Many events inside one such window must
786
+ // still sum exactly, and events split across the 15-minute boundary but
787
+ // within the same local day must land in the same day bucket.
788
+ const windowStart = utcMs(2025, 3, 1, 10); // 10:00:00 UTC
789
+ for (let i = 0; i < 50; i++) {
790
+ // Spread across ~16 minutes so the events straddle a 15-minute boundary.
791
+ insertEventAt(windowStart + i * 20_000, {
792
+ inputTokens: 2,
793
+ outputTokens: 1,
794
+ });
795
+ }
796
+
797
+ const buckets = getUsageDayBuckets({
798
+ from: utcMs(2025, 3, 1),
799
+ to: utcMs(2025, 3, 2),
800
+ });
801
+ expect(buckets).toHaveLength(1);
802
+ expect(buckets[0].date).toBe("2025-03-01");
803
+ expect(buckets[0].totalInputTokens).toBe(100);
804
+ expect(buckets[0].totalOutputTokens).toBe(50);
805
+ expect(buckets[0].eventCount).toBe(50);
806
+ });
782
807
  });
783
808
 
784
809
  describe("getUsageDayBuckets — timezone-aware", () => {
@@ -176,6 +176,22 @@ mock.module("../calls/channel-admission-reader.js", () => ({
176
176
  getChannelAdmissionPolicy: mockGetChannelAdmissionPolicy,
177
177
  }));
178
178
 
179
+ // Mock the inbound trust reader. handleStart awaits this and threads the
180
+ // verdict into routeSetup so the media-stream transport enforces the same
181
+ // gateway ACL as ConversationRelay. Tests override mockInboundVerdict.
182
+ let mockInboundVerdict: unknown = null;
183
+ const mockGetInboundTrustVerdict = jest.fn(
184
+ async (_args?: Record<string, unknown>) => mockInboundVerdict,
185
+ );
186
+ mock.module("../calls/inbound-trust-reader.js", () => ({
187
+ getInboundTrustVerdict: mockGetInboundTrustVerdict,
188
+ getPhoneCallerVerdict: (otherPartyNumber: string | undefined) =>
189
+ mockGetInboundTrustVerdict({
190
+ channelType: "phone",
191
+ actorExternalId: otherPartyNumber || undefined,
192
+ }),
193
+ }));
194
+
179
195
  // Mock the actor trust resolver (used by handleStart to derive trust context)
180
196
  mock.module("../runtime/actor-trust-resolver.js", () => ({
181
197
  toTrustContext: jest.fn(() => ({
@@ -356,6 +372,8 @@ beforeEach(() => {
356
372
  mockGetChannelAdmissionPolicy.mockClear();
357
373
  mockAdmissionPolicy = null;
358
374
  mockAdmissionGate = null;
375
+ mockGetInboundTrustVerdict.mockClear();
376
+ mockInboundVerdict = null;
359
377
  // Reset routeSetup to default normal_call
360
378
  mockRouteSetupResult = {
361
379
  outcome: { action: "normal_call" as const, isInbound: true },
@@ -1535,4 +1553,113 @@ describe("media-stream setup outcome scenarios", () => {
1535
1553
  expect(speakSystemPrompt).not.toHaveBeenCalled();
1536
1554
  });
1537
1555
  });
1556
+
1557
+ // ── Gateway trust verdict ──────────────────────────────────────────
1558
+ // handleStart fetches getInboundTrustVerdict for the inbound caller and
1559
+ // threads it into routeSetup, so the media-stream transport enforces the
1560
+ // same gateway ACL as ConversationRelay. routeSetup itself decides
1561
+ // verdict-vs-local; these tests assert the verdict is fetched and passed.
1562
+
1563
+ describe("gateway trust verdict", () => {
1564
+ test("fetches the inbound caller's verdict and threads it into routeSetup", async () => {
1565
+ mockInboundVerdict = {
1566
+ channelType: "phone",
1567
+ actorExternalId: "+14155550000",
1568
+ contactId: "contact-1",
1569
+ channelId: "channel-1",
1570
+ status: "verified",
1571
+ policy: "allow",
1572
+ resolutionFailed: false,
1573
+ };
1574
+
1575
+ const mockWs = createMockWs();
1576
+ mockSessions.set("call-verdict-thread-1", {
1577
+ id: "call-verdict-thread-1",
1578
+ conversationId: "conv-verdict-thread-1",
1579
+ status: "initiated",
1580
+ task: null,
1581
+ startedAt: null,
1582
+ fromNumber: "+14155550000",
1583
+ toNumber: "+15550001111",
1584
+ });
1585
+
1586
+ const session = new MediaStreamCallSession(
1587
+ mockWs.ws,
1588
+ "call-verdict-thread-1",
1589
+ );
1590
+ session.handleMessage(makeStartMessage());
1591
+ await session.whenSetupSettled();
1592
+
1593
+ // Verdict fetched for the inbound caller (from number) on the phone channel.
1594
+ expect(mockGetInboundTrustVerdict).toHaveBeenCalledWith({
1595
+ channelType: "phone",
1596
+ actorExternalId: "+14155550000",
1597
+ });
1598
+ // Verdict threaded into routeSetup.
1599
+ expect(routeSetup).toHaveBeenCalledWith(
1600
+ expect.objectContaining({ verdict: mockInboundVerdict }),
1601
+ );
1602
+ });
1603
+
1604
+ test("a blocked/denied member verdict is enforced (deny) on the media-stream transport", async () => {
1605
+ // The real router returns `deny` for a member verdict whose ACL is
1606
+ // blocked/revoked/deny; the mock reflects that outcome here.
1607
+ mockInboundVerdict = {
1608
+ channelType: "phone",
1609
+ actorExternalId: "+14155550000",
1610
+ contactId: "contact-1",
1611
+ channelId: "channel-1",
1612
+ status: "blocked",
1613
+ policy: "deny",
1614
+ resolutionFailed: false,
1615
+ };
1616
+ mockRouteSetupResult = {
1617
+ outcome: {
1618
+ action: "deny",
1619
+ message:
1620
+ "This number is not authorized to reach the assistant right now.",
1621
+ logReason: "Inbound voice ACL: member blocked",
1622
+ },
1623
+ resolved: {
1624
+ assistantId: "self",
1625
+ isInbound: true,
1626
+ otherPartyNumber: "+14155550000",
1627
+ actorTrust: { trustClass: "unknown", memberRecord: null },
1628
+ },
1629
+ };
1630
+
1631
+ const mockWs = createMockWs();
1632
+ mockSessions.set("call-verdict-deny-1", {
1633
+ id: "call-verdict-deny-1",
1634
+ conversationId: "conv-verdict-deny-1",
1635
+ status: "initiated",
1636
+ task: null,
1637
+ startedAt: null,
1638
+ fromNumber: "+14155550000",
1639
+ toNumber: "+15550001111",
1640
+ });
1641
+
1642
+ const session = new MediaStreamCallSession(
1643
+ mockWs.ws,
1644
+ "call-verdict-deny-1",
1645
+ );
1646
+ session.handleMessage(makeStartMessage());
1647
+ await session.whenSetupSettled();
1648
+
1649
+ // Verdict was passed into routeSetup, which denied the caller.
1650
+ expect(routeSetup).toHaveBeenCalledWith(
1651
+ expect.objectContaining({ verdict: mockInboundVerdict }),
1652
+ );
1653
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
1654
+ expect.anything(),
1655
+ "This number is not authorized to reach the assistant right now.",
1656
+ );
1657
+ expect(updateCallSession).toHaveBeenCalledWith(
1658
+ "call-verdict-deny-1",
1659
+ expect.objectContaining({ status: "failed" }),
1660
+ );
1661
+ expect(registerCallController).not.toHaveBeenCalled();
1662
+ expect(mockStartInitialGreeting).not.toHaveBeenCalled();
1663
+ });
1664
+ });
1538
1665
  });
@@ -19,6 +19,8 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
19
19
  // bindings resolve through the mocks.
20
20
  const updateMessageMetadataMock = mock((_id: string, _updates: unknown) => {});
21
21
  mock.module("../memory/conversation-crud.js", () => ({
22
+ setConversationProcessingStartedAt: () => {},
23
+ isConversationProcessing: () => false,
22
24
  updateMessageMetadata: updateMessageMetadataMock,
23
25
  }));
24
26
 
@@ -79,6 +79,8 @@ const getBindingByChannelChatMock = mock(
79
79
  );
80
80
 
81
81
  mock.module("../memory/conversation-crud.js", () => ({
82
+ setConversationProcessingStartedAt: () => {},
83
+ isConversationProcessing: () => false,
82
84
  addMessage: addMessageMock,
83
85
  getConversation: getConversationMock,
84
86
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
@@ -421,8 +421,8 @@ describe("handleMigrationImport — JSON {url} body", () => {
421
421
  describe("handleMigrationImport — no-swap path omits newer-migration warning", () => {
422
422
  test("credentials-only bundle does not inherit live-DB migration warnings", async () => {
423
423
  // Seed the live workspace DB with a step:* checkpoint that's NOT
424
- // in the registry. validateMigrationState treats this as a "newer
425
- // version" and would otherwise push a warning into the report. With
424
+ // in the known step list. validateMigrationState treats this as a
425
+ // "newer version" and would otherwise push a warning into the report. With
426
426
  // the gate in appendNewerMigrationWarningsIfAny the warning must be
427
427
  // suppressed when the import didn't modify the workspace.
428
428
  const dbDir = join(testWorkspaceRoot, "data", "db");
@@ -95,6 +95,8 @@ mock.module("../config/loader.js", () => ({
95
95
  }));
96
96
 
97
97
  mock.module("../memory/conversation-crud.js", () => ({
98
+ setConversationProcessingStartedAt: () => {},
99
+ isConversationProcessing: () => false,
98
100
  addMessage: () => ({ id: "mock-msg-id" }),
99
101
  getMessageById: () => null,
100
102
  updateMessageContent: () => {},
@@ -61,6 +61,36 @@ mock.module("../runtime/gateway-client.js", () => ({
61
61
  },
62
62
  }));
63
63
 
64
+ // Guardian identity for the access request resolves via the gateway delivery
65
+ // reader, not the local contacts DB. Seed it per-test via seedGatewayGuardian.
66
+ interface GatewayGuardian {
67
+ channelType: string;
68
+ contactId: string;
69
+ principalId?: string | null;
70
+ displayName?: string | null;
71
+ address: string;
72
+ externalChatId?: string | null;
73
+ status: string;
74
+ verifiedAt?: number | null;
75
+ }
76
+ let gatewayGuardians: GatewayGuardian[] = [];
77
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
78
+ getGuardianDelivery: async () => gatewayGuardians,
79
+ guardianForChannel: (list: GatewayGuardian[], channelType: string) =>
80
+ list.find((g) => g.channelType === channelType && g.status === "active"),
81
+ }));
82
+
83
+ function seedGatewayGuardian(g: Partial<GatewayGuardian> & {
84
+ channelType: string;
85
+ address: string;
86
+ }): void {
87
+ gatewayGuardians.push({
88
+ contactId: `c-${g.channelType}`,
89
+ status: "active",
90
+ ...g,
91
+ });
92
+ }
93
+
64
94
  import {
65
95
  listCanonicalGuardianDeliveries,
66
96
  listCanonicalGuardianRequests,
@@ -95,6 +125,7 @@ function resetState(): string {
95
125
  db.run("DELETE FROM contacts");
96
126
  emitSignalCalls.length = 0;
97
127
  deliverReplyCalls.length = 0;
128
+ gatewayGuardians = [];
98
129
  mockEmitResult = {
99
130
  signalId: "mock-signal-id",
100
131
  deduplicated: false,
@@ -102,8 +133,15 @@ function resetState(): string {
102
133
  reason: "mock",
103
134
  deliveryResults: [],
104
135
  };
105
- // Seed the vellum anchor binding (gateway does this at startup in production)
136
+ // Seed the vellum anchor binding in the gateway list (gateway does this at
137
+ // startup in production). The DB write mirrors it for any local INFO reads.
106
138
  const principalId = `vellum-principal-${crypto.randomUUID()}`;
139
+ seedGatewayGuardian({
140
+ channelType: "vellum",
141
+ address: principalId,
142
+ principalId,
143
+ displayName: principalId,
144
+ });
107
145
  createGuardianBinding({
108
146
  channel: "vellum",
109
147
  guardianExternalUserId: principalId,
@@ -172,6 +210,12 @@ describe("non-member access request notification", () => {
172
210
 
173
211
  test("guardian is notified when a non-member messages and a guardian binding exists", async () => {
174
212
  // Set up a guardian binding for this channel using the anchor principal
213
+ seedGatewayGuardian({
214
+ channelType: "telegram",
215
+ address: "guardian-user-789",
216
+ externalChatId: "guardian-chat-789",
217
+ principalId: anchorPrincipalId,
218
+ });
175
219
  createGuardianBinding({
176
220
  channel: "telegram",
177
221
  guardianExternalUserId: "guardian-user-789",
@@ -217,6 +261,12 @@ describe("non-member access request notification", () => {
217
261
  });
218
262
 
219
263
  test("no duplicate approval requests for repeated messages from same non-member", async () => {
264
+ seedGatewayGuardian({
265
+ channelType: "telegram",
266
+ address: "guardian-user-789",
267
+ externalChatId: "guardian-chat-789",
268
+ principalId: anchorPrincipalId,
269
+ });
220
270
  createGuardianBinding({
221
271
  channel: "telegram",
222
272
  guardianExternalUserId: "guardian-user-789",
@@ -289,6 +339,12 @@ describe("non-member access request notification", () => {
289
339
  // Only a voice guardian binding exists — no Telegram binding.
290
340
  // Since cross-channel fallback was removed, the access request resolves
291
341
  // to the assistant's vellum anchor identity instead.
342
+ seedGatewayGuardian({
343
+ channelType: "phone",
344
+ address: "guardian-voice-user",
345
+ externalChatId: "guardian-voice-chat",
346
+ principalId: anchorPrincipalId,
347
+ });
292
348
  createGuardianBinding({
293
349
  channel: "phone",
294
350
  guardianExternalUserId: "guardian-voice-user",
@@ -351,8 +407,8 @@ describe("access-request-helper unit tests", () => {
351
407
  anchorPrincipalId = resetState();
352
408
  });
353
409
 
354
- test("notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent", () => {
355
- const result = notifyGuardianOfAccessRequest({
410
+ test("notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent", async () => {
411
+ const result = await notifyGuardianOfAccessRequest({
356
412
  canonicalAssistantId: "self",
357
413
  sourceChannel: "telegram",
358
414
  conversationExternalId: "chat-123",
@@ -372,8 +428,8 @@ describe("access-request-helper unit tests", () => {
372
428
  expect(pending.length).toBe(0);
373
429
  });
374
430
 
375
- test("notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists", () => {
376
- const result = notifyGuardianOfAccessRequest({
431
+ test("notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists", async () => {
432
+ const result = await notifyGuardianOfAccessRequest({
377
433
  canonicalAssistantId: "self",
378
434
  sourceChannel: "telegram",
379
435
  conversationExternalId: "chat-123",
@@ -400,8 +456,14 @@ describe("access-request-helper unit tests", () => {
400
456
  expect(emitSignalCalls.length).toBe(1);
401
457
  });
402
458
 
403
- test("notifyGuardianOfAccessRequest falls back to assistant-anchored vellum identity when source-channel binding is missing", () => {
459
+ test("notifyGuardianOfAccessRequest falls back to assistant-anchored vellum identity when source-channel binding is missing", async () => {
404
460
  // Only voice binding exists
461
+ seedGatewayGuardian({
462
+ channelType: "phone",
463
+ address: "guardian-voice",
464
+ externalChatId: "voice-chat",
465
+ principalId: "test-principal-id",
466
+ });
405
467
  createGuardianBinding({
406
468
  channel: "phone",
407
469
  guardianExternalUserId: "guardian-voice",
@@ -410,7 +472,7 @@ describe("access-request-helper unit tests", () => {
410
472
  verifiedVia: "test",
411
473
  });
412
474
 
413
- const result = notifyGuardianOfAccessRequest({
475
+ const result = await notifyGuardianOfAccessRequest({
414
476
  canonicalAssistantId: "self",
415
477
  sourceChannel: "telegram",
416
478
  conversationExternalId: "tg-chat",
@@ -435,8 +497,20 @@ describe("access-request-helper unit tests", () => {
435
497
  expect(payload.guardianBindingChannel).toBe("vellum");
436
498
  });
437
499
 
438
- test("notifyGuardianOfAccessRequest prefers source-channel binding over vellum anchor", () => {
500
+ test("notifyGuardianOfAccessRequest prefers source-channel binding over vellum anchor", async () => {
439
501
  // Both Telegram and voice bindings exist with the anchor principal
502
+ seedGatewayGuardian({
503
+ channelType: "telegram",
504
+ address: "guardian-tg",
505
+ externalChatId: "tg-chat",
506
+ principalId: anchorPrincipalId,
507
+ });
508
+ seedGatewayGuardian({
509
+ channelType: "phone",
510
+ address: "guardian-voice",
511
+ externalChatId: "voice-chat",
512
+ principalId: anchorPrincipalId,
513
+ });
440
514
  createGuardianBinding({
441
515
  channel: "telegram",
442
516
  guardianExternalUserId: "guardian-tg",
@@ -452,7 +526,7 @@ describe("access-request-helper unit tests", () => {
452
526
  verifiedVia: "test",
453
527
  });
454
528
 
455
- const result = notifyGuardianOfAccessRequest({
529
+ const result = await notifyGuardianOfAccessRequest({
456
530
  canonicalAssistantId: "self",
457
531
  sourceChannel: "telegram",
458
532
  conversationExternalId: "chat-123",
@@ -477,8 +551,100 @@ describe("access-request-helper unit tests", () => {
477
551
  expect(payload.guardianBindingChannel).toBe("telegram");
478
552
  });
479
553
 
480
- test("notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload", () => {
481
- const result = notifyGuardianOfAccessRequest({
554
+ test("notifyGuardianOfAccessRequest falls back to local source-channel binding when gateway delivery is empty", async () => {
555
+ // Gateway delivery read yields nothing (restart/timeout/malformed IPC).
556
+ gatewayGuardians = [];
557
+ // Local dual-written mirror still has the vellum anchor + telegram binding.
558
+ createGuardianBinding({
559
+ channel: "telegram",
560
+ guardianExternalUserId: "guardian-tg",
561
+ guardianDeliveryChatId: "tg-chat",
562
+ guardianPrincipalId: anchorPrincipalId,
563
+ verifiedVia: "test",
564
+ });
565
+
566
+ const result = await notifyGuardianOfAccessRequest({
567
+ canonicalAssistantId: "self",
568
+ sourceChannel: "telegram",
569
+ conversationExternalId: "chat-123",
570
+ actorExternalId: "unknown-user",
571
+ });
572
+
573
+ expect(result.notified).toBe(true);
574
+
575
+ const pending = listCanonicalGuardianRequests({
576
+ status: "pending",
577
+ requesterExternalUserId: "unknown-user",
578
+ kind: "access_request",
579
+ });
580
+ expect(pending.length).toBe(1);
581
+ // Request is decidable: local read supplied the principal + source binding.
582
+ expect(pending[0].guardianPrincipalId).toBe(anchorPrincipalId);
583
+ expect(pending[0].guardianExternalUserId).toBe("guardian-tg");
584
+
585
+ const payload = emitSignalCalls[0].contextPayload as Record<
586
+ string,
587
+ unknown
588
+ >;
589
+ expect(payload.guardianBindingChannel).toBe("telegram");
590
+ });
591
+
592
+ test("notifyGuardianOfAccessRequest falls back to local vellum anchor when gateway delivery is empty", async () => {
593
+ // Gateway empty; only the local vellum anchor from resetState remains.
594
+ gatewayGuardians = [];
595
+
596
+ const result = await notifyGuardianOfAccessRequest({
597
+ canonicalAssistantId: "self",
598
+ sourceChannel: "telegram",
599
+ conversationExternalId: "chat-123",
600
+ actorExternalId: "unknown-user",
601
+ });
602
+
603
+ expect(result.notified).toBe(true);
604
+
605
+ const pending = listCanonicalGuardianRequests({
606
+ status: "pending",
607
+ requesterExternalUserId: "unknown-user",
608
+ kind: "access_request",
609
+ });
610
+ expect(pending.length).toBe(1);
611
+ // Decidable via the local vellum anchor principal.
612
+ expect(pending[0].guardianPrincipalId).toBe(anchorPrincipalId);
613
+
614
+ const payload = emitSignalCalls[0].contextPayload as Record<
615
+ string,
616
+ unknown
617
+ >;
618
+ expect(payload.guardianBindingChannel).toBe("vellum");
619
+ });
620
+
621
+ test("notifyGuardianOfAccessRequest does not create a decisionable request when both gateway and local reads are empty", async () => {
622
+ // Genuinely unbound assistant: gateway empty AND no local binding. Prior
623
+ // behavior rejects creation of a decisionable request without a principal.
624
+ gatewayGuardians = [];
625
+ const db = getDb();
626
+ db.run("DELETE FROM contact_channels");
627
+ db.run("DELETE FROM contacts");
628
+
629
+ await expect(
630
+ notifyGuardianOfAccessRequest({
631
+ canonicalAssistantId: "self",
632
+ sourceChannel: "telegram",
633
+ conversationExternalId: "chat-123",
634
+ actorExternalId: "unknown-user",
635
+ }),
636
+ ).rejects.toThrow();
637
+
638
+ const pending = listCanonicalGuardianRequests({
639
+ status: "pending",
640
+ requesterExternalUserId: "unknown-user",
641
+ kind: "access_request",
642
+ });
643
+ expect(pending.length).toBe(0);
644
+ });
645
+
646
+ test("notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload", async () => {
647
+ const result = await notifyGuardianOfAccessRequest({
482
648
  canonicalAssistantId: "self",
483
649
  sourceChannel: "phone",
484
650
  conversationExternalId: "+15559998888",
@@ -508,8 +674,8 @@ describe("access-request-helper unit tests", () => {
508
674
  expect(pending.length).toBe(1);
509
675
  });
510
676
 
511
- test("notifyGuardianOfAccessRequest includes requestCode in contextPayload", () => {
512
- const result = notifyGuardianOfAccessRequest({
677
+ test("notifyGuardianOfAccessRequest includes requestCode in contextPayload", async () => {
678
+ const result = await notifyGuardianOfAccessRequest({
513
679
  canonicalAssistantId: "self",
514
680
  sourceChannel: "telegram",
515
681
  conversationExternalId: "chat-123",
@@ -529,8 +695,8 @@ describe("access-request-helper unit tests", () => {
529
695
  expect((payload.requestCode as string).length).toBe(6);
530
696
  });
531
697
 
532
- test("notifyGuardianOfAccessRequest includes previousMemberStatus in contextPayload", () => {
533
- const result = notifyGuardianOfAccessRequest({
698
+ test("notifyGuardianOfAccessRequest includes previousMemberStatus in contextPayload", async () => {
699
+ const result = await notifyGuardianOfAccessRequest({
534
700
  canonicalAssistantId: "self",
535
701
  sourceChannel: "telegram",
536
702
  conversationExternalId: "chat-123",
@@ -570,7 +736,7 @@ describe("access-request-helper unit tests", () => {
570
736
  ],
571
737
  };
572
738
 
573
- const result = notifyGuardianOfAccessRequest({
739
+ const result = await notifyGuardianOfAccessRequest({
574
740
  canonicalAssistantId: "self",
575
741
  sourceChannel: "phone",
576
742
  conversationExternalId: "+15556667777",
@@ -603,6 +769,12 @@ describe("access-request-helper unit tests", () => {
603
769
  // Set up a telegram guardian binding with the anchor principal so
604
770
  // guardianResolutionSource resolves to "source-channel-contact" and
605
771
  // sameChannelOnly is true.
772
+ seedGatewayGuardian({
773
+ channelType: "telegram",
774
+ address: "guardian-user-456",
775
+ externalChatId: "guardian-chat-456",
776
+ principalId: anchorPrincipalId,
777
+ });
606
778
  createGuardianBinding({
607
779
  channel: "telegram",
608
780
  guardianExternalUserId: "guardian-user-456",
@@ -625,7 +797,7 @@ describe("access-request-helper unit tests", () => {
625
797
  ],
626
798
  };
627
799
 
628
- const result = notifyGuardianOfAccessRequest({
800
+ const result = await notifyGuardianOfAccessRequest({
629
801
  canonicalAssistantId: "self",
630
802
  sourceChannel: "telegram",
631
803
  conversationExternalId: "chat-123",
@@ -24,6 +24,10 @@ mock.module("../util/logger.js", () => ({
24
24
  }),
25
25
  }));
26
26
 
27
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
28
+ getGuardianDelivery: async () => null,
29
+ }));
30
+
27
31
  // Mock destination-resolver to return a destination for every requested channel.
28
32
  // External channels (telegram, slack) include bindingContext.
29
33
  mock.module("../notifications/destination-resolver.js", () => ({