@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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 (866) hide show
  1. package/ARCHITECTURE.md +94 -91
  2. package/Dockerfile +5 -16
  3. package/README.md +7 -10
  4. package/bun.lock +0 -4
  5. package/docs/architecture/security.md +20 -20
  6. package/docs/stt-provider-onboarding.md +4 -5
  7. package/docs/trusted-contact-access.md +7 -7
  8. package/eslint-rules/cli-no-daemon-internals.js +4 -0
  9. package/eslint.config.mjs +7 -1
  10. package/knip.json +0 -1
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
  13. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
  14. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
  15. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
  17. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
  19. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
  21. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
  22. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
  23. package/openapi.yaml +1073 -225
  24. package/package.json +1 -4
  25. package/scripts/test.sh +35 -13
  26. package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
  27. package/src/__tests__/acp-session.test.ts +4 -1
  28. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
  29. package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
  30. package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
  31. package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
  32. package/src/__tests__/app-source-watcher.test.ts +11 -0
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  34. package/src/__tests__/assistant-attachments.test.ts +62 -4
  35. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
  36. package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
  37. package/src/__tests__/attachments-store.test.ts +3 -0
  38. package/src/__tests__/background-shell-bash.test.ts +34 -0
  39. package/src/__tests__/background-shell-host-bash.test.ts +12 -0
  40. package/src/__tests__/background-tool-registry.test.ts +97 -0
  41. package/src/__tests__/background-tool-routes.test.ts +72 -1
  42. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  43. package/src/__tests__/btw-routes.test.ts +57 -3
  44. package/src/__tests__/call-controller.test.ts +474 -84
  45. package/src/__tests__/call-domain.test.ts +48 -1
  46. package/src/__tests__/call-pointer-messages.test.ts +57 -19
  47. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
  48. package/src/__tests__/call-routes-http.test.ts +15 -14
  49. package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
  50. package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
  51. package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
  52. package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
  53. package/src/__tests__/call-setup-flow.test.ts +311 -0
  54. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
  55. package/src/__tests__/channel-guardian.test.ts +4 -7
  56. package/src/__tests__/channel-readiness-service.test.ts +0 -6
  57. package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
  58. package/src/__tests__/channel-retry-sweep.test.ts +76 -64
  59. package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
  60. package/src/__tests__/clear-all-lexical.test.ts +90 -0
  61. package/src/__tests__/compactor-media-strip.test.ts +177 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  63. package/src/__tests__/config-schema.test.ts +17 -0
  64. package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
  65. package/src/__tests__/contacts-relay-reads.test.ts +188 -23
  66. package/src/__tests__/contacts-tools.test.ts +42 -1
  67. package/src/__tests__/contacts-write.test.ts +181 -0
  68. package/src/__tests__/context-search-conversations-source.test.ts +220 -81
  69. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  71. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  72. package/src/__tests__/conversation-agent-loop.test.ts +39 -0
  73. package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
  74. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
  75. package/src/__tests__/conversation-clear-safety.test.ts +0 -1
  76. package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
  77. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
  78. package/src/__tests__/conversation-disk-view.test.ts +1 -0
  79. package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
  80. package/src/__tests__/conversation-error.test.ts +0 -18
  81. package/src/__tests__/conversation-fork-crud.test.ts +67 -0
  82. package/src/__tests__/conversation-history-web-search.test.ts +7 -137
  83. package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
  84. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
  85. package/src/__tests__/conversation-process-callsite.test.ts +37 -0
  86. package/src/__tests__/conversation-queue.test.ts +149 -0
  87. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
  88. package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
  89. package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
  90. package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
  91. package/src/__tests__/conversation-skill-tools.test.ts +73 -0
  92. package/src/__tests__/conversation-wipe.test.ts +20 -5
  93. package/src/__tests__/credential-prompt-route.test.ts +69 -0
  94. package/src/__tests__/credential-security-invariants.test.ts +1 -3
  95. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  96. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
  97. package/src/__tests__/edit-propagation.test.ts +56 -2
  98. package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
  99. package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
  100. package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
  101. package/src/__tests__/guardian-question-mode.test.ts +57 -0
  102. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
  103. package/src/__tests__/guardian-wait-controller.test.ts +619 -0
  104. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
  105. package/src/__tests__/heic-fixture.ts +55 -0
  106. package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
  107. package/src/__tests__/http-conversation-lineage.test.ts +6 -1
  108. package/src/__tests__/image-conversion.test.ts +170 -0
  109. package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
  110. package/src/__tests__/introduction-card-resolver.test.ts +354 -0
  111. package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
  112. package/src/__tests__/invite-routes-http.test.ts +246 -420
  113. package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
  114. package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
  115. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
  116. package/src/__tests__/list-messages-attachments.test.ts +159 -0
  117. package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
  118. package/src/__tests__/list-messages-queued.test.ts +25 -0
  119. package/src/__tests__/managed-store.test.ts +129 -1
  120. package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
  121. package/src/__tests__/media-stream-output.test.ts +350 -19
  122. package/src/__tests__/media-stream-server-integration.test.ts +943 -169
  123. package/src/__tests__/media-stream-stt-session.test.ts +498 -1
  124. package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
  125. package/src/__tests__/model-intents.test.ts +1 -1
  126. package/src/__tests__/mtime-cache.test.ts +19 -0
  127. package/src/__tests__/non-member-access-request.test.ts +349 -3
  128. package/src/__tests__/notification-decision-fallback.test.ts +104 -8
  129. package/src/__tests__/notification-decision-strategy.test.ts +57 -8
  130. package/src/__tests__/onboarding-template-contract.test.ts +27 -43
  131. package/src/__tests__/openai-provider.test.ts +33 -0
  132. package/src/__tests__/openai-responses-provider.test.ts +40 -0
  133. package/src/__tests__/persistence-layering-guard.test.ts +29 -20
  134. package/src/__tests__/persona-resolver.test.ts +56 -2
  135. package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
  136. package/src/__tests__/phone.test.ts +1 -15
  137. package/src/__tests__/plugin-bootstrap.test.ts +25 -196
  138. package/src/__tests__/plugin-disabled-state.test.ts +33 -0
  139. package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
  140. package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
  141. package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
  142. package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
  143. package/src/__tests__/plugin-pipeline.test.ts +63 -1
  144. package/src/__tests__/plugin-route-contribution.test.ts +62 -115
  145. package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
  146. package/src/__tests__/plugin-types.test.ts +0 -1
  147. package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
  148. package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
  149. package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
  150. package/src/__tests__/public-ingress-urls.test.ts +0 -48
  151. package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
  152. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
  153. package/src/__tests__/resolve-trust-class.test.ts +28 -10
  154. package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
  155. package/src/__tests__/schedule-retry.test.ts +38 -14
  156. package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
  157. package/src/__tests__/schedule-routes.test.ts +14 -6
  158. package/src/__tests__/schedule-store.test.ts +3 -1
  159. package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
  160. package/src/__tests__/scheduler-recurrence.test.ts +57 -107
  161. package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
  162. package/src/__tests__/scheduler-wake.test.ts +13 -23
  163. package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
  164. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  165. package/src/__tests__/shell-observability.test.ts +1 -0
  166. package/src/__tests__/skill-boundary-guard.test.ts +3 -4
  167. package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
  168. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
  169. package/src/__tests__/skill-script-runner-host.test.ts +53 -0
  170. package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
  171. package/src/__tests__/skill-tool-factory.test.ts +73 -1
  172. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
  173. package/src/__tests__/slack-inbound-verification.test.ts +36 -0
  174. package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
  175. package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
  176. package/src/__tests__/stt-hints.test.ts +2 -256
  177. package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
  178. package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
  179. package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
  180. package/src/__tests__/subagent-persistence.test.ts +122 -0
  181. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  182. package/src/__tests__/subagent-terminal-message.test.ts +99 -0
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
  184. package/src/__tests__/system-prompt.test.ts +17 -118
  185. package/src/__tests__/task-scheduler.test.ts +68 -89
  186. package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
  187. package/src/__tests__/telephony-tts-guard.test.ts +538 -0
  188. package/src/__tests__/title-generate-hook.test.ts +16 -0
  189. package/src/__tests__/tool-approval-handler.test.ts +87 -1
  190. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
  191. package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
  192. package/src/__tests__/trust-context-guards.test.ts +21 -10
  193. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
  194. package/src/__tests__/twilio-provider.test.ts +17 -17
  195. package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
  196. package/src/__tests__/twilio-routes.test.ts +257 -387
  197. package/src/__tests__/twilio-validation.test.ts +1 -1
  198. package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
  199. package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
  200. package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
  201. package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
  202. package/src/__tests__/voice-quality.test.ts +2 -2
  203. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
  204. package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
  205. package/src/__tests__/voice-session-bridge.test.ts +1 -10
  206. package/src/__tests__/workspace-git-service.test.ts +1 -1
  207. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
  208. package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
  209. package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
  210. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  211. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  212. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  213. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
  214. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
  215. package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
  216. package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
  217. package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
  218. package/src/acp/__tests__/session-manager.test.ts +6 -0
  219. package/src/acp/agent-process.test.ts +116 -0
  220. package/src/acp/agent-process.ts +71 -0
  221. package/src/acp/auto-install.ts +12 -8
  222. package/src/acp/failure-error.test.ts +109 -0
  223. package/src/acp/failure-error.ts +111 -0
  224. package/src/acp/prepare-agent-env.ts +0 -12
  225. package/src/acp/resolve-agent.test.ts +6 -45
  226. package/src/acp/resolve-agent.ts +2 -4
  227. package/src/acp/session-manager.test.ts +312 -0
  228. package/src/acp/session-manager.ts +102 -46
  229. package/src/agent/ax-tree-compaction.test.ts +4 -1
  230. package/src/agent/image-optimize.ts +17 -127
  231. package/src/agent/loop.ts +45 -192
  232. package/src/api/events/background-tool-completed.test.ts +51 -0
  233. package/src/api/events/background-tool-completed.ts +29 -0
  234. package/src/api/events/background-tool-started.test.ts +36 -0
  235. package/src/api/events/background-tool-started.ts +28 -0
  236. package/src/api/events/conversation-error.ts +0 -1
  237. package/src/api/events/open-panel.ts +33 -0
  238. package/src/api/events/subagent-status-changed.ts +4 -1
  239. package/src/api/index.ts +19 -0
  240. package/src/api/responses/conversation-message.ts +34 -0
  241. package/src/approvals/guardian-decision-primitive.ts +74 -24
  242. package/src/approvals/guardian-request-resolvers.ts +450 -122
  243. package/src/apps/app-store.ts +1 -0
  244. package/src/apps/shared-app-links-store.ts +1 -0
  245. package/src/avatar/identity-avatar.ts +3 -7
  246. package/src/background-wake/wake-intent-hooks.test.ts +1 -1
  247. package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
  248. package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
  249. package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
  250. package/src/calls/call-controller.ts +134 -48
  251. package/src/calls/call-domain.ts +18 -42
  252. package/src/calls/call-pointer-messages.ts +51 -67
  253. package/src/calls/call-setup-flow-types.ts +76 -0
  254. package/src/calls/call-setup-flow.ts +1743 -0
  255. package/src/calls/call-speech-output.ts +87 -23
  256. package/src/calls/call-store.ts +4 -0
  257. package/src/calls/call-transport.ts +22 -36
  258. package/src/calls/fish-audio-client.ts +3 -0
  259. package/src/calls/gateway-invite-reader.ts +98 -0
  260. package/src/calls/guardian-wait-controller.ts +451 -0
  261. package/src/calls/media-stream-audio-transcode.ts +82 -2
  262. package/src/calls/media-stream-output.ts +142 -36
  263. package/src/calls/media-stream-server.ts +297 -178
  264. package/src/calls/media-stream-stt-session.ts +393 -50
  265. package/src/calls/media-turn-detector.ts +4 -6
  266. package/src/calls/relay-access-wait.ts +3 -4
  267. package/src/calls/relay-setup-router.ts +22 -43
  268. package/src/calls/relay-verification.ts +17 -24
  269. package/src/calls/resolve-call-tts-provider.ts +96 -29
  270. package/src/calls/stt-hints.ts +24 -87
  271. package/src/calls/telephony-credential-preflight.ts +159 -0
  272. package/src/calls/telephony-tts-capability.ts +183 -0
  273. package/src/calls/tts-call-strategy.ts +18 -20
  274. package/src/calls/twilio-provider.ts +3 -2
  275. package/src/calls/twilio-routes.ts +47 -351
  276. package/src/calls/types.ts +5 -3
  277. package/src/calls/voice-ingress-preflight.ts +9 -0
  278. package/src/calls/voice-quality.ts +13 -18
  279. package/src/calls/voice-session-bridge.ts +25 -35
  280. package/src/channels/__tests__/types.test.ts +28 -0
  281. package/src/channels/channel-verification-sessions.ts +7 -2
  282. package/src/channels/config.ts +0 -42
  283. package/src/channels/slack-thread-store.ts +12 -0
  284. package/src/channels/types.ts +23 -0
  285. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  286. package/src/cli/commands/__tests__/schedules.test.ts +83 -34
  287. package/src/cli/commands/__tests__/status.test.ts +42 -2
  288. package/src/cli/commands/contacts.ts +2 -2
  289. package/src/cli/commands/credentials.ts +17 -0
  290. package/src/cli/commands/monitoring.ts +191 -0
  291. package/src/cli/commands/schedules-worker.ts +175 -0
  292. package/src/cli/commands/schedules.ts +169 -43
  293. package/src/cli/commands/status.ts +12 -1
  294. package/src/cli/commands/tools.ts +5 -1
  295. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
  296. package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
  297. package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
  298. package/src/cli/lib/search-plugins.ts +6 -0
  299. package/src/cli/lib/toggle-plugin.ts +21 -2
  300. package/src/cli/program.ts +3 -1
  301. package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
  302. package/src/config/acp-defaults.test.ts +0 -10
  303. package/src/config/acp-defaults.ts +0 -6
  304. package/src/config/bundled-skills/acp/SKILL.md +13 -45
  305. package/src/config/bundled-skills/acp/TOOLS.json +3 -3
  306. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  307. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
  308. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
  309. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
  310. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  311. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
  312. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
  313. package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
  314. package/src/config/call-site-defaults.ts +0 -2
  315. package/src/config/feature-flag-registry.json +16 -8
  316. package/src/config/loader.ts +11 -0
  317. package/src/config/schema.ts +9 -1
  318. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
  319. package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
  320. package/src/config/schemas/call-site-catalog.ts +0 -12
  321. package/src/config/schemas/calls.ts +8 -0
  322. package/src/config/schemas/elevenlabs.ts +1 -1
  323. package/src/config/schemas/llm.ts +0 -2
  324. package/src/config/schemas/memory-v2.ts +1 -1
  325. package/src/config/schemas/memory-v3.ts +82 -0
  326. package/src/config/schemas/monitoring.ts +61 -0
  327. package/src/config/schemas/schedules.ts +35 -0
  328. package/src/config/seed-inference-profiles.ts +2 -2
  329. package/src/config/skills-plugin-filter.test.ts +96 -0
  330. package/src/config/skills.ts +26 -0
  331. package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
  332. package/src/contacts/canonical-guardian-store.ts +5 -0
  333. package/src/contacts/contact-store.ts +81 -35
  334. package/src/contacts/contacts-write.ts +50 -25
  335. package/src/contacts/member-write-relay.ts +115 -31
  336. package/src/contacts/types.ts +0 -8
  337. package/src/context/compactor.ts +29 -15
  338. package/src/context/outbound-sanitize.ts +202 -0
  339. package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
  340. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
  341. package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
  342. package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
  343. package/src/daemon/abort-watchdog.ts +7 -0
  344. package/src/daemon/app-source-watcher.ts +9 -0
  345. package/src/daemon/assistant-attachments.ts +42 -0
  346. package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
  347. package/src/daemon/conversation-agent-loop.ts +61 -68
  348. package/src/daemon/conversation-error.ts +1 -15
  349. package/src/daemon/conversation-history.ts +32 -236
  350. package/src/daemon/conversation-initial-prompt.ts +77 -0
  351. package/src/daemon/conversation-lifecycle.ts +25 -1
  352. package/src/daemon/conversation-messaging.ts +2 -2
  353. package/src/daemon/conversation-plugin-scope.ts +38 -0
  354. package/src/daemon/conversation-process.ts +50 -14
  355. package/src/daemon/conversation-runtime-assembly.ts +36 -6
  356. package/src/daemon/conversation-skill-tools.ts +19 -3
  357. package/src/daemon/conversation-store.ts +3 -3
  358. package/src/daemon/conversation-surfaces.ts +171 -0
  359. package/src/daemon/conversation-tool-setup.test.ts +95 -0
  360. package/src/daemon/conversation-tool-setup.ts +116 -1
  361. package/src/daemon/conversation-turn-finalize.ts +211 -0
  362. package/src/daemon/conversation.ts +92 -1
  363. package/src/daemon/disk-pressure-policy.ts +2 -1
  364. package/src/daemon/event-loop-watchdog.ts +22 -6
  365. package/src/daemon/external-plugins-bootstrap.ts +47 -134
  366. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
  367. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
  368. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
  369. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
  370. package/src/daemon/handlers/config-a2a.ts +7 -14
  371. package/src/daemon/handlers/config-channels.ts +2 -10
  372. package/src/daemon/handlers/conversation-history.ts +3 -1
  373. package/src/daemon/handlers/conversations.ts +0 -52
  374. package/src/daemon/handlers/shared.ts +4 -24
  375. package/src/daemon/handlers/skills.ts +4 -1
  376. package/src/daemon/host-cu-proxy.ts +1 -1
  377. package/src/daemon/lifecycle.ts +59 -472
  378. package/src/daemon/message-protocol.ts +3 -3
  379. package/src/daemon/message-provenance.ts +12 -14
  380. package/src/daemon/message-types/background-tools.ts +28 -0
  381. package/src/daemon/message-types/contacts.ts +2 -10
  382. package/src/daemon/message-types/conversations.ts +10 -0
  383. package/src/daemon/message-types/integrations.ts +2 -0
  384. package/src/daemon/message-types/surfaces.ts +1 -0
  385. package/src/daemon/message-types/sync.ts +1 -0
  386. package/src/daemon/pointer-conversation-trust.ts +23 -9
  387. package/src/daemon/pointer-turn-runner.ts +174 -0
  388. package/src/daemon/providers-setup.ts +16 -0
  389. package/src/daemon/shutdown-handlers.ts +139 -47
  390. package/src/daemon/skill-memory-refresh.ts +2 -2
  391. package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
  392. package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
  393. package/src/daemon/tool-setup-types.ts +8 -0
  394. package/src/daemon/trust-context.ts +21 -6
  395. package/src/daemon/turn-latency-tracker.test.ts +93 -0
  396. package/src/daemon/turn-latency-tracker.ts +38 -0
  397. package/src/daemon/wake-conversation-ops.ts +10 -8
  398. package/src/documents/document-comments-store.test.ts +7 -1
  399. package/src/documents/document-comments-store.ts +8 -0
  400. package/src/documents/document-store.ts +15 -0
  401. package/src/export/__tests__/transcript-formatter.test.ts +11 -0
  402. package/src/export/transcript-formatter.ts +49 -67
  403. package/src/home/__tests__/feed-writer.test.ts +247 -1
  404. package/src/home/conversation-starter-checkpoints.ts +1 -0
  405. package/src/home/feed-writer.ts +61 -0
  406. package/src/home/job-handlers/conversation-starters.ts +1 -0
  407. package/src/hooks/hook-loader.ts +12 -0
  408. package/src/hooks/registry.ts +32 -4
  409. package/src/inbound/public-ingress-urls.ts +0 -17
  410. package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
  411. package/src/ipc/assistant-server.ts +28 -7
  412. package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
  413. package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
  414. package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
  415. package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
  416. package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
  417. package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
  418. package/src/ipc/routes/db-proxy.ts +5 -2
  419. package/src/ipc/routes/invite-ipc-routes.ts +53 -39
  420. package/src/jobs/worker.ts +9 -2
  421. package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
  422. package/src/live-voice/live-voice-archive.ts +5 -0
  423. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
  424. package/src/messaging/providers/channel-transport.ts +6 -0
  425. package/src/messaging/providers/index.ts +3 -0
  426. package/src/messaging/providers/slack/api.test.ts +49 -1
  427. package/src/messaging/providers/slack/api.ts +110 -0
  428. package/src/messaging/providers/slack/send.test.ts +85 -0
  429. package/src/messaging/providers/slack/send.ts +100 -28
  430. package/src/messaging/providers/slack/transport.ts +7 -13
  431. package/src/messaging/providers/whatsapp/send.ts +29 -9
  432. package/src/monitoring/__tests__/control.test.ts +235 -0
  433. package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
  434. package/src/monitoring/control.ts +105 -0
  435. package/src/monitoring/resource-sampler.ts +258 -0
  436. package/src/monitoring/sample-ring-buffer.ts +108 -0
  437. package/src/monitoring/worker.ts +94 -0
  438. package/src/notifications/README.md +1 -1
  439. package/src/notifications/access-request-copy.ts +94 -23
  440. package/src/notifications/adapters/slack.ts +89 -21
  441. package/src/notifications/approval-card-builder.ts +33 -18
  442. package/src/notifications/approval-card-data.ts +48 -15
  443. package/src/notifications/broadcaster.ts +25 -16
  444. package/src/notifications/decision-engine.ts +131 -32
  445. package/src/notifications/guardian-question-mode.ts +58 -10
  446. package/src/permissions/approval-policy.ts +8 -1
  447. package/src/permissions/checker.ts +2 -0
  448. package/src/permissions/gateway-threshold-reader.ts +7 -6
  449. package/src/permissions/ipc-risk-types.ts +1 -0
  450. package/src/permissions/secret-prompter.ts +11 -3
  451. package/src/permissions/types.ts +2 -2
  452. package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
  453. package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
  454. package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
  455. package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
  456. package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
  457. package/src/persistence/a2a-invite-store.ts +205 -0
  458. package/src/persistence/attachments-store.ts +94 -10
  459. package/src/persistence/checkpoints.ts +45 -0
  460. package/src/persistence/conversation-crud.ts +453 -260
  461. package/src/persistence/conversation-display-order-migration.ts +8 -2
  462. package/src/persistence/conversation-group-migration.test.ts +8 -0
  463. package/src/persistence/conversation-group-migration.ts +9 -0
  464. package/src/persistence/conversation-queries.ts +244 -125
  465. package/src/persistence/conversation-row-batch-delete.ts +9 -5
  466. package/src/persistence/conversation-search-lexical.ts +49 -0
  467. package/src/persistence/db-async-query.ts +42 -0
  468. package/src/persistence/db-connection.ts +13 -0
  469. package/src/persistence/delivery-channels.ts +0 -82
  470. package/src/persistence/delivery-crud.ts +27 -6
  471. package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
  472. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
  473. package/src/persistence/embeddings/embedding-identity.ts +17 -5
  474. package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
  475. package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
  476. package/src/persistence/embeddings/qdrant-client.ts +81 -5
  477. package/src/persistence/group-crud.ts +13 -1
  478. package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
  479. package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
  480. package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
  481. package/src/persistence/job-handlers/cleanup.ts +57 -9
  482. package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
  483. package/src/persistence/job-handlers/message-lexical.ts +251 -0
  484. package/src/persistence/jobs-store.ts +102 -8
  485. package/src/persistence/jobs-worker.ts +68 -28
  486. package/src/persistence/llm-request-log-store.ts +35 -19
  487. package/src/persistence/llm-usage-store.ts +11 -0
  488. package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
  489. package/src/persistence/memory-lifecycle-hooks.ts +55 -1
  490. package/src/persistence/migrations/028-call-session-mode.ts +1 -1
  491. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  492. package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
  493. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  494. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  495. package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
  496. package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
  497. package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
  498. package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
  499. package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
  500. package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
  501. package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
  502. package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
  503. package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
  504. package/src/persistence/raw-query.ts +50 -11
  505. package/src/persistence/schema/a2a.ts +14 -0
  506. package/src/persistence/schema/contacts.ts +0 -62
  507. package/src/persistence/schema/conversation-groups.ts +12 -0
  508. package/src/persistence/schema/conversations.ts +2 -0
  509. package/src/persistence/schema/documents.ts +65 -0
  510. package/src/persistence/schema/guardian.ts +4 -0
  511. package/src/persistence/schema/index.ts +4 -0
  512. package/src/persistence/schema/memory-injection.ts +62 -0
  513. package/src/persistence/schema/workflows.ts +55 -0
  514. package/src/persistence/slow-query-log.ts +437 -0
  515. package/src/persistence/slow-sync-log.ts +204 -0
  516. package/src/persistence/steps.ts +16 -0
  517. package/src/persistence/subagent-store.ts +125 -0
  518. package/src/persistence/worker-control.ts +45 -238
  519. package/src/plugin-api/event-hub-facade.ts +172 -0
  520. package/src/plugin-api/index.ts +9 -1
  521. package/src/plugin-api/types.ts +40 -9
  522. package/src/plugins/defaults/default-plugin-names.ts +60 -0
  523. package/src/plugins/defaults/index.ts +19 -29
  524. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
  525. package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
  526. package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
  527. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
  528. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
  530. package/src/plugins/defaults/memory/config.ts +11 -0
  531. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
  532. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
  533. package/src/plugins/defaults/memory/embeddings.ts +77 -0
  534. package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
  535. package/src/plugins/defaults/memory/frontmatter.ts +50 -0
  536. package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
  537. package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
  538. package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
  539. package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
  540. package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
  541. package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
  542. package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
  543. package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
  544. package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
  545. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  546. package/src/plugins/defaults/memory/hooks/init.ts +31 -5
  547. package/src/plugins/defaults/memory/indexer.ts +2 -3
  548. package/src/plugins/defaults/memory/injectors.ts +2 -2
  549. package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
  550. package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
  551. package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
  552. package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
  553. package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
  554. package/src/plugins/defaults/memory/job-handlers.ts +13 -12
  555. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
  556. package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
  557. package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
  558. package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
  559. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
  560. package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
  561. package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
  562. package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
  563. package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
  564. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
  565. package/src/plugins/defaults/memory/search/semantic.ts +2 -2
  566. package/src/plugins/defaults/memory/startup.ts +244 -0
  567. package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
  568. package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
  570. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
  571. package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
  572. package/src/plugins/defaults/memory/v2/activation.ts +2 -4
  573. package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
  574. package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
  575. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
  576. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
  577. package/src/plugins/defaults/memory/v2/migration.ts +2 -5
  578. package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
  579. package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
  580. package/src/plugins/defaults/memory/v2/router.ts +5 -5
  581. package/src/plugins/defaults/memory/v2/sim.ts +2 -4
  582. package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
  583. package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
  584. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
  585. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
  586. package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
  587. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
  588. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
  589. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
  590. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
  591. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
  592. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
  593. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
  594. package/src/plugins/defaults/memory/v3/card.ts +1 -4
  595. package/src/plugins/defaults/memory/v3/dense.ts +60 -25
  596. package/src/plugins/defaults/memory/v3/edge.ts +1 -1
  597. package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
  598. package/src/plugins/defaults/memory/v3/gate.ts +158 -0
  599. package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
  600. package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
  601. package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
  602. package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
  603. package/src/plugins/defaults/memory/v3/prune.ts +2 -2
  604. package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
  605. package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
  606. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
  607. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
  608. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
  609. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  610. package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
  611. package/src/plugins/injector-registry.ts +13 -2
  612. package/src/plugins/mtime-cache.ts +36 -47
  613. package/src/plugins/pipeline.ts +15 -1
  614. package/src/plugins/types.ts +14 -23
  615. package/src/prompts/__tests__/system-prompt.test.ts +170 -32
  616. package/src/prompts/persona-resolver.ts +13 -9
  617. package/src/prompts/system-prompt.ts +30 -9
  618. package/src/prompts/templates/BOOTSTRAP.md +16 -119
  619. package/src/prompts/templates/SOUL.md +43 -13
  620. package/src/prompts/templates/system-sections.ts +32 -27
  621. package/src/prompts/templates/users/default.md +19 -0
  622. package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
  623. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
  624. package/src/providers/model-intents.ts +2 -2
  625. package/src/providers/openai/chat-completions-provider.ts +14 -1
  626. package/src/providers/openai/responses-provider.ts +18 -0
  627. package/src/providers/placeholder-sentinels.ts +18 -8
  628. package/src/providers/retry.ts +1 -0
  629. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
  630. package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
  631. package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
  632. package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
  633. package/src/providers/speech-to-text/provider-catalog.ts +0 -89
  634. package/src/providers/speech-to-text/resolve.ts +55 -0
  635. package/src/providers/tool-progress-events.test.ts +50 -0
  636. package/src/providers/tool-progress-events.ts +99 -0
  637. package/src/providers/types.ts +1 -1
  638. package/src/providers/vellum-model-routing.test.ts +51 -0
  639. package/src/providers/vellum-model-routing.ts +84 -0
  640. package/src/runtime/AGENTS.md +5 -4
  641. package/src/runtime/__tests__/agent-wake.test.ts +158 -1
  642. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  643. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
  644. package/src/runtime/access-request-helper.ts +154 -3
  645. package/src/runtime/actor-trust-resolver.ts +7 -21
  646. package/src/runtime/agent-wake.ts +41 -1
  647. package/src/runtime/assistant-event.ts +86 -12
  648. package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
  649. package/src/runtime/channel-approval-types.ts +52 -2
  650. package/src/runtime/channel-approvals.ts +23 -8
  651. package/src/runtime/channel-invite-transport.ts +2 -2
  652. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  653. package/src/runtime/channel-readiness-service.ts +3 -5
  654. package/src/runtime/channel-retry-sweep.ts +18 -24
  655. package/src/runtime/channel-verification-service.ts +9 -11
  656. package/src/runtime/finalize-event-delivery.ts +30 -16
  657. package/src/runtime/guardian-action-service.ts +10 -11
  658. package/src/runtime/guardian-reply-router.ts +122 -21
  659. package/src/runtime/http-server.ts +44 -101
  660. package/src/runtime/introduction-policy.test.ts +214 -0
  661. package/src/runtime/introduction-policy.ts +284 -0
  662. package/src/runtime/invite-service.ts +104 -361
  663. package/src/runtime/local-actor-identity.test.ts +38 -0
  664. package/src/runtime/local-actor-identity.ts +14 -0
  665. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
  666. package/src/runtime/middleware/rate-limiter.ts +30 -0
  667. package/src/runtime/middleware/twilio-validation.ts +5 -7
  668. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
  669. package/src/runtime/migrations/origin-mode.ts +6 -6
  670. package/src/runtime/pre-first-message-gate.ts +1 -0
  671. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
  672. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
  673. package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
  674. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
  675. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
  676. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
  677. package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
  678. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
  679. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
  680. package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
  681. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
  682. package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
  683. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
  684. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
  685. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
  686. package/src/runtime/routes/acp-routes.ts +15 -3
  687. package/src/runtime/routes/app-management-routes.ts +4 -7
  688. package/src/runtime/routes/approval-routes.ts +4 -0
  689. package/src/runtime/routes/attachment-routes.ts +54 -3
  690. package/src/runtime/routes/avatar-routes.ts +0 -4
  691. package/src/runtime/routes/background-tool-routes.ts +34 -5
  692. package/src/runtime/routes/btw-routes.ts +32 -4
  693. package/src/runtime/routes/channel-route-shared.ts +18 -14
  694. package/src/runtime/routes/channel-verification-routes.ts +0 -3
  695. package/src/runtime/routes/contact-routes.ts +298 -117
  696. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  697. package/src/runtime/routes/conversation-management-routes.ts +74 -37
  698. package/src/runtime/routes/conversation-query-routes.ts +5 -2
  699. package/src/runtime/routes/conversation-routes.ts +222 -82
  700. package/src/runtime/routes/conversations-import-routes.ts +8 -1
  701. package/src/runtime/routes/credential-prompt-routes.ts +20 -5
  702. package/src/runtime/routes/debug-routes.ts +1 -0
  703. package/src/runtime/routes/documents-routes.ts +5 -2
  704. package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
  705. package/src/runtime/routes/global-search-routes.ts +2 -1
  706. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  707. package/src/runtime/routes/home-feed-routes.ts +60 -1
  708. package/src/runtime/routes/identity-routes.ts +4 -87
  709. package/src/runtime/routes/inbound-message-handler.ts +82 -41
  710. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
  711. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
  712. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
  713. package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
  714. package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
  715. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  716. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  717. package/src/runtime/routes/index.ts +6 -0
  718. package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
  719. package/src/runtime/routes/internal-twilio-routes.ts +0 -15
  720. package/src/runtime/routes/messages-lexical-routes.ts +75 -0
  721. package/src/runtime/routes/migration-routes.ts +69 -6
  722. package/src/runtime/routes/monitoring-routes.ts +237 -0
  723. package/src/runtime/routes/plugins-routes.ts +329 -8
  724. package/src/runtime/routes/schedule-routes.ts +49 -8
  725. package/src/runtime/routes/schedule-worker-routes.ts +210 -0
  726. package/src/runtime/routes/secret-routes.ts +1 -1
  727. package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
  728. package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
  729. package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
  730. package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
  731. package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
  732. package/src/runtime/services/analyze-conversation.ts +14 -4
  733. package/src/runtime/services/conversation-serializer.ts +5 -0
  734. package/src/runtime/slack-no-response.ts +47 -0
  735. package/src/runtime/slack-reply-session.test.ts +633 -0
  736. package/src/runtime/slack-reply-session.ts +382 -0
  737. package/src/runtime/slack-task-progress.ts +95 -0
  738. package/src/runtime/sync/resource-sync-events.ts +25 -0
  739. package/src/runtime/tool-grant-request-helper.ts +17 -1
  740. package/src/runtime/trust-class.test.ts +141 -0
  741. package/src/runtime/trust-class.ts +121 -0
  742. package/src/runtime/trust-verdict-consumer.ts +7 -6
  743. package/src/schedule/__tests__/run-script.test.ts +21 -0
  744. package/src/schedule/__tests__/worker-control.test.ts +241 -0
  745. package/src/schedule/run-script.ts +13 -6
  746. package/src/schedule/schedule-usage-store.ts +1 -0
  747. package/src/schedule/scheduler.ts +178 -127
  748. package/src/schedule/worker-control.ts +118 -0
  749. package/src/schedule/worker.ts +131 -0
  750. package/src/skills/__tests__/categories-cache.test.ts +38 -0
  751. package/src/skills/categories-cache.ts +40 -5
  752. package/src/skills/managed-store.ts +41 -6
  753. package/src/skills/validate-input.ts +47 -0
  754. package/src/stt/types.ts +5 -9
  755. package/src/subagent/manager.ts +267 -39
  756. package/src/subagent/types.ts +6 -2
  757. package/src/subagent/validate-allowlists.ts +65 -0
  758. package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
  759. package/src/telemetry/watchdog-direct-emit.ts +89 -0
  760. package/src/tools/acp/list-agents.test.ts +0 -6
  761. package/src/tools/background-tool-registry.ts +61 -0
  762. package/src/tools/document/document-comment-tool.test.ts +4 -0
  763. package/src/tools/host-terminal/host-shell.test.ts +397 -0
  764. package/src/tools/host-terminal/host-shell.ts +203 -14
  765. package/src/tools/skills/find-similar-skills.test.ts +152 -1
  766. package/src/tools/skills/find-similar-skills.ts +30 -4
  767. package/src/tools/skills/load.ts +36 -0
  768. package/src/tools/skills/scaffold-managed.ts +66 -34
  769. package/src/tools/skills/skill-script-runner.ts +17 -0
  770. package/src/tools/skills/skill-tool-factory.ts +5 -1
  771. package/src/tools/terminal/safe-env.ts +3 -0
  772. package/src/tools/terminal/shell.test.ts +177 -0
  773. package/src/tools/terminal/shell.ts +98 -4
  774. package/src/tools/tool-approval-handler.ts +106 -29
  775. package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
  776. package/src/tools/types.ts +29 -17
  777. package/src/tools/ui-surface/definitions.ts +3 -1
  778. package/src/tts/__tests__/provider-adapters.test.ts +37 -0
  779. package/src/tts/__tests__/provider-catalog.test.ts +21 -0
  780. package/src/tts/provider-catalog.ts +40 -9
  781. package/src/tts/providers/fish-audio-provider.ts +19 -10
  782. package/src/tts/types.ts +3 -4
  783. package/src/util/__tests__/process-tree.test.ts +10 -2
  784. package/src/util/__tests__/text-spacing.test.ts +53 -0
  785. package/src/util/cgroup-memory.ts +142 -0
  786. package/src/util/image-conversion.ts +281 -0
  787. package/src/util/phone.ts +1 -8
  788. package/src/util/platform.ts +33 -0
  789. package/src/util/process-tree.ts +10 -2
  790. package/src/util/text-spacing.ts +53 -0
  791. package/src/util/worker-process.ts +271 -0
  792. package/src/workflows/journal-store.ts +16 -1
  793. package/src/workflows/library.test.ts +35 -20
  794. package/src/workflows/library.ts +105 -66
  795. package/src/workspace/git-service.ts +1 -0
  796. package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
  797. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
  798. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
  799. package/src/workspace/migrations/118-seed-now-md.ts +64 -0
  800. package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
  801. package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
  802. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  803. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  804. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  805. package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
  806. package/src/workspace/migrations/registry.ts +12 -0
  807. package/drizzle/0000_dizzy_maggott.sql +0 -301
  808. package/drizzle/meta/0000_snapshot.json +0 -1933
  809. package/drizzle/meta/_journal.json +0 -13
  810. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
  811. package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
  812. package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
  813. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
  814. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
  815. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
  816. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
  817. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
  818. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
  819. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
  820. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
  821. package/src/__tests__/invite-redemption-service.test.ts +0 -1180
  822. package/src/__tests__/invite-service-ipc.test.ts +0 -350
  823. package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
  824. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
  825. package/src/__tests__/relay-server.test.ts +0 -6153
  826. package/src/__tests__/telephony-stt-routing.test.ts +0 -329
  827. package/src/__tests__/voice-invite-redemption.test.ts +0 -702
  828. package/src/calls/relay-server.ts +0 -2463
  829. package/src/calls/telephony-stt-routing.ts +0 -145
  830. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
  831. package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
  832. package/src/daemon/daemon-skill-host.ts +0 -262
  833. package/src/daemon/message-types/meet.ts +0 -143
  834. package/src/daemon/shutdown-registry.ts +0 -40
  835. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
  836. package/src/ipc/__tests__/skill-server.test.ts +0 -182
  837. package/src/ipc/skill-ipc-types.ts +0 -54
  838. package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
  839. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
  840. package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
  841. package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
  842. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
  843. package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
  844. package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
  845. package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
  846. package/src/ipc/skill-routes/config.ts +0 -47
  847. package/src/ipc/skill-routes/events.ts +0 -147
  848. package/src/ipc/skill-routes/identity.ts +0 -21
  849. package/src/ipc/skill-routes/index.ts +0 -37
  850. package/src/ipc/skill-routes/log.ts +0 -40
  851. package/src/ipc/skill-routes/memory.ts +0 -92
  852. package/src/ipc/skill-routes/platform.ts +0 -39
  853. package/src/ipc/skill-routes/providers.ts +0 -162
  854. package/src/ipc/skill-routes/registries.ts +0 -338
  855. package/src/ipc/skill-server.ts +0 -831
  856. package/src/ipc/skill-socket-path.ts +0 -20
  857. package/src/jobs/register-job-handlers.ts +0 -74
  858. package/src/persistence/invite-store.ts +0 -481
  859. package/src/plugins/job-handler-registry.ts +0 -94
  860. package/src/providers/cache-control.ts +0 -26
  861. package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
  862. package/src/runtime/invite-redemption-service.ts +0 -828
  863. package/src/runtime/invite-redemption-templates.ts +0 -46
  864. package/src/runtime/runtime-mode.ts +0 -40
  865. package/src/runtime/slack-dm-text-delivery.ts +0 -177
  866. package/src/util/voice-code.ts +0 -31
