@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5

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 (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -26,6 +26,8 @@ import {
26
26
  test,
27
27
  } from "bun:test";
28
28
 
29
+ import { and, desc, eq } from "drizzle-orm";
30
+
29
31
  // ── Platform + logger mocks (must come before any source imports) ────
30
32
 
31
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -50,12 +52,32 @@ let inviteClaimCalls = 0;
50
52
  let inviteClaimGate: Promise<void> | null = null;
51
53
  mock.module("../ipc/gateway-client.js", () => ({
52
54
  ipcCall: async () => undefined,
53
- ipcCallPersistent: async (method: string) => {
55
+ ipcCallPersistent: async (
56
+ method: string,
57
+ params?: Record<string, unknown>,
58
+ ) => {
54
59
  if (method === "record_invite_redemption") {
55
60
  inviteClaimCalls += 1;
56
61
  if (inviteClaimGate) await inviteClaimGate;
57
62
  return { ok: true, updated: true, mirrored: true };
58
63
  }
64
+ // The gateway owns the ACL verdict; activation fails closed when the relay
65
+ // does not land, so model a verified upsert for the redemption paths.
66
+ if (method === "upsert_verified_channel") {
67
+ return {
68
+ ok: true,
69
+ verified: true,
70
+ channel: {
71
+ id: "gw-channel-id",
72
+ contactId: (params?.contactId as string) ?? "gw-contact",
73
+ type: (params?.type as string) ?? "phone",
74
+ address: (params?.address as string) ?? "gw-addr",
75
+ status: "active",
76
+ verifiedAt: 1,
77
+ verifiedVia: (params?.verifiedVia as string) ?? "invite",
78
+ },
79
+ };
80
+ }
59
81
  return undefined;
60
82
  },
61
83
  }));
@@ -87,6 +109,24 @@ mock.module("../prompts/user-reference.js", () => ({
87
109
  },
88
110
  }));
89
111
 
112
+ // ── Guardian delivery reader mock ───────────────────────────────────
113
+ //
114
+ // resolveGuardianLabel primes its displayName from the gateway binding via
115
+ // getGuardianDelivery at setup. Tests drive the binding through this list.
116
+
117
+ // Tests set this to drive the guardian binding directly. When null (the
118
+ // default), the guardian-delivery-reader mock below derives the binding from
119
+ // the DB-seeded createGuardianBinding setup. Single mock registration lives
120
+ // below since `mock.module` is process-global and last-write-wins in Bun.
121
+ let mockGuardianDeliveryList:
122
+ | Array<{
123
+ channelType: string;
124
+ status: string;
125
+ displayName?: string | null;
126
+ address?: string;
127
+ }>
128
+ | null = null;
129
+
90
130
  // ── Config mock ─────────────────────────────────────────────────────
91
131
 
92
132
  const mockConfig = {
@@ -163,6 +203,129 @@ mock.module("../calls/channel-admission-reader.js", () => ({
163
203
  },
164
204
  }));
165
205
 
206
+ // ── Inbound trust verdict reader mock ───────────────────────────────
207
+ //
208
+ // Mid-call re-resolution (post verification/activation) prefers the gateway
209
+ // verdict via getInboundTrustVerdict, falling back to local resolution on a
210
+ // missing/failed/unusable verdict. Tests drive the verdict through
211
+ // `mockMidCallVerdict`; null (the default) exercises the local fallback. As
212
+ // with the admission reader, delegate to the real module for sibling files
213
+ // that load later in the same worker.
214
+ let mockMidCallVerdict:
215
+ | import("@vellumai/gateway-client").TrustVerdict
216
+ | null = null;
217
+ // When set, the mid-call re-resolution verdict reader blocks on this gate
218
+ // before returning, simulating a slow gateway round-trip so a test can drive a
219
+ // prompt into the re-resolution await window. The gate targets the mid-call
220
+ // re-resolution read (getInboundTrustVerdict) only — the per-caller redemption
221
+ // gate read (getPhoneCallerVerdict) resolves immediately so invite redemption
222
+ // reaches activation before the gated re-resolution runs.
223
+ let mockMidCallVerdictGate: Promise<void> | null = null;
224
+ const realInboundTrustReaderModule = {
225
+ ...(await import("../calls/inbound-trust-reader.js")),
226
+ };
227
+ let inboundTrustMockActive = false;
228
+ mock.module("../calls/inbound-trust-reader.js", () => ({
229
+ ...realInboundTrustReaderModule,
230
+ getInboundTrustVerdict: async (input: {
231
+ channelType: import("../channels/types.js").ChannelId;
232
+ actorExternalId?: string;
233
+ }) => {
234
+ if (!inboundTrustMockActive) {
235
+ return realInboundTrustReaderModule.getInboundTrustVerdict(input);
236
+ }
237
+ if (mockMidCallVerdictGate) await mockMidCallVerdictGate;
238
+ return mockMidCallVerdict;
239
+ },
240
+ getPhoneCallerVerdict: async (otherPartyNumber: string | undefined) => {
241
+ if (!inboundTrustMockActive) {
242
+ return realInboundTrustReaderModule.getPhoneCallerVerdict(
243
+ otherPartyNumber,
244
+ );
245
+ }
246
+ return mockMidCallVerdict;
247
+ },
248
+ }));
249
+
250
+ // ── Guardian delivery reader ────────────────────────────────────────
251
+ //
252
+ // Guardian identity now resolves via the gateway delivery reader. Derive the
253
+ // list from the DB-seeded guardian bindings so the existing createGuardianBinding
254
+ // setup keeps driving guardian resolution without per-test changes. Both the
255
+ // async read and the sync cache peek (read by resolveActorTrust) share the same
256
+ // DB-derived snapshot mirroring the gateway's active-guardian-channel query.
257
+ function deriveGuardianDeliveries(
258
+ channelTypes?: string[],
259
+ ): Array<Record<string, unknown>> {
260
+ if (mockGuardianDeliveryList) {
261
+ return channelTypes
262
+ ? mockGuardianDeliveryList.filter((g) =>
263
+ channelTypes.includes(g.channelType as string),
264
+ )
265
+ : mockGuardianDeliveryList;
266
+ }
267
+ const rows = getDb()
268
+ .select({ contact: contacts, channel: contactChannels })
269
+ .from(contacts)
270
+ .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
271
+ .where(
272
+ and(eq(contacts.role, "guardian"), eq(contactChannels.status, "active")),
273
+ )
274
+ .orderBy(desc(contactChannels.verifiedAt))
275
+ .all();
276
+ if (rows.length === 0) return [];
277
+ const guardianId = rows[0].contact.id;
278
+ const list = rows
279
+ .filter((r) => r.contact.id === guardianId)
280
+ .map((r) => ({
281
+ channelType: r.channel.type,
282
+ contactId: r.contact.id,
283
+ principalId: r.contact.principalId ?? null,
284
+ displayName: r.contact.displayName ?? null,
285
+ address: r.channel.address,
286
+ externalChatId: r.channel.externalChatId ?? null,
287
+ status: r.channel.status,
288
+ verifiedAt: r.channel.verifiedAt ?? null,
289
+ }));
290
+ return channelTypes
291
+ ? list.filter((g) =>
292
+ channelTypes.includes(g.channelType),
293
+ )
294
+ : list;
295
+ }
296
+
297
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
298
+ getGuardianDelivery: async (input?: { channelTypes?: string[] }) =>
299
+ deriveGuardianDeliveries(input?.channelTypes),
300
+ peekCachedGuardianDelivery: (input?: { channelTypes?: string[] }) =>
301
+ deriveGuardianDeliveries(input?.channelTypes),
302
+ guardianForChannel: (
303
+ list: Array<{ channelType: string; status: string }>,
304
+ channelType: string,
305
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
306
+ anyGuardian: (list: unknown[]) => list[0],
307
+ }));
308
+
309
+ // ── Trust verdict consumer spy ──────────────────────────────────────
310
+ //
311
+ // Tracks whether the verdict mapper produced the final mid-call context, so a
312
+ // test can assert the local resolver was used instead (verdict not consumed).
313
+ let trustVerdictMapperUsed = false;
314
+ const realTrustVerdictConsumerModule = {
315
+ ...(await import("../runtime/trust-verdict-consumer.js")),
316
+ };
317
+ mock.module("../runtime/trust-verdict-consumer.js", () => ({
318
+ ...realTrustVerdictConsumerModule,
319
+ trustContextFromVerdict: (
320
+ ...args: Parameters<
321
+ typeof realTrustVerdictConsumerModule.trustContextFromVerdict
322
+ >
323
+ ) => {
324
+ trustVerdictMapperUsed = true;
325
+ return realTrustVerdictConsumerModule.trustContextFromVerdict(...args);
326
+ },
327
+ }));
328
+
166
329
  // ── TTS provider mocks (for call-speech-output) ─────────────────────