@@ -12,10 +12,13 @@ import {
12
12
  // Module mocks — declared before imports of the module under test.
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
- // Mock the STT resolve module (used by MediaStreamSttSession)
15
+ // Mock the STT resolve module (used by MediaStreamSttSession).
16
+ // resolveStreamingTranscriber yields no transcriber, so sessions settle on
17
+ // the batch path regardless of the calls.voice.telephonyStreaming default.
16
18
  mock.module("../providers/speech-to-text/resolve.js", () => ({
17
19
  resolveTelephonySttCapability: jest.fn(),
18
20
  resolveBatchTranscriber: jest.fn(),
21
+ resolveStreamingTranscriber: jest.fn(async () => null),
19
22
  }));
20
23
 
21
24
  // Mock the logger to suppress output during tests
@@ -98,28 +101,33 @@ mock.module("../calls/finalize-call.js", () => ({
98
101
 
99
102
  // Mock the call pointer messages
100
103
  mock.module("../calls/call-pointer-messages.js", () => ({
101
- addPointerMessage: jest.fn(async () => {}),
104
+ postPointerMessageSafe: jest.fn(),
102
105
  formatDuration: jest.fn((ms: number) => `${Math.round(ms / 1000)}s`),
103
106
  }));
104
107
 
105
108
  // Mock the CallController to avoid pulling in the full conversation pipeline
106
109
  const mockStartInitialGreeting = jest.fn(async () => {});
110
+ const mockStartPostVerificationGreeting = jest.fn(async () => {});
111
+ const mockMarkNextCallerTurnAsOpeningAck = jest.fn();
107
112
  const mockHandleCallerUtterance = jest.fn(async () => {});
108
113
  const mockHandleInterrupt = jest.fn();
109
114
  const mockDestroy = jest.fn();
110
115
 
111
- const mockHandleBargeIn = jest.fn(() => false);
116
+ // Mirrors CallController.handleBargeIn: invokes onAccepted only when the
117
+ // barge-in passes the speaking gate.
118
+ const mockHandleBargeIn = jest.fn((_onAccepted?: () => void) => false);
112
119
 
113
120
  mock.module("../calls/call-controller.js", () => ({
114
121
  CallController: jest.fn().mockImplementation(() => ({
115
122
  startInitialGreeting: mockStartInitialGreeting,
123
+ startPostVerificationGreeting: mockStartPostVerificationGreeting,
124
+ markNextCallerTurnAsOpeningAck: mockMarkNextCallerTurnAsOpeningAck,
116
125
  handleCallerUtterance: mockHandleCallerUtterance,
117
126
  handleInterrupt: mockHandleInterrupt,
118
127
  handleBargeIn: mockHandleBargeIn,
119
128
  destroy: mockDestroy,
120
129
  getState: jest.fn(() => "idle"),
121
130
  setTrustContext: jest.fn(),
122
- markNextCallerTurnAsOpeningAck: jest.fn(),
123
131
  getPendingConsultationQuestionId: jest.fn(),
124
132
  handleUserAnswer: jest.fn(),
125
133
  handleUserInstruction: jest.fn(),
@@ -177,11 +185,20 @@ mock.module("../calls/channel-admission-reader.js", () => ({
177
185
  }));
178
186
 
179
187
  // 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.
188
+ // verdict into routeSetup so the media-stream transport enforces the
189
+ // gateway ACL. Tests override mockInboundVerdict.
190
+ // The optional gate lets a test hold mid-setup trust re-resolution open
191
+ // (the setup flow's default resolver reads the verdict first) so it can
192
+ // deliver transcripts during the deferral window.
182
193
  let mockInboundVerdict: unknown = null;
194
+ let mockVerdictGate: Promise<void> | null = null;
183
195
  const mockGetInboundTrustVerdict = jest.fn(
184
- async (_args?: Record<string, unknown>) => mockInboundVerdict,
196
+ async (_args?: Record<string, unknown>) => {
197
+ if (mockVerdictGate) {
198
+ await mockVerdictGate;
199
+ }
200
+ return mockInboundVerdict;
201
+ },
185
202
  );
186
203
  mock.module("../calls/inbound-trust-reader.js", () => ({
187
204
  getInboundTrustVerdict: mockGetInboundTrustVerdict,
@@ -225,12 +242,152 @@ mock.module("../calls/resolve-call-tts-provider.js", () => ({
225
242
  })),
226
243
  }));
227
244
 
245
+ // ── Setup-flow dependency mocks ─────────────────────────────────────────
246
+ // MediaStreamCallSession drives every routeSetup outcome through a real
247
+ // CallSetupFlow (and, for name capture, a real GuardianWaitController).
248
+ // Mock the side-effectful modules those pull in: config-backed timing
249
+ // constants, guardian labels, verification/invite services, the guardian
250
+ // notifier, canonical-request polling, and wait heartbeats.
251
+
252
+ // Config-backed call constants — small real-timer-friendly values.
253
+ let mockTtsPlaybackDelayMs = 5;
254
+ let mockAccessRequestPollIntervalMs = 5;
255
+ let mockUserConsultationTimeoutMs = 500;
256
+ mock.module("../calls/call-constants.js", () => ({
257
+ isDeniedNumber: jest.fn(() => false),
258
+ getMaxCallDurationMs: jest.fn(() => 3_600_000),
259
+ getUserConsultationTimeoutMs: jest.fn(() => mockUserConsultationTimeoutMs),
260
+ getTtsPlaybackDelayMs: jest.fn(() => mockTtsPlaybackDelayMs),
261
+ getAccessRequestPollIntervalMs: jest.fn(
262
+ () => mockAccessRequestPollIntervalMs,
263
+ ),
264
+ getGuardianWaitUpdateInitialIntervalMs: jest.fn(() => 10_000),
265
+ getGuardianWaitUpdateInitialWindowMs: jest.fn(() => 60_000),
266
+ getGuardianWaitUpdateSteadyMinIntervalMs: jest.fn(() => 20_000),
267
+ getGuardianWaitUpdateSteadyMaxIntervalMs: jest.fn(() => 40_000),
268
+ getSilenceTimeoutMs: jest.fn(() => 30_000),
269
+ getEndCallListenWindowMs: jest.fn(() => 15_000),
270
+ }));
271
+
272
+ // Guardian delivery reader (IPC-backed): label priming + cache warming.
273
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
274
+ getGuardianDelivery: jest.fn(async () => [
275
+ { channelType: "phone", status: "active", displayName: "Alex" },
276
+ ]),
277
+ getGuardianDeliveryFresh: jest.fn(async () => []),
278
+ peekCachedGuardianDelivery: jest.fn(() => null),
279
+ voiceGuardianDisplayName: jest.fn(() => "Alex"),
280
+ guardianForChannel: jest.fn(),
281
+ anyGuardian: jest.fn(),
282
+ invalidateGuardianDeliveryCache: jest.fn(),
283
+ }));
284
+
285
+ // Guardian/assistant display labels (filesystem-backed).
286
+ mock.module("../prompts/user-reference.js", () => ({
287
+ DEFAULT_USER_REFERENCE: "my human",
288
+ resolveGuardianName: jest.fn(
289
+ (primed?: string | null) => primed ?? "my human",
290
+ ),
291
+ }));
292
+ mock.module("../daemon/identity-helpers.js", () => ({
293
+ getAssistantName: jest.fn(() => "Aria"),
294
+ resolveUserName: jest.fn(() => null),
295
+ }));
296
+
297
+ // Conversation persistence (used by callee verification code posting).
298
+ const mockAddMessage = jest.fn(async () => ({}));
299
+ mock.module("../persistence/conversation-crud.js", () => ({
300
+ addMessage: mockAddMessage,
301
+ }));
302
+
303
+ // Verification / invite services (gateway + verification-store backed).
304
+ type MockVerificationResult =
305
+ | {
306
+ outcome: "success";
307
+ verificationType: "guardian" | "trusted_contact";
308
+ eventName: string;
309
+ ttsMessage?: string;
310
+ }
311
+ | {
312
+ outcome: "failure";
313
+ eventName: string;
314
+ ttsMessage: string;
315
+ attempts: number;
316
+ }
317
+ | {
318
+ outcome: "retry";
319
+ ttsMessage: string;
320
+ attempt: number;
321
+ maxAttempts: number;
322
+ };
323
+ let mockVerificationResult: MockVerificationResult = {
324
+ outcome: "success",
325
+ verificationType: "guardian",
326
+ eventName: "voice_verification_succeeded",
327
+ };
328
+ const mockAttemptVerificationCode = jest.fn(async () => mockVerificationResult);
329
+ type MockInviteResult =
330
+ | { outcome: "success"; memberId: string; inviteId: string; type: string }
331
+ | { outcome: "failure"; ttsMessage: string };
332
+ let mockInviteResult: MockInviteResult = {
333
+ outcome: "success",
334
+ memberId: "member-1",
335
+ inviteId: "invite-1",
336
+ type: "trusted_contact",
337
+ };
338
+ const mockAttemptInviteCodeRedemption = jest.fn(async () => mockInviteResult);
339
+ mock.module("../calls/relay-verification.js", () => ({
340
+ attemptVerificationCode: mockAttemptVerificationCode,
341
+ attemptInviteCodeRedemption: mockAttemptInviteCodeRedemption,
342
+ parseDigitsFromSpeech: jest.fn((text: string) => text.replace(/\D+/g, "")),
343
+ }));
344
+
345
+ // Guardian access-request notifier (name-capture sub-flow).
346
+ type MockNotifyResult =
347
+ | { notified: true; created: boolean; requestId: string }
348
+ | { notified: false; reason: string };
349
+ let mockNotifyResult: MockNotifyResult = {
350
+ notified: true,
351
+ created: true,
352
+ requestId: "req-1",
353
+ };
354
+ const mockNotifyGuardianOfAccessRequest = jest.fn(async () => mockNotifyResult);
355
+ mock.module("../runtime/access-request-helper.js", () => ({
356
+ notifyGuardianOfAccessRequest: mockNotifyGuardianOfAccessRequest,
357
+ }));
358
+
359
+ // Canonical guardian-request store polled by the guardian wait controller.
360
+ let mockCanonicalRequest: { status: string } | null = null;
361
+ const mockGetCanonicalGuardianRequest = jest.fn(() => mockCanonicalRequest);
362
+ mock.module("../contacts/canonical-guardian-store.js", () => ({
363
+ getCanonicalGuardianRequest: mockGetCanonicalGuardianRequest,
364
+ }));
365
+
366
+ // Wait-state helpers: no heartbeats in tests; capture callback handoffs.
367
+ const mockEmitCallbackHandoff = jest.fn(
368
+ (args: { callbackHandoffNotified: boolean }) => ({
369
+ callbackHandoffNotified: args.callbackHandoffNotified,
370
+ notified: false,
371
+ }),
372
+ );
373
+ mock.module("../calls/relay-access-wait.js", () => ({
374
+ classifyWaitUtterance: jest.fn(() => "neutral"),
375
+ scheduleNextHeartbeat: jest.fn(() => null),
376
+ emitAccessRequestCallbackHandoff: mockEmitCallbackHandoff,
377
+ }));
378
+
228
379
  // ---------------------------------------------------------------------------
229
380
  // Now import the module under test.
230
381
  // ---------------------------------------------------------------------------
231
382
 
383
+ import { revokeScopedApprovalGrantsForContext } from "../approvals/scoped-approval-grants.js";
384
+ import { CallController } from "../calls/call-controller.js";
385
+ import { postPointerMessageSafe } from "../calls/call-pointer-messages.js";
232
386
  import { speakSystemPrompt } from "../calls/call-speech-output.js";
233
- import { registerCallController } from "../calls/call-state.js";
387
+ import {
388
+ fireCallTranscriptNotifier,
389
+ registerCallController,
390
+ } from "../calls/call-state.js";
234
391
  import { recordCallEvent, updateCallSession } from "../calls/call-store.js";
235
392
  import { finalizeCall } from "../calls/finalize-call.js";
236
393
  import {
@@ -358,22 +515,51 @@ beforeEach(() => {
358
515
  mockControllers.clear();
359
516
  activeMediaStreamSessions.clear();
360
517
  mockStartInitialGreeting.mockClear();
518
+ mockStartPostVerificationGreeting.mockClear();
519
+ mockMarkNextCallerTurnAsOpeningAck.mockClear();
361
520
  mockHandleCallerUtterance.mockClear();
362
521
  mockHandleInterrupt.mockClear();
363
522
  mockHandleBargeIn.mockClear();
364
523
  mockHandleBargeIn.mockReturnValue(false);
365
524
  mockDestroy.mockClear();
525
+ (CallController as unknown as jest.Mock).mockClear();
366
526
  (registerCallController as jest.Mock).mockClear();
527
+ (fireCallTranscriptNotifier as jest.Mock).mockClear();
367
528
  (recordCallEvent as jest.Mock).mockClear();
368
529
  (updateCallSession as jest.Mock).mockClear();
369
530
  (finalizeCall as jest.Mock).mockClear();
531
+ (revokeScopedApprovalGrantsForContext as jest.Mock).mockClear();
370
532
  (speakSystemPrompt as jest.Mock).mockClear();
533
+ (postPointerMessageSafe as jest.Mock).mockClear();
371
534
  (routeSetup as jest.Mock).mockClear();
372
535
  mockGetChannelAdmissionPolicy.mockClear();
373
536
  mockAdmissionPolicy = null;
374
537
  mockAdmissionGate = null;
375
538
  mockGetInboundTrustVerdict.mockClear();
376
539
  mockInboundVerdict = null;
540
+ mockVerdictGate = null;
541
+ mockAddMessage.mockClear();
542
+ mockAttemptVerificationCode.mockClear();
543
+ mockVerificationResult = {
544
+ outcome: "success",
545
+ verificationType: "guardian",
546
+ eventName: "voice_verification_succeeded",
547
+ };
548
+ mockAttemptInviteCodeRedemption.mockClear();
549
+ mockInviteResult = {
550
+ outcome: "success",
551
+ memberId: "member-1",
552
+ inviteId: "invite-1",
553
+ type: "trusted_contact",
554
+ };
555
+ mockNotifyGuardianOfAccessRequest.mockClear();
556
+ mockNotifyResult = { notified: true, created: true, requestId: "req-1" };
557
+ mockGetCanonicalGuardianRequest.mockClear();
558
+ mockCanonicalRequest = null;
559
+ mockEmitCallbackHandoff.mockClear();
560
+ mockTtsPlaybackDelayMs = 5;
561
+ mockAccessRequestPollIntervalMs = 5;
562
+ mockUserConsultationTimeoutMs = 500;
377
563
  // Reset routeSetup to default normal_call
378
564
  mockRouteSetupResult = {
379
565
  outcome: { action: "normal_call" as const, isInbound: true },
@@ -404,7 +590,6 @@ describe("MediaStreamCallSession", () => {
404
590
  const session = new MediaStreamCallSession(ws, "call-1");
405
591
  expect(session.callSessionId).toBe("call-1");
406
592
  expect(session.getOutput()).toBeDefined();
407
- expect(session.getOutput().getConnectionState()).toBe("connected");
408
593
  });
409
594
 
410
595
  describe("start event handling", () => {
@@ -472,7 +657,7 @@ describe("MediaStreamCallSession", () => {
472
657
  });
473
658
 
474
659
  describe("transport close handling", () => {
475
- test("normal close (1000) marks session as completed", () => {
660
+ test("normal close (1000) marks session as completed and writes the completed pointer", () => {
476
661
  const mock = createMockWs();
477
662
  mockSessions.set("call-1", {
478
663
  id: "call-1",
@@ -480,6 +665,7 @@ describe("MediaStreamCallSession", () => {
480
665
  status: "in_progress",
481
666
  startedAt: Date.now() - 60000,
482
667
  toNumber: "+15551234567",
668
+ initiatedFromConversationId: "conv-origin",
483
669
  });
484
670
 
485
671
  const session = new MediaStreamCallSession(mock.ws, "call-1");
@@ -490,9 +676,15 @@ describe("MediaStreamCallSession", () => {
490
676
  expect.objectContaining({ status: "completed" }),
491
677
  );
492
678
  expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
679
+ expect(postPointerMessageSafe).toHaveBeenCalledWith(
680
+ "conv-origin",
681
+ "completed",
682
+ "+15551234567",
683
+ expect.objectContaining({ duration: expect.any(String) }),
684
+ );
493
685
  });
494
686
 
495
- test("abnormal close marks session as failed", () => {
687
+ test("abnormal close marks session as failed and writes the failed pointer", () => {
496
688
  const mock = createMockWs();
497
689
  mockSessions.set("call-1", {
498
690
  id: "call-1",
@@ -500,6 +692,7 @@ describe("MediaStreamCallSession", () => {
500
692
  status: "in_progress",
501
693
  startedAt: Date.now() - 60000,
502
694
  toNumber: "+15551234567",
695
+ initiatedFromConversationId: "conv-origin",
503
696
  });
504
697
 
505
698
  const session = new MediaStreamCallSession(mock.ws, "call-1");
@@ -513,6 +706,12 @@ describe("MediaStreamCallSession", () => {
513
706
  }),
514
707
  );
515
708
  expect(finalizeCall).toHaveBeenCalledWith("call-1", "conv-1");
709
+ expect(postPointerMessageSafe).toHaveBeenCalledWith(
710
+ "conv-origin",
711
+ "failed",
712
+ "+15551234567",
713
+ expect.objectContaining({ reason: expect.any(String) }),
714
+ );
516
715
  });
517
716
 
518
717
  test("close on already-terminal session is a no-op", () => {
@@ -552,7 +751,6 @@ describe("MediaStreamCallSession", () => {
552
751
 
553
752
  session.destroy();
554
753
  expect(mockDestroy).toHaveBeenCalled();
555
- expect(session.getOutput().getConnectionState()).toBe("closed");
556
754
  });
557
755
 
558
756
  test("destroy is idempotent", () => {
@@ -955,157 +1153,7 @@ describe("media-stream setup outcome scenarios", () => {
955
1153
  });
956
1154
  });
957
1155
 
958
- describe("unsupported interactive setup flow", () => {
959
- test("verification outcome records call_failed with preflight-bypass reason", async () => {
960
- mockRouteSetupResult = {
961
- outcome: {
962
- action: "verification",
963
- assistantId: "self",
964
- fromNumber: "+14155551234",
965
- },
966
- resolved: {
967
- assistantId: "self",
968
- isInbound: true,
969
- otherPartyNumber: "+14155551234",
970
- actorTrust: { trustClass: "unknown", memberRecord: null },
971
- },
972
- };
973
-
974
- const mockWs = createMockWs();
975
- mockSessions.set("call-unsup-verify-1", {
976
- id: "call-unsup-verify-1",
977
- conversationId: "conv-unsup-verify-1",
978
- status: "initiated",
979
- task: null,
980
- startedAt: null,
981
- fromNumber: "+14155551234",
982
- toNumber: "+15550001111",
983
- });
984
-
985
- const session = new MediaStreamCallSession(
986
- mockWs.ws,
987
- "call-unsup-verify-1",
988
- );
989
- session.handleMessage(makeStartMessage());
990
- await session.whenSetupSettled();
991
-
992
- // Should record call_failed event with preflight-bypass note
993
- expect(recordCallEvent).toHaveBeenCalledWith(
994
- "call-unsup-verify-1",
995
- "call_failed",
996
- expect.objectContaining({
997
- reason: expect.stringContaining("verification"),
998
- transport: "media-stream",
999
- }),
1000
- );
1001
-
1002
- // Should set session status to failed
1003
- expect(updateCallSession).toHaveBeenCalledWith(
1004
- "call-unsup-verify-1",
1005
- expect.objectContaining({
1006
- status: "failed",
1007
- lastError: expect.stringContaining("preflight guard"),
1008
- }),
1009
- );
1010
-
1011
- // Should NOT register a controller
1012
- expect(registerCallController).not.toHaveBeenCalled();
1013
- });
1014
-
1015
- test("name_capture outcome speaks generic apology and tears down", async () => {
1016
- mockRouteSetupResult = {
1017
- outcome: {
1018
- action: "name_capture",
1019
- assistantId: "self",
1020
- fromNumber: "+14155551234",
1021
- },
1022
- resolved: {
1023
- assistantId: "self",
1024
- isInbound: true,
1025
- otherPartyNumber: "+14155551234",
1026
- actorTrust: { trustClass: "unknown", memberRecord: null },
1027
- },
1028
- };
1029
-
1030
- const mockWs = createMockWs();
1031
- mockSessions.set("call-unsup-name-1", {
1032
- id: "call-unsup-name-1",
1033
- conversationId: "conv-unsup-name-1",
1034
- status: "initiated",
1035
- task: null,
1036
- startedAt: null,
1037
- fromNumber: "+14155551234",
1038
- toNumber: "+15550001111",
1039
- });
1040
-
1041
- const session = new MediaStreamCallSession(
1042
- mockWs.ws,
1043
- "call-unsup-name-1",
1044
- );
1045
- session.handleMessage(makeStartMessage());
1046
- await session.whenSetupSettled();
1047
-
1048
- // speakSystemPrompt should be called with the generic apology
1049
- expect(speakSystemPrompt).toHaveBeenCalledWith(
1050
- expect.anything(),
1051
- expect.stringContaining("additional verification"),
1052
- );
1053
-
1054
- // Should run finalization inline
1055
- expect(finalizeCall).toHaveBeenCalledWith(
1056
- "call-unsup-name-1",
1057
- "conv-unsup-name-1",
1058
- );
1059
- });
1060
-
1061
- test("callee_verification outcome fails with explicit reason", async () => {
1062
- mockRouteSetupResult = {
1063
- outcome: {
1064
- action: "callee_verification",
1065
- verificationConfig: { maxAttempts: 3, codeLength: 6 },
1066
- },
1067
- resolved: {
1068
- assistantId: "self",
1069
- isInbound: false,
1070
- otherPartyNumber: "+14155551234",
1071
- actorTrust: { trustClass: "guardian", memberRecord: null },
1072
- },
1073
- };
1074
-
1075
- const mockWs = createMockWs();
1076
- mockSessions.set("call-unsup-callee-1", {
1077
- id: "call-unsup-callee-1",
1078
- conversationId: "conv-unsup-callee-1",
1079
- status: "initiated",
1080
- task: null,
1081
- startedAt: null,
1082
- fromNumber: "+15550001111",
1083
- toNumber: "+14155551234",
1084
- });
1085
-
1086
- const session = new MediaStreamCallSession(
1087
- mockWs.ws,
1088
- "call-unsup-callee-1",
1089
- );
1090
- session.handleMessage(makeStartMessage());
1091
- await session.whenSetupSettled();
1092
-
1093
- // Should record the failure with the specific action
1094
- expect(recordCallEvent).toHaveBeenCalledWith(
1095
- "call-unsup-callee-1",
1096
- "call_failed",
1097
- expect.objectContaining({
1098
- reason: expect.stringContaining("callee_verification"),
1099
- }),
1100
- );
1101
-
1102
- // Session should be failed
1103
- expect(updateCallSession).toHaveBeenCalledWith(
1104
- "call-unsup-callee-1",
1105
- expect.objectContaining({ status: "failed" }),
1106
- );
1107
- });
1108
-
1156
+ describe("normal_call reset after deny", () => {
1109
1157
  test("normal_call after deny scenario still creates controller", async () => {
1110
1158
  // Verify that after a deny-scenario test, resetting to normal_call
1111
1159
  // properly creates a controller (no cross-test pollution).
@@ -1179,6 +1227,15 @@ describe("media-stream setup outcome scenarios", () => {
1179
1227
  expect(mockHandleBargeIn).toHaveBeenCalled();
1180
1228
  expect(mockHandleInterrupt).not.toHaveBeenCalled();
1181
1229
 
1230
+ // An ignored barge-in flushes only Twilio's buffered audio (to
1231
+ // stop a completed turn's tail talking over the caller). The
1232
+ // internal playback queue is untouched, so the queued greeting
1233
+ // still plays — handleInterrupt must not have run.
1234
+ const clearCommands = mockWs.sent.filter(
1235
+ (s) => JSON.parse(s).event === "clear",
1236
+ );
1237
+ expect(clearCommands.length).toBeGreaterThan(0);
1238
+
1182
1239
  // voice_session_aborted should NOT appear in recorded events
1183
1240
  const abortEvents = mockEvents.filter(
1184
1241
  (e) =>
@@ -1191,8 +1248,12 @@ describe("media-stream setup outcome scenarios", () => {
1191
1248
  });
1192
1249
 
1193
1250
  test("barge-in is accepted when controller is speaking", async () => {
1194
- // Configure mock to indicate the controller is speaking
1195
- mockHandleBargeIn.mockReturnValue(true);
1251
+ // Configure mock to indicate the controller is speaking; like the
1252
+ // real controller, it fires onAccepted before interrupting.
1253
+ mockHandleBargeIn.mockImplementation((onAccepted?: () => void) => {
1254
+ onAccepted?.();
1255
+ return true;
1256
+ });
1196
1257
 
1197
1258
  const mockWs = createMockWs();
1198
1259
  mockSessions.set("call-bargein-2", {
@@ -1213,8 +1274,13 @@ describe("media-stream setup outcome scenarios", () => {
1213
1274
  const speechPayload = Buffer.alloc(160, 0x00).toString("base64");
1214
1275
  session.handleMessage(makeMediaMessage(speechPayload, "1"));
1215
1276
 
1216
- // handleBargeIn should have been called (returning true)
1277
+ // handleBargeIn should have been called (returning true), and the
1278
+ // accepted barge-in flushes outbound audio via the onAccepted hook.
1217
1279
  expect(mockHandleBargeIn).toHaveBeenCalled();
1280
+ const clearCommands = mockWs.sent.filter(
1281
+ (s) => JSON.parse(s).event === "clear",
1282
+ );
1283
+ expect(clearCommands.length).toBe(1);
1218
1284
 
1219
1285
  session.destroy();
1220
1286
  });
@@ -1556,9 +1622,9 @@ describe("media-stream setup outcome scenarios", () => {
1556
1622
 
1557
1623
  // ── Gateway trust verdict ──────────────────────────────────────────
1558
1624
  // 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.
1625
+ // threads it into routeSetup, so the media-stream transport enforces
1626
+ // the gateway ACL. routeSetup itself decides verdict-vs-local; these
1627
+ // tests assert the verdict is fetched and passed.
1562
1628
 
1563
1629
  describe("gateway trust verdict", () => {
1564
1630
  test("fetches the inbound caller's verdict and threads it into routeSetup", async () => {
@@ -1663,3 +1729,711 @@ describe("media-stream setup outcome scenarios", () => {
1663
1729
  });
1664
1730
  });
1665
1731
  });
1732
+
1733
+ // ---------------------------------------------------------------------------
1734
+ // Setup-flow integration scenarios
1735
+ // ---------------------------------------------------------------------------
1736
+ // Drive every interactive routeSetup outcome end to end through the real
1737
+ // CallSetupFlow (and, for name capture, the real GuardianWaitController)
1738
+ // over a fake WebSocket. These use real timers with small mocked timing
1739
+ // constants so terminal hangups and guardian-wait polling actually fire.
1740
+
1741
+ describe("setup flows over the media-stream transport", () => {
1742
+ beforeEach(() => {
1743
+ jest.useRealTimers();
1744
+ });
1745
+
1746
+ const FROM = "+14155550100";
1747
+ const TO = "+15550001111";
1748
+
1749
+ const sleep = (ms = 15) => Bun.sleep(ms);
1750
+
1751
+ function setupCall(opts: {
1752
+ callId: string;
1753
+ outcome: { action: string; [key: string]: unknown };
1754
+ resolved?: Partial<(typeof mockRouteSetupResult)["resolved"]>;
1755
+ session?: Record<string, unknown>;
1756
+ }) {
1757
+ mockRouteSetupResult = {
1758
+ outcome: opts.outcome,
1759
+ resolved: {
1760
+ assistantId: "self",
1761
+ isInbound: true,
1762
+ otherPartyNumber: FROM,
1763
+ actorTrust: { trustClass: "unknown", memberRecord: null },
1764
+ ...opts.resolved,
1765
+ },
1766
+ };
1767
+ mockSessions.set(opts.callId, {
1768
+ id: opts.callId,
1769
+ conversationId: `conv-${opts.callId}`,
1770
+ status: "initiated",
1771
+ task: null,
1772
+ startedAt: null,
1773
+ fromNumber: FROM,
1774
+ toNumber: TO,
1775
+ ...opts.session,
1776
+ });
1777
+ const mockWs = createMockWs();
1778
+ const session = new MediaStreamCallSession(mockWs.ws, opts.callId);
1779
+ session.handleMessage(makeStartMessage());
1780
+ return { session, mockWs };
1781
+ }
1782
+
1783
+ /**
1784
+ * Deliver a final caller transcript, mirroring the STT session's
1785
+ * onTranscriptFinal callback wiring (driving real audio through the
1786
+ * batch transcriber is out of scope for these tests).
1787
+ */
1788
+ function deliverTranscript(
1789
+ session: MediaStreamCallSession,
1790
+ text: string,
1791
+ ): void {
1792
+ (
1793
+ session as unknown as {
1794
+ handleTranscriptFinal(text: string, durationMs: number): void;
1795
+ }
1796
+ ).handleTranscriptFinal(text, 500);
1797
+ }
1798
+
1799
+ function enterDigits(session: MediaStreamCallSession, digits: string): void {
1800
+ for (const digit of digits) {
1801
+ session.handleMessage(makeDtmfMessage(digit));
1802
+ }
1803
+ }
1804
+
1805
+ function callerSpokeEvents(callId: string) {
1806
+ return mockEvents.filter(
1807
+ (e) => e.callSessionId === callId && e.eventType === "caller_spoke",
1808
+ );
1809
+ }
1810
+
1811
+ describe("inbound verification", () => {
1812
+ test("success routes DTMF to the flow, then proceeds to the initial greeting", async () => {
1813
+ const { session } = setupCall({
1814
+ callId: "call-verif-ok",
1815
+ outcome: {
1816
+ action: "verification",
1817
+ assistantId: "self",
1818
+ fromNumber: FROM,
1819
+ },
1820
+ });
1821
+ await session.whenSetupSettled();
1822
+
1823
+ // The flow prompts for the code; no controller exists during setup.
1824
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
1825
+ expect.anything(),
1826
+ expect.stringContaining("six-digit verification code"),
1827
+ );
1828
+ expect(session.getSetupFlow()?.getState()).toBe("collecting_code");
1829
+ expect(session.getController()).toBeNull();
1830
+ expect(registerCallController).not.toHaveBeenCalled();
1831
+
1832
+ enterDigits(session, "123456");
1833
+ await sleep();
1834
+
1835
+ expect(mockAttemptVerificationCode).toHaveBeenCalledWith(
1836
+ expect.objectContaining({ enteredCode: "123456", isOutbound: false }),
1837
+ );
1838
+ // The server records per-digit caller_spoke events for the flow.
1839
+ expect(callerSpokeEvents("call-verif-ok")).toHaveLength(6);
1840
+
1841
+ // Flow completed: controller created + initial greeting fired.
1842
+ expect(registerCallController).toHaveBeenCalledWith(
1843
+ "call-verif-ok",
1844
+ expect.anything(),
1845
+ );
1846
+ expect(
1847
+ (CallController as unknown as jest.Mock).mock.calls[0]?.[3],
1848
+ ).toMatchObject({ assistantId: "self" });
1849
+ expect(mockStartInitialGreeting).toHaveBeenCalled();
1850
+ expect(session.getSetupFlow()).toBeNull();
1851
+ expect(session.getController()).not.toBeNull();
1852
+ expect(finalizeCall).not.toHaveBeenCalled();
1853
+
1854
+ // Post-setup transcripts route to the controller.
1855
+ deliverTranscript(session, "hello there");
1856
+ await sleep(5);
1857
+ expect(mockHandleCallerUtterance).toHaveBeenCalledWith("hello there");
1858
+
1859
+ session.destroy();
1860
+ });
1861
+
1862
+ test("failure fails the session, finalizes exactly once, and hangs up", async () => {
1863
+ mockVerificationResult = {
1864
+ outcome: "failure",
1865
+ eventName: "voice_verification_failed",
1866
+ ttsMessage: "Verification failed. Goodbye.",
1867
+ attempts: 3,
1868
+ };
1869
+ const { session, mockWs } = setupCall({
1870
+ callId: "call-verif-fail",
1871
+ outcome: {
1872
+ action: "verification",
1873
+ assistantId: "self",
1874
+ fromNumber: FROM,
1875
+ },
1876
+ });
1877
+ await session.whenSetupSettled();
1878
+
1879
+ enterDigits(session, "000000");
1880
+ await sleep();
1881
+
1882
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
1883
+ expect.anything(),
1884
+ "Verification failed. Goodbye.",
1885
+ );
1886
+ expect(updateCallSession).toHaveBeenCalledWith(
1887
+ "call-verif-fail",
1888
+ expect.objectContaining({ status: "failed" }),
1889
+ );
1890
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
1891
+ expect(finalizeCall).toHaveBeenCalledWith(
1892
+ "call-verif-fail",
1893
+ "conv-call-verif-fail",
1894
+ );
1895
+ expect(registerCallController).not.toHaveBeenCalled();
1896
+
1897
+ // The flow schedules its own delayed endSession.
1898
+ await sleep(30);
1899
+ expect(mockWs.closed).toBe(true);
1900
+
1901
+ // Transport close after flow-side finalization must not double-finalize.
1902
+ session.handleTransportClosed(1000, "session-ended");
1903
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
1904
+
1905
+ session.destroy();
1906
+ });
1907
+ });
1908
+
1909
+ describe("outbound verification", () => {
1910
+ test("success posts the pointer and fires the post-verification greeting", async () => {
1911
+ const { session } = setupCall({
1912
+ callId: "call-outverif",
1913
+ outcome: {
1914
+ action: "outbound_verification",
1915
+ assistantId: "self",
1916
+ sessionId: "verif-sess-1",
1917
+ toNumber: TO,
1918
+ },
1919
+ resolved: {
1920
+ isInbound: false,
1921
+ otherPartyNumber: TO,
1922
+ actorTrust: { trustClass: "guardian", memberRecord: null },
1923
+ },
1924
+ session: { initiatedFromConversationId: "conv-origin-1" },
1925
+ });
1926
+ await session.whenSetupSettled();
1927
+ expect(registerCallController).not.toHaveBeenCalled();
1928
+
1929
+ enterDigits(session, "654321");
1930
+ await sleep();
1931
+
1932
+ expect(mockAttemptVerificationCode).toHaveBeenCalledWith(
1933
+ expect.objectContaining({ enteredCode: "654321", isOutbound: true }),
1934
+ );
1935
+ expect(postPointerMessageSafe).toHaveBeenCalledWith(
1936
+ "conv-origin-1",
1937
+ "verification_succeeded",
1938
+ TO,
1939
+ expect.objectContaining({ channel: "phone" }),
1940
+ );
1941
+ expect(mockStartPostVerificationGreeting).toHaveBeenCalled();
1942
+ expect(mockStartInitialGreeting).not.toHaveBeenCalled();
1943
+ expect(registerCallController).toHaveBeenCalledWith(
1944
+ "call-outverif",
1945
+ expect.anything(),
1946
+ );
1947
+
1948
+ session.destroy();
1949
+ });
1950
+ });
1951
+
1952
+ describe("callee verification", () => {
1953
+ test("posts the code to the origin conversation, retries a wrong code, greets on a match", async () => {
1954
+ const { session } = setupCall({
1955
+ callId: "call-callee",
1956
+ outcome: {
1957
+ action: "callee_verification",
1958
+ verificationConfig: { maxAttempts: 3, codeLength: 4 },
1959
+ },
1960
+ resolved: {
1961
+ isInbound: false,
1962
+ otherPartyNumber: TO,
1963
+ actorTrust: { trustClass: "guardian", memberRecord: null },
1964
+ },
1965
+ session: { initiatedFromConversationId: "conv-origin-2" },
1966
+ });
1967
+ await session.whenSetupSettled();
1968
+
1969
+ // The generated code is posted to the initiating conversation.
1970
+ expect(mockAddMessage).toHaveBeenCalledTimes(1);
1971
+ const [postedConvId, , postedContent] = mockAddMessage.mock
1972
+ .calls[0] as unknown as [string, string, string];
1973
+ expect(postedConvId).toBe("conv-origin-2");
1974
+ const code = /: (\d{4})/.exec(postedContent)?.[1];
1975
+ expect(code).toBeDefined();
1976
+
1977
+ // A wrong code re-prompts without ending the call.
1978
+ const wrongCode = code === "0000" ? "1111" : "0000";
1979
+ enterDigits(session, wrongCode);
1980
+ await sleep();
1981
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
1982
+ expect.anything(),
1983
+ "That code was incorrect. Please try again.",
1984
+ );
1985
+ expect(registerCallController).not.toHaveBeenCalled();
1986
+
1987
+ // The right code proceeds to the initial greeting.
1988
+ enterDigits(session, code!);
1989
+ await sleep();
1990
+ expect(registerCallController).toHaveBeenCalledWith(
1991
+ "call-callee",
1992
+ expect.anything(),
1993
+ );
1994
+ expect(mockStartInitialGreeting).toHaveBeenCalled();
1995
+ expect(finalizeCall).not.toHaveBeenCalled();
1996
+
1997
+ session.destroy();
1998
+ });
1999
+ });
2000
+
2001
+ describe("invite redemption", () => {
2002
+ test("success speaks the handoff, marks the opening ack, and replays deferred transcripts", async () => {
2003
+ const { session } = setupCall({
2004
+ callId: "call-invite",
2005
+ outcome: {
2006
+ action: "invite_redemption",
2007
+ assistantId: "self",
2008
+ fromNumber: FROM,
2009
+ inviteeName: "Casey Example",
2010
+ },
2011
+ });
2012
+ await session.whenSetupSettled();
2013
+
2014
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2015
+ expect.anything(),
2016
+ expect.stringContaining("Welcome Casey."),
2017
+ );
2018
+
2019
+ // Hold mid-setup trust re-resolution open so a transcript arrives
2020
+ // during the deferral window.
2021
+ let releaseVerdict!: () => void;
2022
+ mockVerdictGate = new Promise<void>((resolve) => {
2023
+ releaseVerdict = resolve;
2024
+ });
2025
+
2026
+ enterDigits(session, "111222");
2027
+ await sleep();
2028
+ expect(mockAttemptInviteCodeRedemption).toHaveBeenCalledWith(
2029
+ expect.objectContaining({ enteredCode: "111222" }),
2030
+ );
2031
+
2032
+ // Caller speaks while the trust upgrade is in flight — deferred, but
2033
+ // the transcript notifier still fires for UI subscribers.
2034
+ deliverTranscript(session, "hi can you help me book a flight");
2035
+ expect(fireCallTranscriptNotifier).toHaveBeenCalledWith(
2036
+ "conv-call-invite",
2037
+ "call-invite",
2038
+ "caller",
2039
+ "hi can you help me book a flight",
2040
+ );
2041
+ expect(mockHandleCallerUtterance).not.toHaveBeenCalled();
2042
+
2043
+ releaseVerdict();
2044
+ await sleep();
2045
+
2046
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2047
+ expect.anything(),
2048
+ expect.stringContaining("verified that you are Casey"),
2049
+ );
2050
+ expect(mockMarkNextCallerTurnAsOpeningAck).toHaveBeenCalled();
2051
+ expect(mockStartInitialGreeting).not.toHaveBeenCalled();
2052
+ expect(registerCallController).toHaveBeenCalledWith(
2053
+ "call-invite",
2054
+ expect.anything(),
2055
+ );
2056
+ // The deferred transcript replays into the controller after the opener.
2057
+ expect(mockHandleCallerUtterance).toHaveBeenCalledWith(
2058
+ "hi can you help me book a flight",
2059
+ );
2060
+
2061
+ session.destroy();
2062
+ });
2063
+
2064
+ test("failure speaks the failure copy and finalizes exactly once", async () => {
2065
+ mockInviteResult = {
2066
+ outcome: "failure",
2067
+ ttsMessage: "That code is not valid. Goodbye.",
2068
+ };
2069
+ const { session, mockWs } = setupCall({
2070
+ callId: "call-invite-fail",
2071
+ outcome: {
2072
+ action: "invite_redemption",
2073
+ assistantId: "self",
2074
+ fromNumber: FROM,
2075
+ inviteeName: null,
2076
+ },
2077
+ });
2078
+ await session.whenSetupSettled();
2079
+
2080
+ enterDigits(session, "999999");
2081
+ await sleep();
2082
+
2083
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2084
+ expect.anything(),
2085
+ "That code is not valid. Goodbye.",
2086
+ );
2087
+ expect(updateCallSession).toHaveBeenCalledWith(
2088
+ "call-invite-fail",
2089
+ expect.objectContaining({ status: "failed" }),
2090
+ );
2091
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2092
+ expect(registerCallController).not.toHaveBeenCalled();
2093
+
2094
+ await sleep(30);
2095
+ expect(mockWs.closed).toBe(true);
2096
+ session.handleTransportClosed(1000, "session-ended");
2097
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2098
+
2099
+ session.destroy();
2100
+ });
2101
+ });
2102
+
2103
+ describe("name capture and guardian wait", () => {
2104
+ function nameCaptureOutcome() {
2105
+ return {
2106
+ action: "name_capture",
2107
+ assistantId: "self",
2108
+ fromNumber: FROM,
2109
+ };
2110
+ }
2111
+
2112
+ test("waits for the guardian with NO controller, then approval hands off", async () => {
2113
+ const { session } = setupCall({
2114
+ callId: "call-name-ok",
2115
+ outcome: nameCaptureOutcome(),
2116
+ });
2117
+ await session.whenSetupSettled();
2118
+
2119
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2120
+ expect.anything(),
2121
+ expect.stringContaining("Can I get your name?"),
2122
+ );
2123
+ expect(session.getSetupFlow()?.getState()).toBe("capturing_name");
2124
+
2125
+ mockCanonicalRequest = { status: "pending" };
2126
+ deliverTranscript(session, "Casey Example");
2127
+ await sleep();
2128
+
2129
+ expect(mockNotifyGuardianOfAccessRequest).toHaveBeenCalledWith(
2130
+ expect.objectContaining({ actorDisplayName: "Casey Example" }),
2131
+ );
2132
+ // The transcript notifier fires for caller turns during setup.
2133
+ expect(fireCallTranscriptNotifier).toHaveBeenCalledWith(
2134
+ "conv-call-name-ok",
2135
+ "call-name-ok",
2136
+ "caller",
2137
+ "Casey Example",
2138
+ );
2139
+ expect(updateCallSession).toHaveBeenCalledWith("call-name-ok", {
2140
+ status: "waiting_on_user",
2141
+ });
2142
+ // No controller exists during the wait — no silence nudges can fire.
2143
+ expect(session.getSetupFlow()?.getState()).toBe(
2144
+ "awaiting_guardian_decision",
2145
+ );
2146
+ expect(session.getController()).toBeNull();
2147
+ expect(CallController as unknown as jest.Mock).not.toHaveBeenCalled();
2148
+ expect(registerCallController).not.toHaveBeenCalled();
2149
+
2150
+ // The guardian approves; the wait poll picks it up.
2151
+ mockCanonicalRequest = { status: "approved" };
2152
+ await sleep(40);
2153
+
2154
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2155
+ expect.anything(),
2156
+ "Great! Alex said I can speak with you. How can I help?",
2157
+ );
2158
+ expect(mockMarkNextCallerTurnAsOpeningAck).toHaveBeenCalled();
2159
+ expect(registerCallController).toHaveBeenCalledWith(
2160
+ "call-name-ok",
2161
+ expect.anything(),
2162
+ );
2163
+ expect(session.getSetupFlow()).toBeNull();
2164
+ expect(finalizeCall).not.toHaveBeenCalled();
2165
+
2166
+ session.destroy();
2167
+ });
2168
+
2169
+ test("guardian denial speaks the goodbye copy and finalizes exactly once", async () => {
2170
+ const { session, mockWs } = setupCall({
2171
+ callId: "call-name-deny",
2172
+ outcome: nameCaptureOutcome(),
2173
+ });
2174
+ await session.whenSetupSettled();
2175
+
2176
+ mockCanonicalRequest = { status: "pending" };
2177
+ deliverTranscript(session, "Casey Example");
2178
+ await sleep();
2179
+
2180
+ mockCanonicalRequest = { status: "denied" };
2181
+ await sleep(40);
2182
+
2183
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2184
+ expect.anything(),
2185
+ "Sorry, Alex says I'm not allowed to speak with you. Goodbye.",
2186
+ );
2187
+ expect(updateCallSession).toHaveBeenCalledWith(
2188
+ "call-name-deny",
2189
+ expect.objectContaining({ status: "failed" }),
2190
+ );
2191
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2192
+ expect(registerCallController).not.toHaveBeenCalled();
2193
+
2194
+ await sleep(30);
2195
+ expect(mockWs.closed).toBe(true);
2196
+ session.handleTransportClosed(1000, "session-ended");
2197
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2198
+
2199
+ session.destroy();
2200
+ });
2201
+
2202
+ test("guardian-wait timeout speaks the timeout copy and finalizes exactly once", async () => {
2203
+ mockUserConsultationTimeoutMs = 40;
2204
+ const { session } = setupCall({
2205
+ callId: "call-name-timeout",
2206
+ outcome: nameCaptureOutcome(),
2207
+ });
2208
+ await session.whenSetupSettled();
2209
+
2210
+ mockCanonicalRequest = { status: "pending" };
2211
+ deliverTranscript(session, "Casey Example");
2212
+ await sleep();
2213
+
2214
+ await sleep(80);
2215
+
2216
+ expect(mockEmitCallbackHandoff).toHaveBeenCalledWith(
2217
+ expect.objectContaining({ reason: "timeout" }),
2218
+ );
2219
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2220
+ expect.anything(),
2221
+ expect.stringContaining("Sorry, I can't get ahold of Alex right now."),
2222
+ );
2223
+ expect(updateCallSession).toHaveBeenCalledWith(
2224
+ "call-name-timeout",
2225
+ expect.objectContaining({ status: "failed" }),
2226
+ );
2227
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2228
+ expect(registerCallController).not.toHaveBeenCalled();
2229
+
2230
+ session.destroy();
2231
+ });
2232
+
2233
+ test("disconnect mid-wait disposes the flow, emits the callback handoff, and finalizes once", async () => {
2234
+ const { session } = setupCall({
2235
+ callId: "call-name-drop",
2236
+ outcome: nameCaptureOutcome(),
2237
+ });
2238
+ await session.whenSetupSettled();
2239
+
2240
+ mockCanonicalRequest = { status: "pending" };
2241
+ deliverTranscript(session, "Casey Example");
2242
+ await sleep();
2243
+ expect(session.getSetupFlow()?.getState()).toBe(
2244
+ "awaiting_guardian_decision",
2245
+ );
2246
+
2247
+ session.handleTransportClosed(1006, "network dropped");
2248
+
2249
+ expect(mockEmitCallbackHandoff).toHaveBeenCalledWith(
2250
+ expect.objectContaining({ reason: "transport_closed" }),
2251
+ );
2252
+ expect(updateCallSession).toHaveBeenCalledWith(
2253
+ "call-name-drop",
2254
+ expect.objectContaining({ status: "failed" }),
2255
+ );
2256
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2257
+ expect(session.getSetupFlow()).toBeNull();
2258
+
2259
+ // A late approval must not resurrect the torn-down call.
2260
+ mockCanonicalRequest = { status: "approved" };
2261
+ await sleep(40);
2262
+ expect(registerCallController).not.toHaveBeenCalled();
2263
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2264
+
2265
+ session.destroy();
2266
+ });
2267
+ });
2268
+
2269
+ describe("unverified caller", () => {
2270
+ test("hears the verification guidance and the call ends without a controller", async () => {
2271
+ const { session, mockWs } = setupCall({
2272
+ callId: "call-unverified",
2273
+ outcome: {
2274
+ action: "unverified_caller",
2275
+ assistantId: "self",
2276
+ fromNumber: FROM,
2277
+ displayName: "Sam Example",
2278
+ isGuardian: false,
2279
+ },
2280
+ });
2281
+ await session.whenSetupSettled();
2282
+
2283
+ expect(speakSystemPrompt).toHaveBeenCalledWith(
2284
+ expect.anything(),
2285
+ expect.stringContaining("registered as Sam Example's phone"),
2286
+ );
2287
+ expect(
2288
+ mockEvents.some(
2289
+ (e) =>
2290
+ e.callSessionId === "call-unverified" &&
2291
+ e.eventType === "inbound_acl_unverified_caller",
2292
+ ),
2293
+ ).toBe(true);
2294
+ expect(updateCallSession).toHaveBeenCalledWith(
2295
+ "call-unverified",
2296
+ expect.objectContaining({ status: "failed" }),
2297
+ );
2298
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2299
+ expect(registerCallController).not.toHaveBeenCalled();
2300
+
2301
+ await sleep(30);
2302
+ expect(mockWs.closed).toBe(true);
2303
+
2304
+ session.destroy();
2305
+ });
2306
+ });
2307
+
2308
+ describe("hangup during terminal setup speech", () => {
2309
+ /** Gate the next speakSystemPrompt call on a manually released promise. */
2310
+ function gateNextSpeech(): () => void {
2311
+ let release!: () => void;
2312
+ (speakSystemPrompt as jest.Mock).mockImplementationOnce(
2313
+ () =>
2314
+ new Promise<void>((resolve) => {
2315
+ release = resolve;
2316
+ }),
2317
+ );
2318
+ return () => release();
2319
+ }
2320
+
2321
+ test("hangup during the deny goodbye finalizes exactly once and revokes grants", async () => {
2322
+ const releaseSpeech = gateNextSpeech();
2323
+ const { session } = setupCall({
2324
+ callId: "call-deny-hangup",
2325
+ outcome: {
2326
+ action: "deny",
2327
+ message: "Not authorized.",
2328
+ logReason: "ACL deny",
2329
+ },
2330
+ });
2331
+ await sleep();
2332
+
2333
+ // The deny path sets terminal status before its goodbye finishes.
2334
+ expect(updateCallSession).toHaveBeenCalledWith(
2335
+ "call-deny-hangup",
2336
+ expect.objectContaining({ status: "failed" }),
2337
+ );
2338
+ expect(finalizeCall).not.toHaveBeenCalled();
2339
+
2340
+ // Caller hangs up while the goodbye is still speaking.
2341
+ session.handleTransportClosed(1006, "caller hung up");
2342
+
2343
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2344
+ expect(finalizeCall).toHaveBeenCalledWith(
2345
+ "call-deny-hangup",
2346
+ "conv-call-deny-hangup",
2347
+ );
2348
+ expect(revokeScopedApprovalGrantsForContext).toHaveBeenCalledWith({
2349
+ callSessionId: "call-deny-hangup",
2350
+ });
2351
+ expect(revokeScopedApprovalGrantsForContext).toHaveBeenCalledWith({
2352
+ conversationId: "conv-call-deny-hangup",
2353
+ });
2354
+ expect(session.getSetupFlow()).toBeNull();
2355
+
2356
+ // The speech promise settling later must not double-finalize.
2357
+ releaseSpeech();
2358
+ await session.whenSetupSettled();
2359
+ await sleep();
2360
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2361
+ expect(registerCallController).not.toHaveBeenCalled();
2362
+
2363
+ session.destroy();
2364
+ });
2365
+
2366
+ test("hangup during the guardian-denial goodbye finalizes exactly once and revokes grants", async () => {
2367
+ const { session } = setupCall({
2368
+ callId: "call-guard-deny-hangup",
2369
+ outcome: {
2370
+ action: "name_capture",
2371
+ assistantId: "self",
2372
+ fromNumber: FROM,
2373
+ },
2374
+ });
2375
+ await session.whenSetupSettled();
2376
+
2377
+ mockCanonicalRequest = { status: "pending" };
2378
+ deliverTranscript(session, "Casey Example");
2379
+ await sleep();
2380
+
2381
+ const releaseSpeech = gateNextSpeech();
2382
+ mockCanonicalRequest = { status: "denied" };
2383
+ await sleep(40);
2384
+
2385
+ expect(updateCallSession).toHaveBeenCalledWith(
2386
+ "call-guard-deny-hangup",
2387
+ expect.objectContaining({ status: "failed" }),
2388
+ );
2389
+ expect(finalizeCall).not.toHaveBeenCalled();
2390
+
2391
+ // Caller hangs up while the denial goodbye is still speaking.
2392
+ session.handleTransportClosed(1006, "caller hung up");
2393
+
2394
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2395
+ expect(finalizeCall).toHaveBeenCalledWith(
2396
+ "call-guard-deny-hangup",
2397
+ "conv-call-guard-deny-hangup",
2398
+ );
2399
+ expect(revokeScopedApprovalGrantsForContext).toHaveBeenCalledWith({
2400
+ callSessionId: "call-guard-deny-hangup",
2401
+ });
2402
+ expect(session.getSetupFlow()).toBeNull();
2403
+
2404
+ releaseSpeech();
2405
+ await sleep();
2406
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2407
+ expect(registerCallController).not.toHaveBeenCalled();
2408
+
2409
+ session.destroy();
2410
+ });
2411
+
2412
+ test("deny completion without a hangup finalizes exactly once; a later close does not re-finalize", async () => {
2413
+ const { session, mockWs } = setupCall({
2414
+ callId: "call-deny-normal",
2415
+ outcome: {
2416
+ action: "deny",
2417
+ message: "Not authorized.",
2418
+ logReason: "ACL deny",
2419
+ },
2420
+ });
2421
+ await session.whenSetupSettled();
2422
+
2423
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2424
+ expect(finalizeCall).toHaveBeenCalledWith(
2425
+ "call-deny-normal",
2426
+ "conv-call-deny-normal",
2427
+ );
2428
+
2429
+ // The flow schedules its own delayed endSession.
2430
+ await sleep(30);
2431
+ expect(mockWs.closed).toBe(true);
2432
+
2433
+ session.handleTransportClosed(1000, "session-ended");
2434
+ expect(finalizeCall).toHaveBeenCalledTimes(1);
2435
+
2436
+ session.destroy();
2437
+ });
2438
+ });
2439
+ });