167
330
 
168
331
  let mockTtsProviderId: string = "elevenlabs";
@@ -278,7 +441,6 @@ import {
278
441
  } from "../calls/relay-server.js";
279
442
  import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
280
443
  import { upsertContact } from "../contacts/contact-store.js";
281
- import { upsertContactChannel } from "../contacts/contacts-write.js";
282
444
  import {
283
445
  listCanonicalGuardianRequests,
284
446
  resolveCanonicalGuardianRequest,
@@ -292,7 +454,11 @@ import { getDb } from "../memory/db-connection.js";
292
454
  import { initializeDb } from "../memory/db-init.js";
293
455
  import { createInvite } from "../memory/invite-store.js";
294
456
  import { resetTestTables } from "../memory/raw-query.js";
295
- import { conversations } from "../memory/schema.js";
457
+ import {
458
+ contactChannels,
459
+ contacts,
460
+ conversations,
461
+ } from "../memory/schema.js";
296
462
  import {
297
463
  createOutboundSession,
298
464
  getGuardianBinding,
@@ -300,6 +466,7 @@ import {
300
466
  import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
301
467
  import { resetDbForTesting } from "./db-test-helpers.js";
302
468
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
469
+ import { seedContactChannel } from "./helpers/seed-contact-channel.js";
303
470
 
304
471
  await initializeDb();
305
472
 
@@ -308,10 +475,12 @@ await initializeDb();
308
475
  // sibling files that load later in the same worker.
309
476
  beforeAll(() => {
310
477
  admissionMockActive = true;
478
+ inboundTrustMockActive = true;
311
479
  });
312
480
 
313
481
  afterAll(() => {
314
482
  admissionMockActive = false;
483
+ inboundTrustMockActive = false;
315
484
  resetDbForTesting();
316
485
  });
317
486
 
@@ -388,7 +557,7 @@ function createTargetContact(displayName = "Test Contact"): string {
388
557
  }
389
558
 
390
559
  function addTrustedVoiceContact(phoneNumber: string): void {
391
- upsertContactChannel({
560
+ seedContactChannel({
392
561
  sourceChannel: "phone",
393
562
  externalUserId: phoneNumber,
394
563
  externalChatId: phoneNumber,
@@ -477,8 +646,12 @@ describe("relay-server", () => {
477
646
  inviteClaimGate = null;
478
647
  mockUserReference = "my human";
479
648
  mockAssistantName = "Vellum";
649
+ mockGuardianDeliveryList = null;
480
650
  mockAdmissionPolicy = null;
481
651
  mockAdmissionGate = null;
652
+ mockMidCallVerdict = null;
653
+ mockMidCallVerdictGate = null;
654
+ trustVerdictMapperUsed = false;
482
655
  mockSendMessage.mockImplementation(createMockProviderResponse(["Hello"]));
483
656
  mockConfig.calls.verification.enabled = false;
484
657
  mockConfig.calls.verification.maxAttempts = 3;
@@ -1630,7 +1803,7 @@ describe("relay-server", () => {
1630
1803
  // Guardian binding is NOT created by the assistant — the gateway owns
1631
1804
  // binding creation for inbound voice verification. The assistant only
1632
1805
  // transitions to connected state and starts the normal call flow.
1633
- const binding = getGuardianBinding("self", "phone");
1806
+ const binding = await getGuardianBinding("self", "phone");
1634
1807
  expect(binding).toBeNull();
1635
1808
 
1636
1809
  // Orchestrator greeting should have fired
@@ -1701,7 +1874,7 @@ describe("relay-server", () => {
1701
1874
  expect(relay.getConnectionState()).toBe("connected");
1702
1875
 
1703
1876
  // Binding is NOT created by the assistant — gateway owns this.
1704
- const binding = getGuardianBinding("self", "phone");
1877
+ const binding = await getGuardianBinding("self", "phone");
1705
1878
  expect(binding).toBeNull();
1706
1879
 
1707
1880
  // Greeting should have started
@@ -2223,6 +2396,9 @@ describe("relay-server", () => {
2223
2396
  await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
2224
2397
  }
2225
2398
 
2399
+ // Let the fire-and-forget verification result handler flush
2400
+ await new Promise((resolve) => setTimeout(resolve, 10));
2401
+
2226
2402
  // Verification should have succeeded
2227
2403
  expect(relay.isVerificationSessionActive()).toBe(false);
2228
2404
 
@@ -2692,7 +2868,7 @@ describe("relay-server", () => {
2692
2868
  toNumber: "+15551111111",
2693
2869
  });
2694
2870
 
2695
- upsertContactChannel({
2871
+ seedContactChannel({
2696
2872
  sourceChannel: "phone",
2697
2873
  externalUserId: "+15558886666",
2698
2874
  externalChatId: "+15558886666",
@@ -2755,7 +2931,7 @@ describe("relay-server", () => {
2755
2931
  toNumber: "+15551111111",
2756
2932
  });
2757
2933
 
2758
- upsertContactChannel({
2934
+ seedContactChannel({
2759
2935
  sourceChannel: "phone",
2760
2936
  externalUserId: "+15558887777",
2761
2937
  externalChatId: "+15558887777",
@@ -3054,7 +3230,7 @@ describe("relay-server", () => {
3054
3230
  });
3055
3231
 
3056
3232
  // Create a blocked member
3057
- upsertContactChannel({
3233
+ seedContactChannel({
3058
3234
  sourceChannel: "phone",
3059
3235
  externalUserId: "+15558881111",
3060
3236
  externalChatId: "+15558881111",
@@ -4672,7 +4848,7 @@ describe("relay-server", () => {
4672
4848
  expect(relay.getConnectionState()).toBe("connected");
4673
4849
 
4674
4850
  // Guardian binding is NOT created by the assistant — gateway owns this.
4675
- const binding = getGuardianBinding("self", "phone");
4851
+ const binding = await getGuardianBinding("self", "phone");
4676
4852
  expect(binding).toBeNull();
4677
4853
 
4678
4854
  // Normal greeting should fire (from mockSendMessage), not the handoff copy
@@ -4950,15 +5126,10 @@ describe("relay-server", () => {
4950
5126
  test("guardian label: guardian persona name takes precedence over Contact.displayName", async () => {
4951
5127
  mockUserReference = "Alice";
4952
5128
 
4953
- // Create a guardian binding with a different displayName
4954
- createGuardianBinding({
4955
- channel: "phone",
4956
- guardianExternalUserId: "+15559990001",
4957
- guardianDeliveryChatId: "+15559990001",
4958
- guardianPrincipalId: "+15559990001",
4959
- verifiedVia: "test",
4960
- metadataJson: JSON.stringify({ displayName: "Bob" }),
4961
- });
5129
+ // Gateway binding carries a different displayName
5130
+ mockGuardianDeliveryList = [
5131
+ { channelType: "phone", status: "active", address: "+15550000001", displayName: "Bob" },
5132
+ ];
4962
5133
 
4963
5134
  ensureConversation("conv-label-user-md");
4964
5135
  const session = createCallSession({
@@ -4995,15 +5166,10 @@ describe("relay-server", () => {
4995
5166
  test("guardian label: Contact.displayName used when guardian persona name is empty", async () => {
4996
5167
  mockUserReference = "my human";
4997
5168
 
4998
- // Create a guardian binding with a displayName
4999
- createGuardianBinding({
5000
- channel: "phone",
5001
- guardianExternalUserId: "+15559990002",
5002
- guardianDeliveryChatId: "+15559990002",
5003
- guardianPrincipalId: "+15559990002",
5004
- verifiedVia: "test",
5005
- metadataJson: JSON.stringify({ displayName: "Charlie" }),
5006
- });
5169
+ // Gateway binding carries the guardian displayName
5170
+ mockGuardianDeliveryList = [
5171
+ { channelType: "phone", status: "active", address: "+15550000002", displayName: "Charlie" },
5172
+ ];
5007
5173
 
5008
5174
  ensureConversation("conv-label-contact");
5009
5175
  const session = createCallSession({
@@ -5039,10 +5205,8 @@ describe("relay-server", () => {
5039
5205
  test("guardian label: DEFAULT_USER_REFERENCE used when both guardian persona name and Contact.displayName are empty", async () => {
5040
5206
  mockUserReference = "my human";
5041
5207
 
5042
- // Clear guardian binding so resolveGuardianLabel falls back to DEFAULT_USER_REFERENCE
5043
- const db = getDb();
5044
- db.run("DELETE FROM contact_channels");
5045
- db.run("DELETE FROM contacts");
5208
+ // Empty binding list so resolveGuardianLabel falls back to DEFAULT_USER_REFERENCE
5209
+ mockGuardianDeliveryList = [];
5046
5210
 
5047
5211
  ensureConversation("conv-label-default");
5048
5212
  const session = createCallSession({
@@ -5440,4 +5604,566 @@ describe("relay-server", () => {
5440
5604
  relay.destroy();
5441
5605
  });
5442
5606
  });
5607
+
5608
+ // ── Mid-call trust re-resolution from the gateway verdict ───────────
5609
+ //
5610
+ // After a verification/activation success the relay re-resolves caller trust.
5611
+ // It prefers the gateway verdict (authoritative right after the gateway
5612
+ // updated the binding) and falls back to local resolution on a missing/
5613
+ // failed/unusable verdict so a blip never drops the call.
5614
+
5615
+ function readControllerTrustClass(relay: RelayConnection): string | undefined {
5616
+ return (
5617
+ relay.getController() as unknown as {
5618
+ trustContext?: { trustClass?: string };
5619
+ }
5620
+ )?.trustContext?.trustClass;
5621
+ }
5622
+
5623
+ test("inbound guardian verification: re-resolves trust from the gateway verdict", async () => {
5624
+ ensureConversation("conv-midcall-verdict-guardian");
5625
+ const session = createCallSession({
5626
+ conversationId: "conv-midcall-verdict-guardian",
5627
+ provider: "twilio",
5628
+ fromNumber: "+15559999999",
5629
+ toNumber: "+15551111111",
5630
+ });
5631
+
5632
+ const secret = createPendingVoiceGuardianChallenge();
5633
+
5634
+ // The gateway verdict upgrades the caller to guardian post-verification.
5635
+ mockMidCallVerdict = {
5636
+ trustClass: "guardian",
5637
+ canonicalSenderId: "+15559999999",
5638
+ guardianExternalUserId: "+15559999999",
5639
+ guardianPrincipalId: "+15559999999",
5640
+ };
5641
+
5642
+ mockSendMessage.mockImplementation(
5643
+ createMockProviderResponse(["Hello, verified guardian!"]),
5644
+ );
5645
+
5646
+ const { relay } = createMockWs(session.id);
5647
+
5648
+ await relay.handleMessage(
5649
+ JSON.stringify({
5650
+ type: "setup",
5651
+ callSid: "CA_midcall_verdict_guardian",
5652
+ from: "+15559999999",
5653
+ to: "+15551111111",
5654
+ }),
5655
+ );
5656
+
5657
+ for (const digit of secret) {
5658
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5659
+ }
5660
+ await new Promise((resolve) => setTimeout(resolve, 10));
5661
+
5662
+ expect(relay.getConnectionState()).toBe("connected");
5663
+ // Controller trust reflects the gateway verdict's upgraded class.
5664
+ expect(readControllerTrustClass(relay)).toBe("guardian");
5665
+
5666
+ relay.destroy();
5667
+ });
5668
+
5669
+ test("inbound guardian verification: resolutionFailed verdict falls back to local resolution without dropping the call", async () => {
5670
+ ensureConversation("conv-midcall-verdict-failed");
5671
+ const session = createCallSession({
5672
+ conversationId: "conv-midcall-verdict-failed",
5673
+ provider: "twilio",
5674
+ fromNumber: "+15559999999",
5675
+ toNumber: "+15551111111",
5676
+ });
5677
+
5678
+ const secret = createPendingVoiceGuardianChallenge();
5679
+
5680
+ // A failed verdict must fall back to local resolution — the call stays up.
5681
+ mockMidCallVerdict = {
5682
+ trustClass: "unknown",
5683
+ canonicalSenderId: null,
5684
+ resolutionFailed: true,
5685
+ };
5686
+
5687
+ mockSendMessage.mockImplementation(
5688
+ createMockProviderResponse(["Hello, how can I help you?"]),
5689
+ );
5690
+
5691
+ const { relay } = createMockWs(session.id);
5692
+
5693
+ await relay.handleMessage(
5694
+ JSON.stringify({
5695
+ type: "setup",
5696
+ callSid: "CA_midcall_verdict_failed",
5697
+ from: "+15559999999",
5698
+ to: "+15551111111",
5699
+ }),
5700
+ );
5701
+
5702
+ for (const digit of secret) {
5703
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5704
+ }
5705
+ await new Promise((resolve) => setTimeout(resolve, 10));
5706
+
5707
+ // Fail-soft: verification still completes and the call connects.
5708
+ expect(relay.isVerificationSessionActive()).toBe(false);
5709
+ expect(relay.getConnectionState()).toBe("connected");
5710
+ expect(readControllerTrustClass(relay)).toBeDefined();
5711
+
5712
+ relay.destroy();
5713
+ });
5714
+
5715
+ test("inbound guardian verification: member-claiming but unusable verdict falls back to local resolution", async () => {
5716
+ ensureConversation("conv-midcall-verdict-unusable");
5717
+ const session = createCallSession({
5718
+ conversationId: "conv-midcall-verdict-unusable",
5719
+ provider: "twilio",
5720
+ fromNumber: "+15559999999",
5721
+ toNumber: "+15551111111",
5722
+ });
5723
+
5724
+ const secret = createPendingVoiceGuardianChallenge();
5725
+
5726
+ // Claims a member (contactId/channelId) but the ACL can't be reassembled
5727
+ // (missing status/policy) — mirrors the setup path's unusable condition.
5728
+ mockMidCallVerdict = {
5729
+ trustClass: "trusted_contact",
5730
+ canonicalSenderId: "+15559999999",
5731
+ contactId: "ct_unusable",
5732
+ channelId: "ch_unusable",
5733
+ };
5734
+
5735
+ mockSendMessage.mockImplementation(
5736
+ createMockProviderResponse(["Hello there."]),
5737
+ );
5738
+
5739
+ const { relay } = createMockWs(session.id);
5740
+
5741
+ await relay.handleMessage(
5742
+ JSON.stringify({
5743
+ type: "setup",
5744
+ callSid: "CA_midcall_verdict_unusable",
5745
+ from: "+15559999999",
5746
+ to: "+15551111111",
5747
+ }),
5748
+ );
5749
+
5750
+ for (const digit of secret) {
5751
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5752
+ }
5753
+ await new Promise((resolve) => setTimeout(resolve, 10));
5754
+
5755
+ // Fail-soft: unusable verdict does not drop the call; local fallback fires.
5756
+ expect(relay.getConnectionState()).toBe("connected");
5757
+ expect(readControllerTrustClass(relay)).toBeDefined();
5758
+
5759
+ relay.destroy();
5760
+ });
5761
+
5762
+ test("inbound guardian verification: memberless unknown verdict falls back to local resolution (just-activated invitee not downgraded)", async () => {
5763
+ ensureConversation("conv-midcall-verdict-unknown");
5764
+ const session = createCallSession({
5765
+ conversationId: "conv-midcall-verdict-unknown",
5766
+ provider: "twilio",
5767
+ fromNumber: "+15559999999",
5768
+ toNumber: "+15551111111",
5769
+ });
5770
+
5771
+ const secret = createPendingVoiceGuardianChallenge();
5772
+
5773
+ // Invite redemption writes the channel assistant-side, so right after
5774
+ // activation the gateway has no member and returns a memberless unknown
5775
+ // verdict. Mid-call re-resolution must treat it as a stale gateway view
5776
+ // and fall back to local resolution (which has the fresh channel) rather
5777
+ // than downgrade the just-activated invitee to unknown.
5778
+ mockMidCallVerdict = {
5779
+ trustClass: "unknown",
5780
+ canonicalSenderId: "+15559999999",
5781
+ };
5782
+
5783
+ mockSendMessage.mockImplementation(
5784
+ createMockProviderResponse(["Hello there."]),
5785
+ );
5786
+
5787
+ const { relay } = createMockWs(session.id);
5788
+
5789
+ await relay.handleMessage(
5790
+ JSON.stringify({
5791
+ type: "setup",
5792
+ callSid: "CA_midcall_verdict_unknown",
5793
+ from: "+15559999999",
5794
+ to: "+15551111111",
5795
+ }),
5796
+ );
5797
+
5798
+ for (const digit of secret) {
5799
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5800
+ }
5801
+ await new Promise((resolve) => setTimeout(resolve, 10));
5802
+
5803
+ expect(relay.getConnectionState()).toBe("connected");
5804
+ // Local resolver produced the final context; the unknown verdict was not
5805
+ // consumed as authoritative.
5806
+ expect(trustVerdictMapperUsed).toBe(false);
5807
+ expect(readControllerTrustClass(relay)).toBeDefined();
5808
+
5809
+ relay.destroy();
5810
+ });
5811
+
5812
+ function readControllerMemberStatus(
5813
+ relay: RelayConnection,
5814
+ ): string | undefined {
5815
+ return (
5816
+ relay.getController() as unknown as {
5817
+ trustContext?: { memberStatus?: string };
5818
+ }
5819
+ )?.trustContext?.memberStatus;
5820
+ }
5821
+
5822
+ test("inbound guardian verification: memberful blocked unknown verdict is honored (verdict path enforces blocked status)", async () => {
5823
+ ensureConversation("conv-midcall-verdict-blocked");
5824
+ const session = createCallSession({
5825
+ conversationId: "conv-midcall-verdict-blocked",
5826
+ provider: "twilio",
5827
+ fromNumber: "+15559999999",
5828
+ toNumber: "+15551111111",
5829
+ });
5830
+
5831
+ const secret = createPendingVoiceGuardianChallenge();
5832
+
5833
+ mockSendMessage.mockImplementation(
5834
+ createMockProviderResponse(["Hello there."]),
5835
+ );
5836
+
5837
+ const { relay } = createMockWs(session.id);
5838
+
5839
+ // Setup resolves locally (no verdict) so the pending guardian challenge
5840
+ // drives verification rather than denying at the door.
5841
+ await relay.handleMessage(
5842
+ JSON.stringify({
5843
+ type: "setup",
5844
+ callSid: "CA_midcall_verdict_blocked",
5845
+ from: "+15559999999",
5846
+ to: "+15551111111",
5847
+ }),
5848
+ );
5849
+
5850
+ // The gateway classifies a blocked member as trustClass "unknown" but still
5851
+ // carries contactId/channelId and the deny ACL. This memberful unknown must
5852
+ // take the verdict path on mid-call re-resolution so its blocked status is
5853
+ // enforced — not fall back to local, which could miss a stale block.
5854
+ mockMidCallVerdict = {
5855
+ trustClass: "unknown",
5856
+ canonicalSenderId: "+15559999999",
5857
+ contactId: "ct_blocked",
5858
+ channelId: "ch_blocked",
5859
+ status: "blocked",
5860
+ policy: "deny",
5861
+ };
5862
+
5863
+ for (const digit of secret) {
5864
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5865
+ }
5866
+ await new Promise((resolve) => setTimeout(resolve, 10));
5867
+
5868
+ expect(relay.getConnectionState()).toBe("connected");
5869
+ // Verdict path consumed the memberful unknown verdict; blocked status lands.
5870
+ expect(trustVerdictMapperUsed).toBe(true);
5871
+ expect(readControllerMemberStatus(relay)).toBe("blocked");
5872
+
5873
+ relay.destroy();
5874
+ });
5875
+
5876
+ test("inbound guardian verification: memberful revoked unknown verdict is honored (verdict path enforces revoked status)", async () => {
5877
+ ensureConversation("conv-midcall-verdict-revoked");
5878
+ const session = createCallSession({
5879
+ conversationId: "conv-midcall-verdict-revoked",
5880
+ provider: "twilio",
5881
+ fromNumber: "+15559999999",
5882
+ toNumber: "+15551111111",
5883
+ });
5884
+
5885
+ const secret = createPendingVoiceGuardianChallenge();
5886
+
5887
+ mockSendMessage.mockImplementation(
5888
+ createMockProviderResponse(["Hello there."]),
5889
+ );
5890
+
5891
+ const { relay } = createMockWs(session.id);
5892
+
5893
+ await relay.handleMessage(
5894
+ JSON.stringify({
5895
+ type: "setup",
5896
+ callSid: "CA_midcall_verdict_revoked",
5897
+ from: "+15559999999",
5898
+ to: "+15551111111",
5899
+ }),
5900
+ );
5901
+
5902
+ mockMidCallVerdict = {
5903
+ trustClass: "unknown",
5904
+ canonicalSenderId: "+15559999999",
5905
+ contactId: "ct_revoked",
5906
+ channelId: "ch_revoked",
5907
+ status: "revoked",
5908
+ policy: "deny",
5909
+ };
5910
+
5911
+ for (const digit of secret) {
5912
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5913
+ }
5914
+ await new Promise((resolve) => setTimeout(resolve, 10));
5915
+
5916
+ expect(relay.getConnectionState()).toBe("connected");
5917
+ expect(trustVerdictMapperUsed).toBe(true);
5918
+ expect(readControllerMemberStatus(relay)).toBe("revoked");
5919
+
5920
+ relay.destroy();
5921
+ });
5922
+
5923
+ test("a prompt arriving during the mid-call re-resolution await is deferred and runs under the upgraded trust", async () => {
5924
+ ensureConversation("conv-midcall-race");
5925
+ const session = createCallSession({
5926
+ conversationId: "conv-midcall-race",
5927
+ provider: "twilio",
5928
+ fromNumber: "+15559999999",
5929
+ toNumber: "+15551111111",
5930
+ });
5931
+
5932
+ const secret = createPendingVoiceGuardianChallenge();
5933
+
5934
+ // Capture the controller's trust class at the moment a turn actually fires,
5935
+ // so we can prove the deferred prompt did not run under the stale context.
5936
+ const trustClassAtTurn: Array<string | undefined> = [];
5937
+ mockSendMessage.mockImplementation((...args: unknown[]) => {
5938
+ trustClassAtTurn.push(readControllerTrustClass(relay));
5939
+ return createMockProviderResponse(["Hello, verified guardian!"])(
5940
+ ...(args as Parameters<ReturnType<typeof createMockProviderResponse>>),
5941
+ );
5942
+ });
5943
+
5944
+ const { relay } = createMockWs(session.id);
5945
+
5946
+ // Setup resolves locally (no verdict) so the pending challenge drives
5947
+ // verification; the gated guardian verdict is armed only for the mid-call
5948
+ // re-resolution below.
5949
+ await relay.handleMessage(
5950
+ JSON.stringify({
5951
+ type: "setup",
5952
+ callSid: "CA_midcall_race",
5953
+ from: "+15559999999",
5954
+ to: "+15551111111",
5955
+ }),
5956
+ );
5957
+
5958
+ // The gateway verdict upgrades the caller to guardian, but the round-trip is
5959
+ // slow — gate it so a prompt can land in the re-resolution await window.
5960
+ mockMidCallVerdict = {
5961
+ trustClass: "guardian",
5962
+ canonicalSenderId: "+15559999999",
5963
+ guardianExternalUserId: "+15559999999",
5964
+ guardianPrincipalId: "+15559999999",
5965
+ };
5966
+ let releaseVerdict!: () => void;
5967
+ mockMidCallVerdictGate = new Promise<void>((resolve) => {
5968
+ releaseVerdict = resolve;
5969
+ });
5970
+
5971
+ // Enter the gated re-resolution: the final DTMF digit triggers
5972
+ // handleVerificationCodeResult, which awaits the slow verdict.
5973
+ const digits = secret.split("");
5974
+ for (const digit of digits.slice(0, -1)) {
5975
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
5976
+ }
5977
+ const verificationDone = relay.handleMessage(
5978
+ JSON.stringify({ type: "dtmf", digit: digits[digits.length - 1] }),
5979
+ );
5980
+ // Let the verdict await begin (connectionState is now "connected", guard set).
5981
+ await new Promise((resolve) => setTimeout(resolve, 0));
5982
+ expect(relay.getConnectionState()).toBe("connected");
5983
+ // Trust is still stale (verdict gated) — caller is not yet guardian.
5984
+ expect(readControllerTrustClass(relay)).not.toBe("guardian");
5985
+
5986
+ // Prompt arrives mid-await: it must be deferred, not processed under stale trust.
5987
+ await relay.handleMessage(
5988
+ JSON.stringify({
5989
+ type: "prompt",
5990
+ voicePrompt: "Are my appointments confirmed?",
5991
+ lang: "en-US",
5992
+ last: true,
5993
+ }),
5994
+ );
5995
+ // No turn yet — the prompt was buffered, not run under the stale context.
5996
+ expect(trustClassAtTurn).toHaveLength(0);
5997
+
5998
+ // Release the verdict; re-resolution installs the upgraded context, then the
5999
+ // deferred prompt is flushed and its turn runs under guardian trust.
6000
+ releaseVerdict();
6001
+ await verificationDone;
6002
+ await new Promise((resolve) => setTimeout(resolve, 20));
6003
+
6004
+ expect(readControllerTrustClass(relay)).toBe("guardian");
6005
+ expect(trustClassAtTurn.length).toBeGreaterThan(0);
6006
+ expect(trustClassAtTurn.every((c) => c === "guardian")).toBe(true);
6007
+
6008
+ relay.destroy();
6009
+ });
6010
+
6011
+ test("invite redemption: a prompt buffered during re-resolution runs as a real turn after activation (not dropped)", async () => {
6012
+ ensureConversation("conv-midcall-invite-flush");
6013
+ const session = createCallSession({
6014
+ conversationId: "conv-midcall-invite-flush",
6015
+ provider: "twilio",
6016
+ fromNumber: "+15558887777",
6017
+ toNumber: "+15551111111",
6018
+ });
6019
+
6020
+ const code = generateVoiceCode(6);
6021
+ createInvite({
6022
+ sourceChannel: "phone",
6023
+ contactId: createTargetContact("Alice"),
6024
+ maxUses: 1,
6025
+ expectedExternalUserId: "+15558887777",
6026
+ voiceCodeHash: hashVoiceCode(code),
6027
+ voiceCodeDigits: 6,
6028
+ });
6029
+
6030
+ const turnCountBefore = mockSendMessage.mock.calls.length;
6031
+ mockSendMessage.mockImplementation(
6032
+ createMockProviderResponse(["Sure, here you go."]),
6033
+ );
6034
+
6035
+ const { relay } = createMockWs(session.id);
6036
+
6037
+ await relay.handleMessage(
6038
+ JSON.stringify({
6039
+ type: "setup",
6040
+ callSid: "CA_midcall_invite_flush",
6041
+ from: "+15558887777",
6042
+ to: "+15551111111",
6043
+ }),
6044
+ );
6045
+ expect(relay.getConnectionState()).toBe("verification_pending");
6046
+
6047
+ // Gate the mid-call verdict so the prompt lands inside the re-resolution
6048
+ // await, after activation flips connectionState to "connected".
6049
+ let releaseVerdict!: () => void;
6050
+ mockMidCallVerdictGate = new Promise<void>((resolve) => {
6051
+ releaseVerdict = resolve;
6052
+ });
6053
+
6054
+ const digits = code.split("");
6055
+ for (const digit of digits.slice(0, -1)) {
6056
+ await relay.handleMessage(JSON.stringify({ type: "dtmf", digit }));
6057
+ }
6058
+ const redemptionDone = relay.handleMessage(
6059
+ JSON.stringify({ type: "dtmf", digit: digits[digits.length - 1] }),
6060
+ );
6061
+ // Let the redemption chain (gateway claim, caller-verdict read, DB write)
6062
+ // drain up to activation, which flips the state to "connected" before
6063
+ // entering the gated re-resolution.
6064
+ await new Promise((resolve) => setTimeout(resolve, 50));
6065
+ // Activation already reached the terminal state before re-resolution.
6066
+ expect(relay.getConnectionState()).toBe("connected");
6067
+
6068
+ await relay.handleMessage(
6069
+ JSON.stringify({
6070
+ type: "prompt",
6071
+ voicePrompt: "What's on my calendar today?",
6072
+ lang: "en-US",
6073
+ last: true,
6074
+ }),
6075
+ );
6076
+ // Buffered, not yet run (verdict gated).
6077
+ expect(mockSendMessage.mock.calls.length).toBe(turnCountBefore);
6078
+
6079
+ releaseVerdict();
6080
+ await redemptionDone;
6081
+ await new Promise((resolve) => setTimeout(resolve, 20));
6082
+
6083
+ // Flushed onto the real-turn path: the prompt produced an LLM turn rather
6084
+ // than being dropped by the verification-pending branch.
6085
+ expect(mockSendMessage.mock.calls.length).toBeGreaterThan(turnCountBefore);
6086
+
6087
+ relay.destroy();
6088
+ });
6089
+
6090
+ test("access-request approval: a prompt buffered during re-resolution runs as a real turn (not misrouted to wait-state)", async () => {
6091
+ ensureConversation("conv-midcall-access-flush");
6092
+ const session = createCallSession({
6093
+ conversationId: "conv-midcall-access-flush",
6094
+ provider: "twilio",
6095
+ fromNumber: "+15557770003",
6096
+ toNumber: "+15551111111",
6097
+ });
6098
+
6099
+ const turnCountBefore = mockSendMessage.mock.calls.length;
6100
+ mockSendMessage.mockImplementation(
6101
+ createMockProviderResponse(["Sure, here you go."]),
6102
+ );
6103
+
6104
+ const { relay } = createMockWs(session.id);
6105
+
6106
+ await relay.handleMessage(
6107
+ JSON.stringify({
6108
+ type: "setup",
6109
+ callSid: "CA_midcall_access_flush",
6110
+ from: "+15557770003",
6111
+ to: "+15551111111",
6112
+ }),
6113
+ );
6114
+
6115
+ await relay.handleMessage(
6116
+ JSON.stringify({
6117
+ type: "prompt",
6118
+ voicePrompt: "Bob Smith",
6119
+ lang: "en-US",
6120
+ last: true,
6121
+ }),
6122
+ );
6123
+ expect(relay.getConnectionState()).toBe("awaiting_guardian_decision");
6124
+
6125
+ // Gate the mid-call verdict so the prompt lands inside the re-resolution
6126
+ // await triggered by the approval poll.
6127
+ let releaseVerdict!: () => void;
6128
+ mockMidCallVerdictGate = new Promise<void>((resolve) => {
6129
+ releaseVerdict = resolve;
6130
+ });
6131
+
6132
+ const pending = listCanonicalGuardianRequests({
6133
+ status: "pending",
6134
+ requesterExternalUserId: "+15557770003",
6135
+ sourceChannel: "phone",
6136
+ kind: "access_request",
6137
+ });
6138
+ expect(pending.length).toBe(1);
6139
+ resolveCanonicalGuardianRequest(pending[0].id, "pending", {
6140
+ status: "approved",
6141
+ answerText: undefined,
6142
+ decidedByExternalUserId: undefined,
6143
+ });
6144
+
6145
+ // Let the poll detect approval and enter the gated re-resolution.
6146
+ await new Promise((resolve) => setTimeout(resolve, 200));
6147
+ expect(relay.getConnectionState()).toBe("connected");
6148
+
6149
+ await relay.handleMessage(
6150
+ JSON.stringify({
6151
+ type: "prompt",
6152
+ voicePrompt: "What's on my calendar today?",
6153
+ lang: "en-US",
6154
+ last: true,
6155
+ }),
6156
+ );
6157
+ // Buffered, not yet run (verdict gated).
6158
+ expect(mockSendMessage.mock.calls.length).toBe(turnCountBefore);
6159
+
6160
+ releaseVerdict();
6161
+ await new Promise((resolve) => setTimeout(resolve, 20));
6162
+
6163
+ // Flushed onto the real-turn path rather than the awaiting-guardian-decision
6164
+ // wait-state classifier: the prompt produced an LLM turn.
6165
+ expect(mockSendMessage.mock.calls.length).toBeGreaterThan(turnCountBefore);
6166
+
6167
+ relay.destroy();
6168
+ });
5443
6169
  });