@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
@@ -0,0 +1,1743 @@
1
+ /**
2
+ * Transport-agnostic call setup flow.
3
+ *
4
+ * Runs the pre-conversation phase of a phone call — acting on the routing
5
+ * outcome produced by `routeSetup` (relay-setup-router.ts) — against any
6
+ * {@link SetupFlowTransport}. All side effects (speech, call-store writes,
7
+ * completion) flow through injected deps so the flow is unit-testable and
8
+ * independent of any wire protocol.
9
+ *
10
+ * Handles `normal_call`, `deny`, the three verification actions
11
+ * (`verification`, `outbound_verification`, `callee_verification`),
12
+ * `invite_redemption`, `name_capture` (delegating the guardian-decision
13
+ * wait to {@link GuardianWaitController}), and `unverified_caller`.
14
+ */
15
+
16
+ import { randomInt } from "node:crypto";
17
+
18
+ import { getGuardianDeliveryFresh } from "../contacts/guardian-delivery-reader.js";
19
+ import type { TrustContext } from "../daemon/trust-context.js";
20
+ import type { addMessage as addMessageFn } from "../persistence/conversation-crud.js";
21
+ import { notifyGuardianOfAccessRequest as notifyGuardianOfAccessRequestImpl } from "../runtime/access-request-helper.js";
22
+ import {
23
+ resolveActorTrust,
24
+ toTrustContext,
25
+ } from "../runtime/actor-trust-resolver.js";
26
+ import {
27
+ trustContextFromVerdict,
28
+ verdictHasMemberIdentity,
29
+ verdictMemberUnresolvable,
30
+ } from "../runtime/trust-verdict-consumer.js";
31
+ import {
32
+ composeVerificationVoice,
33
+ GUARDIAN_VERIFY_TEMPLATE_KEYS,
34
+ } from "../runtime/verification-templates.js";
35
+ import { getLogger } from "../util/logger.js";
36
+ import { getTtsPlaybackDelayMs } from "./call-constants.js";
37
+ import type { postPointerMessageSafe as postPointerMessageSafeFn } from "./call-pointer-messages.js";
38
+ import type {
39
+ SetupFlowResult,
40
+ SetupFlowState,
41
+ SetupFlowTransport,
42
+ } from "./call-setup-flow-types.js";
43
+ import type { fireCallTranscriptNotifier as fireCallTranscriptNotifierFn } from "./call-state.js";
44
+ import type {
45
+ recordCallEvent as recordCallEventFn,
46
+ updateCallSession as updateCallSessionFn,
47
+ } from "./call-store.js";
48
+ import type { finalizeCall as finalizeCallFn } from "./finalize-call.js";
49
+ import {
50
+ GuardianWaitController,
51
+ type GuardianWaitControllerDeps,
52
+ type GuardianWaitDisposeReason,
53
+ type GuardianWaitResolutionContext,
54
+ } from "./guardian-wait-controller.js";
55
+ import { getPhoneCallerVerdict } from "./inbound-trust-reader.js";
56
+ import type { SetupOutcome, SetupResolved } from "./relay-setup-router.js";
57
+ import {
58
+ attemptInviteCodeRedemption as attemptInviteCodeRedemptionImpl,
59
+ attemptVerificationCode as attemptVerificationCodeImpl,
60
+ parseDigitsFromSpeech,
61
+ } from "./relay-verification.js";
62
+
63
+ const log = getLogger("call-setup-flow");
64
+
65
+ const INVITE_CODE_LENGTH = 6;
66
+
67
+ // 30-second name-capture window — enough time to speak a name but short
68
+ // enough to avoid keeping the call open for callers who never respond.
69
+ const NAME_CAPTURE_TIMEOUT_MS = 30_000;
70
+
71
+ // ── Errors ───────────────────────────────────────────────────────────
72
+
73
+ /** Thrown when `start()` receives a setup action the flow does not implement. */
74
+ export class UnsupportedSetupFlowError extends Error {
75
+ constructor(action: string) {
76
+ super(`Setup action '${action}' is not supported by CallSetupFlow`);
77
+ this.name = "UnsupportedSetupFlowError";
78
+ }
79
+ }
80
+
81
+ // ── Dependencies ─────────────────────────────────────────────────────
82
+
83
+ /** Structural subset of the call session that setup sub-flows read. */
84
+ export interface SetupFlowCallSession {
85
+ conversationId: string;
86
+ initiatedFromConversationId?: string | null;
87
+ toNumber?: string;
88
+ }
89
+
90
+ /**
91
+ * Side-effect surface injected into the flow. Sub-flows extend this
92
+ * interface with additional accessors rather than reshaping the
93
+ * constructor.
94
+ */
95
+ export interface CallSetupFlowDeps {
96
+ /** Speak a deterministic system prompt through the call transport. */
97
+ speakSystemPrompt(text: string): Promise<void>;
98
+ updateCallSession(
99
+ id: string,
100
+ updates: Parameters<typeof updateCallSessionFn>[1],
101
+ ): void;
102
+ recordCallEvent(
103
+ callSessionId: string,
104
+ eventType: Parameters<typeof recordCallEventFn>[1],
105
+ payload?: Record<string, unknown>,
106
+ ): void;
107
+ /** Invoked exactly once when the flow reaches a terminal result. */
108
+ onComplete(result: SetupFlowResult): void;
109
+ /**
110
+ * Delay between speaking terminal copy and ending the session.
111
+ * Defaults to the configured TTS playback delay.
112
+ */
113
+ ttsPlaybackDelayMs?: number;
114
+
115
+ // ── Verification / invite sub-flow deps ────────────────────────────
116
+ // Optional so callers exercising only normal_call/deny need not supply
117
+ // them. Each sub-flow resolves the deps it needs up front at start()
118
+ // via requireDeps, which applies defaults for the pure logic and throws
119
+ // on a missing side-effect dep.
120
+ /** Look up the call session (used for its conversation/routing fields). */
121
+ getCallSession?(id: string): SetupFlowCallSession | null;
122
+ finalizeCall?: typeof finalizeCallFn;
123
+ addMessage?: typeof addMessageFn;
124
+ /** Fire-and-forget pointer write tolerating an evicted conversation. */
125
+ postPointerMessage?: typeof postPointerMessageSafeFn;
126
+ fireCallTranscriptNotifier?: typeof fireCallTranscriptNotifierFn;
127
+ /** Human-readable guardian label for prompts and handoff copy. */
128
+ resolveGuardianLabel?(): string;
129
+ /** Assistant display name, or null when unavailable. */
130
+ resolveAssistantLabel?(): string | null;
131
+ /** Defaults to {@link attemptVerificationCodeImpl} (relay-verification). */
132
+ attemptVerificationCode?: typeof attemptVerificationCodeImpl;
133
+ /** Gateway-native invite claim. Defaults to relay-verification's. */
134
+ attemptInviteCodeRedemption?: typeof attemptInviteCodeRedemptionImpl;
135
+ /**
136
+ * Re-resolve caller trust after a successful mid-setup activation
137
+ * (verdict-first with local fallback). Defaults to
138
+ * {@link resolveMidCallTrustContext}; errors fail soft to the
139
+ * setup-time trust.
140
+ */
141
+ resolveMidCallTrustContext?(
142
+ assistantId: string,
143
+ fromNumber: string,
144
+ ): Promise<TrustContext>;
145
+
146
+ // ── Name-capture sub-flow deps ──────────────────────────────────────
147
+ /**
148
+ * Create the canonical access request and notify the guardian. Defaults
149
+ * to access-request-helper's {@link notifyGuardianOfAccessRequestImpl}.
150
+ */
151
+ notifyGuardianOfAccessRequest?: typeof notifyGuardianOfAccessRequestImpl;
152
+ /**
153
+ * Guardian wait controller factory, injectable so tests can substitute
154
+ * a fake. Defaults to constructing a real {@link GuardianWaitController}.
155
+ */
156
+ createGuardianWaitController?(
157
+ callSessionId: string,
158
+ deps: GuardianWaitControllerDeps,
159
+ ): GuardianWaitHandle;
160
+ /** Overrides the 30s name-capture response timeout. */
161
+ nameCaptureTimeoutMs?: number;
162
+ }
163
+
164
+ /** Structural subset of {@link GuardianWaitController} the flow drives. */
165
+ export type GuardianWaitHandle = Pick<
166
+ GuardianWaitController,
167
+ "start" | "handleTranscript" | "dispose"
168
+ >;
169
+
170
+ /** Verification deps with defaults applied and optionality removed. */
171
+ type VerificationDeps = Required<
172
+ Pick<
173
+ CallSetupFlowDeps,
174
+ | "getCallSession"
175
+ | "finalizeCall"
176
+ | "addMessage"
177
+ | "postPointerMessage"
178
+ | "fireCallTranscriptNotifier"
179
+ | "resolveGuardianLabel"
180
+ | "resolveAssistantLabel"
181
+ | "attemptVerificationCode"
182
+ | "resolveMidCallTrustContext"
183
+ >
184
+ >;
185
+
186
+ /** Invite-redemption deps with defaults applied and optionality removed. */
187
+ type InviteDeps = Required<
188
+ Pick<
189
+ CallSetupFlowDeps,
190
+ | "getCallSession"
191
+ | "finalizeCall"
192
+ | "fireCallTranscriptNotifier"
193
+ | "resolveGuardianLabel"
194
+ | "resolveAssistantLabel"
195
+ | "attemptInviteCodeRedemption"
196
+ | "resolveMidCallTrustContext"
197
+ >
198
+ >;
199
+
200
+ /** Name-capture deps with defaults applied and optionality removed. */
201
+ type NameCaptureDeps = Required<
202
+ Pick<
203
+ CallSetupFlowDeps,
204
+ | "getCallSession"
205
+ | "fireCallTranscriptNotifier"
206
+ | "resolveGuardianLabel"
207
+ | "resolveAssistantLabel"
208
+ | "notifyGuardianOfAccessRequest"
209
+ | "createGuardianWaitController"
210
+ | "resolveMidCallTrustContext"
211
+ >
212
+ >;
213
+
214
+ /**
215
+ * Deps needed by the shared trusted-contact activation continuation —
216
+ * the common subset of {@link VerificationDeps} and {@link InviteDeps}.
217
+ */
218
+ type ActivationDeps = Pick<
219
+ VerificationDeps,
220
+ | "getCallSession"
221
+ | "fireCallTranscriptNotifier"
222
+ | "resolveGuardianLabel"
223
+ | "resolveAssistantLabel"
224
+ | "resolveMidCallTrustContext"
225
+ >;
226
+
227
+ // ── Module helpers ───────────────────────────────────────────────────
228
+
229
+ /**
230
+ * Real-implementation defaults for the optional pure-logic deps. Built per
231
+ * call so the module live bindings are read at resolution time.
232
+ */
233
+ function setupFlowDepDefaults(): Partial<CallSetupFlowDeps> {
234
+ return {
235
+ attemptVerificationCode: attemptVerificationCodeImpl,
236
+ attemptInviteCodeRedemption: attemptInviteCodeRedemptionImpl,
237
+ notifyGuardianOfAccessRequest: notifyGuardianOfAccessRequestImpl,
238
+ resolveMidCallTrustContext,
239
+ createGuardianWaitController: (callSessionId, deps) =>
240
+ new GuardianWaitController(callSessionId, deps),
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Resolve a sub-flow's dep subset, applying {@link setupFlowDepDefaults}
246
+ * for omitted pure-logic deps and throwing a descriptive error naming any
247
+ * missing side-effect dep.
248
+ */
249
+ function requireDeps<K extends keyof CallSetupFlowDeps>(
250
+ kind: string,
251
+ deps: CallSetupFlowDeps,
252
+ names: readonly K[],
253
+ ): Required<Pick<CallSetupFlowDeps, K>> {
254
+ const defaults = setupFlowDepDefaults();
255
+ const resolved: Partial<Record<keyof CallSetupFlowDeps, unknown>> = {};
256
+ const missing: string[] = [];
257
+ for (const name of names) {
258
+ const value = deps[name] ?? defaults[name];
259
+ if (value == null) {
260
+ missing.push(name);
261
+ } else {
262
+ resolved[name] = value;
263
+ }
264
+ }
265
+ if (missing.length > 0) {
266
+ throw new Error(
267
+ `CallSetupFlow ${kind} deps missing: ${missing.join(", ")}`,
268
+ );
269
+ }
270
+ return resolved as Required<Pick<CallSetupFlowDeps, K>>;
271
+ }
272
+
273
+ /**
274
+ * Re-resolve caller trust after a mid-setup verification/activation. Prefers
275
+ * the gateway verdict (authoritative right after the gateway updated the
276
+ * binding); falls back to local resolution on a missing/failed/unusable
277
+ * verdict so a blip never drops the call. Mirrors the setup path's
278
+ * verdict-first-with-fallback condition.
279
+ */
280
+ export async function resolveMidCallTrustContext(
281
+ assistantId: string,
282
+ fromNumber: string,
283
+ ): Promise<TrustContext> {
284
+ const verdict = await getPhoneCallerVerdict(fromNumber);
285
+
286
+ // Only a MEMBERLESS unknown verdict is treated as a stale gateway view and
287
+ // falls back to local: the caller was just activated, and invite redemption
288
+ // writes the channel assistant-side, so the gateway may not see the member
289
+ // yet — local resolution has it. A MEMBERFUL unknown verdict (blocked/revoked
290
+ // member, carrying contactId/channelId) is honored so its deny ACL is
291
+ // enforced; falling back could lose the gateway's member status if local
292
+ // state is stale.
293
+ const memberlessUnknown =
294
+ verdict?.trustClass === "unknown" && !verdictHasMemberIdentity(verdict);
295
+ const usable =
296
+ verdict &&
297
+ !verdict.resolutionFailed &&
298
+ !verdictMemberUnresolvable(verdict) &&
299
+ !memberlessUnknown;
300
+
301
+ if (usable) {
302
+ return trustContextFromVerdict(verdict, {
303
+ sourceChannel: "phone",
304
+ conversationExternalId: fromNumber,
305
+ });
306
+ }
307
+
308
+ // Warm the phone-channel guardian-delivery cache before the SYNC
309
+ // resolveActorTrust fallback, which reads the IO-free per-channel snapshot
310
+ // that daemon startup leaves cold for `phone`. Read fresh: gateway-side
311
+ // binding writes don't invalidate the daemon cache, so a stale empty
312
+ // snapshot would otherwise survive the TTL and misclassify the guardian.
313
+ await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
314
+
315
+ return toTrustContext(
316
+ resolveActorTrust({
317
+ assistantId,
318
+ sourceChannel: "phone",
319
+ conversationExternalId: fromNumber,
320
+ actorExternalId: fromNumber,
321
+ }),
322
+ fromNumber,
323
+ );
324
+ }
325
+
326
+ /**
327
+ * Return the first whitespace-delimited token of a name, or `null` when the
328
+ * input is null/blank. Used for greetings so "Alice Example" -> "Alice".
329
+ */
330
+ function firstToken(name: string | null | undefined): string | null {
331
+ if (!name) {
332
+ return null;
333
+ }
334
+ const trimmed = name.trim();
335
+ if (!trimmed) {
336
+ return null;
337
+ }
338
+ return trimmed.split(/\s+/)[0] ?? null;
339
+ }
340
+
341
+ // ── Flow ─────────────────────────────────────────────────────────────
342
+
343
+ type VerificationMode =
344
+ | "inbound_verification"
345
+ | "outbound_verification"
346
+ | "callee_verification";
347
+
348
+ /** Sub-flows that collect a numeric code from the caller. */
349
+ type CodeCollectionMode = VerificationMode | "invite_redemption";
350
+
351
+ interface InviteRedemptionState {
352
+ assistantId: string;
353
+ fromNumber: string;
354
+ inviteeName: string | null;
355
+ }
356
+
357
+ interface AccessRequestState {
358
+ assistantId: string;
359
+ fromNumber: string;
360
+ callerName: string | null;
361
+ requestId: string | null;
362
+ }
363
+
364
+ export class CallSetupFlow {
365
+ private state: SetupFlowState = "idle";
366
+ /** Set before dispatch so `start()` runs at most once per instance. */
367
+ private started = false;
368
+
369
+ // ── Code-collection state (shared by verification + invite) ────────
370
+ private codeMode: CodeCollectionMode | null = null;
371
+ /** Shared digit buffer for code collection (DTMF + spoken digits). */
372
+ private digitBuffer = "";
373
+ private codeLength = 6;
374
+ /**
375
+ * In-flight guard shared by every code-collection sub-flow: async
376
+ * validation reads and advances the attempt counter, so digits and
377
+ * spoken codes arriving while one submission is pending are dropped
378
+ * rather than launching a parallel attempt against the same counter.
379
+ * Set synchronously before dispatch and cleared when the attempt
380
+ * settles.
381
+ */
382
+ private codeSubmissionInFlight = false;
383
+ /** Setup-time trust, kept as the fallback when re-resolution fails. */
384
+ private initialTrustContext: TrustContext | null = null;
385
+ private trustReResolving = false;
386
+ private deferredTranscripts: string[] = [];
387
+
388
+ // ── Verification sub-flow state ─────────────────────────────────────
389
+ private vdeps: VerificationDeps | null = null;
390
+ private verificationAttempts = 0;
391
+ private verificationMaxAttempts = 3;
392
+ private verificationAssistantId = "";
393
+ private verificationFromNumber = "";
394
+ private outboundVerificationSessionId: string | null = null;
395
+ private calleeVerificationCode: string | null = null;
396
+
397
+ // ── Invite-redemption sub-flow state ────────────────────────────────
398
+ private ideps: InviteDeps | null = null;
399
+ private invite: InviteRedemptionState | null = null;
400
+
401
+ // ── Name-capture / guardian-wait sub-flow state ─────────────────────
402
+ private ndeps: NameCaptureDeps | null = null;
403
+ private accessRequest: AccessRequestState | null = null;
404
+ private nameCaptureTimer: ReturnType<typeof setTimeout> | null = null;
405
+ /**
406
+ * Busy guard: access-request creation and the capture-timeout teardown
407
+ * are async, and a final transcript arriving while either is pending
408
+ * must not be treated as a (second) name and fire a duplicate guardian
409
+ * notification.
410
+ */
411
+ private nameCaptureBusy = false;
412
+ private guardianWait: GuardianWaitHandle | null = null;
413
+
414
+ // ── Terminal bookkeeping ────────────────────────────────────────────
415
+ private endSessionTimer: ReturnType<typeof setTimeout> | null = null;
416
+ private finalized = false;
417
+ private disposed = false;
418
+
419
+ constructor(
420
+ private readonly callSessionId: string,
421
+ private readonly transport: SetupFlowTransport,
422
+ private readonly deps: CallSetupFlowDeps,
423
+ ) {}
424
+
425
+ /** Explicit flow state — never inferred from the transport. */
426
+ getState(): SetupFlowState {
427
+ return this.state;
428
+ }
429
+
430
+ /**
431
+ * Whether a flow-terminal path already ran `finalizeCall`. The owning
432
+ * server's transport-close handler checks this to keep finalization
433
+ * exactly-once across flow-terminal and transport-close paths.
434
+ */
435
+ hasFinalized(): boolean {
436
+ return this.finalized;
437
+ }
438
+
439
+ /**
440
+ * Tear down a flow whose transport disconnected mid-setup: clears the
441
+ * name-capture and end-session timers and disposes the guardian wait
442
+ * controller (which emits the callback handoff when the caller opted in
443
+ * and the reason is `transport_closed`). Idempotent; never fires
444
+ * `onComplete`.
445
+ */
446
+ dispose(reason: GuardianWaitDisposeReason = "teardown"): void {
447
+ if (this.disposed) {
448
+ return;
449
+ }
450
+ this.disposed = true;
451
+ this.clearNameCaptureTimer();
452
+ if (this.endSessionTimer) {
453
+ clearTimeout(this.endSessionTimer);
454
+ this.endSessionTimer = null;
455
+ }
456
+ this.guardianWait?.dispose(reason);
457
+ // Stop accepting input without emitting a terminal result — the
458
+ // transport is gone, so there is no continuation to run.
459
+ this.state = "completed";
460
+ }
461
+
462
+ /**
463
+ * Act on a routing outcome. Runs at most once per flow instance;
464
+ * terminal continuation is delivered via `deps.onComplete`.
465
+ */
466
+ async start(outcome: SetupOutcome, resolved: SetupResolved): Promise<void> {
467
+ // Mark started before dispatching: async paths (e.g. deny awaits TTS)
468
+ // stay in `idle` state until their terminal continuation, so the state
469
+ // check alone would admit a concurrent second start().
470
+ if (this.started || this.state !== "idle") {
471
+ throw new Error("CallSetupFlow.start() may only be called once");
472
+ }
473
+ this.started = true;
474
+
475
+ switch (outcome.action) {
476
+ case "normal_call":
477
+ this.complete({
478
+ kind: "proceed-initial-greeting",
479
+ assistantId: resolved.assistantId,
480
+ trustContext: toTrustContext(
481
+ resolved.actorTrust,
482
+ resolved.otherPartyNumber,
483
+ ),
484
+ });
485
+ return;
486
+
487
+ case "deny":
488
+ await this.runDeny(outcome, resolved);
489
+ return;
490
+
491
+ case "verification":
492
+ this.startInboundVerification(
493
+ outcome.assistantId,
494
+ outcome.fromNumber,
495
+ resolved,
496
+ );
497
+ return;
498
+
499
+ case "outbound_verification":
500
+ this.startOutboundVerification(
501
+ outcome.assistantId,
502
+ outcome.sessionId,
503
+ outcome.toNumber,
504
+ resolved,
505
+ );
506
+ return;
507
+
508
+ case "callee_verification":
509
+ await this.startCalleeVerification(
510
+ outcome.verificationConfig,
511
+ resolved,
512
+ );
513
+ return;
514
+
515
+ case "invite_redemption":
516
+ this.startInviteRedemption(outcome, resolved);
517
+ return;
518
+
519
+ case "name_capture":
520
+ this.startNameCapture(outcome, resolved);
521
+ return;
522
+
523
+ case "unverified_caller":
524
+ await this.runUnverifiedCaller(outcome);
525
+ return;
526
+
527
+ default:
528
+ throw new UnsupportedSetupFlowError((outcome as SetupOutcome).action);
529
+ }
530
+ }
531
+
532
+ // ── Caller input ────────────────────────────────────────────────────
533
+
534
+ /** Feed a DTMF digit to the active sub-flow. No-op while idle/completed. */
535
+ pushDtmfDigit(digit: string): void {
536
+ if (!this.acceptsInput()) {
537
+ return;
538
+ }
539
+ // codeMode is null during name capture and the guardian wait, so DTMF
540
+ // is ignored in those states.
541
+ if (this.codeMode == null) {
542
+ return;
543
+ }
544
+ // Digits arriving while a submitted code is still validating are
545
+ // dropped — buffering them would launch a parallel attempt.
546
+ if (this.codeSubmissionInFlight) {
547
+ return;
548
+ }
549
+ this.digitBuffer += digit;
550
+ if (this.digitBuffer.length < this.codeLength) {
551
+ return;
552
+ }
553
+ const enteredCode = this.digitBuffer.slice(0, this.codeLength);
554
+ this.digitBuffer = "";
555
+ this.submitCode(enteredCode);
556
+ }
557
+
558
+ /** Feed a final caller transcript to the active sub-flow. No-op while idle/completed. */
559
+ pushTranscriptFinal(text: string): void {
560
+ if (!this.acceptsInput()) {
561
+ return;
562
+ }
563
+ // Defer (don't drop) transcripts while trust is being re-resolved so a
564
+ // verified caller's first utterance runs under the upgraded context, not
565
+ // the stale pre-verification one. The drained buffer rides the terminal
566
+ // result for the owning server to replay in order.
567
+ if (this.trustReResolving) {
568
+ this.deferredTranscripts.push(text);
569
+ return;
570
+ }
571
+ // During name capture, the caller's response is their name. A blank
572
+ // transcript (silence/noise) keeps waiting; the capture timeout still
573
+ // fires if the caller never provides one.
574
+ if (this.state === "capturing_name") {
575
+ const callerName = text.trim();
576
+ if (!callerName || this.nameCaptureBusy) {
577
+ return;
578
+ }
579
+ log.info(
580
+ { callSessionId: this.callSessionId, callerName },
581
+ "Name captured from unknown inbound caller",
582
+ );
583
+ this.nameCaptureBusy = true;
584
+ void this.handleNameCaptureResponse(callerName)
585
+ .catch((err) =>
586
+ log.error(
587
+ { err, callSessionId: this.callSessionId },
588
+ "Name capture handling failed",
589
+ ),
590
+ )
591
+ .finally(() => {
592
+ this.nameCaptureBusy = false;
593
+ });
594
+ return;
595
+ }
596
+ // During the guardian decision wait, caller speech is classified by the
597
+ // wait controller (reassurance, impatience, callback offer/opt-in).
598
+ if (this.state === "awaiting_guardian_decision") {
599
+ this.guardianWait?.handleTranscript(text);
600
+ return;
601
+ }
602
+ if (this.codeMode == null) {
603
+ return;
604
+ }
605
+ // Spoken codes arriving while a submitted code is still validating
606
+ // are dropped — they would launch a parallel attempt.
607
+ if (this.codeSubmissionInFlight) {
608
+ return;
609
+ }
610
+ // Callee verification is DTMF-only — the callee should be entering
611
+ // digits on their keypad, not speaking.
612
+ if (this.codeMode === "callee_verification") {
613
+ return;
614
+ }
615
+ const spokenDigits = parseDigitsFromSpeech(text);
616
+ if (spokenDigits.length >= this.codeLength) {
617
+ this.submitCode(spokenDigits.slice(0, this.codeLength));
618
+ } else if (spokenDigits.length > 0) {
619
+ void this.deps.speakSystemPrompt(
620
+ `I heard ${spokenDigits.length} digits. Please enter all ${this.codeLength} digits of your code.`,
621
+ );
622
+ }
623
+ }
624
+
625
+ // ── Sub-flows ───────────────────────────────────────────────────────
626
+
627
+ /** Deny the call: record the ACL event, speak the denial, then hang up. */
628
+ private async runDeny(
629
+ outcome: Extract<SetupOutcome, { action: "deny" }>,
630
+ resolved: SetupResolved,
631
+ ): Promise<void> {
632
+ this.deps.recordCallEvent(this.callSessionId, "inbound_acl_denied", {
633
+ from: resolved.otherPartyNumber,
634
+ trustClass: resolved.actorTrust.trustClass,
635
+ channelId: resolved.actorTrust.memberRecord?.channel.id,
636
+ memberPolicy: resolved.actorTrust.memberRecord?.policy,
637
+ });
638
+ this.deps.updateCallSession(this.callSessionId, {
639
+ status: "failed",
640
+ endedAt: Date.now(),
641
+ lastError: outcome.logReason,
642
+ });
643
+ await this.deps.speakSystemPrompt(outcome.message);
644
+ this.endSessionAfterPlayback(outcome.logReason);
645
+ this.complete({ kind: "ended", reason: outcome.logReason });
646
+ }
647
+
648
+ // ── Verification sub-flows ──────────────────────────────────────────
649
+
650
+ /**
651
+ * Inbound guardian / trusted-contact verification: the caller has a
652
+ * pending voice challenge and must enter (or speak) their six-digit code.
653
+ */
654
+ private startInboundVerification(
655
+ assistantId: string,
656
+ fromNumber: string,
657
+ resolved: SetupResolved,
658
+ ): void {
659
+ this.enterCodeCollection("inbound_verification", resolved, {
660
+ maxAttempts: 3,
661
+ codeLength: 6,
662
+ });
663
+ this.verificationAssistantId = assistantId;
664
+ this.verificationFromNumber = fromNumber;
665
+
666
+ this.deps.recordCallEvent(
667
+ this.callSessionId,
668
+ "voice_verification_started",
669
+ {
670
+ assistantId,
671
+ maxAttempts: this.verificationMaxAttempts,
672
+ },
673
+ );
674
+
675
+ void this.deps.speakSystemPrompt(
676
+ "Welcome. Please enter your six-digit verification code using your keypad, or speak the digits now.",
677
+ );
678
+ }
679
+
680
+ /**
681
+ * Outbound guardian verification: the system called the guardian's phone;
682
+ * prompt them to enter the verification code via DTMF or speech.
683
+ */
684
+ private startOutboundVerification(
685
+ assistantId: string,
686
+ verificationSessionId: string,
687
+ toNumber: string,
688
+ resolved: SetupResolved,
689
+ ): void {
690
+ this.enterCodeCollection("outbound_verification", resolved, {
691
+ maxAttempts: 3,
692
+ codeLength: 6,
693
+ });
694
+ this.verificationAssistantId = assistantId;
695
+ // For outbound guardian calls, the "to" number is the guardian's phone.
696
+ this.verificationFromNumber = toNumber;
697
+ this.outboundVerificationSessionId = verificationSessionId;
698
+
699
+ this.deps.recordCallEvent(
700
+ this.callSessionId,
701
+ "outbound_voice_verification_started",
702
+ {
703
+ assistantId,
704
+ verificationSessionId,
705
+ maxAttempts: this.verificationMaxAttempts,
706
+ },
707
+ );
708
+
709
+ void this.deps.speakSystemPrompt(
710
+ composeVerificationVoice(GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_CALL_INTRO, {
711
+ codeDigits: this.codeLength,
712
+ }),
713
+ );
714
+ }
715
+
716
+ /**
717
+ * Outbound callee verification: generate a random code, post it to the
718
+ * initiating conversation so the guardian can share it with the callee,
719
+ * and prompt the callee to enter it. DTMF-only — speech is ignored.
720
+ */
721
+ private async startCalleeVerification(
722
+ verificationConfig: { maxAttempts: number; codeLength: number },
723
+ resolved: SetupResolved,
724
+ ): Promise<void> {
725
+ const vdeps = this.enterCodeCollection(
726
+ "callee_verification",
727
+ resolved,
728
+ verificationConfig,
729
+ );
730
+ this.verificationAssistantId = resolved.assistantId;
731
+
732
+ const maxValue = Math.pow(10, this.codeLength);
733
+ const code = randomInt(0, maxValue)
734
+ .toString()
735
+ .padStart(this.codeLength, "0");
736
+ this.calleeVerificationCode = code;
737
+
738
+ this.deps.recordCallEvent(
739
+ this.callSessionId,
740
+ "callee_verification_started",
741
+ {
742
+ codeLength: this.codeLength,
743
+ maxAttempts: this.verificationMaxAttempts,
744
+ },
745
+ );
746
+
747
+ const spokenCode = code.split("").join(". ");
748
+ void this.deps.speakSystemPrompt(
749
+ `Please enter the verification code: ${spokenCode}.`,
750
+ );
751
+
752
+ // Post the verification code to the initiating conversation so the
753
+ // guardian (user) can share it with the callee.
754
+ const session = vdeps.getCallSession(this.callSessionId);
755
+ if (session?.initiatedFromConversationId) {
756
+ const codeMsg = `\u{1F510} Verification code for call to ${session.toNumber}: ${code}`;
757
+ await vdeps.addMessage(
758
+ session.initiatedFromConversationId,
759
+ "assistant",
760
+ JSON.stringify([{ type: "text", text: codeMsg }]),
761
+ {
762
+ metadata: {
763
+ userMessageChannel: "phone",
764
+ assistantMessageChannel: "phone",
765
+ userMessageInterface: "phone",
766
+ assistantMessageInterface: "phone",
767
+ },
768
+ },
769
+ );
770
+ }
771
+ }
772
+
773
+ /** Shared entry bookkeeping for the verification code-collection sub-flows. */
774
+ private enterCodeCollection(
775
+ mode: VerificationMode,
776
+ resolved: SetupResolved,
777
+ config: { maxAttempts: number; codeLength: number },
778
+ ): VerificationDeps {
779
+ const vdeps = this.requireVerificationDeps();
780
+ this.vdeps = vdeps;
781
+ this.codeMode = mode;
782
+ this.verificationAttempts = 0;
783
+ this.verificationMaxAttempts = config.maxAttempts;
784
+ this.codeLength = config.codeLength;
785
+ this.digitBuffer = "";
786
+ this.initialTrustContext = toTrustContext(
787
+ resolved.actorTrust,
788
+ resolved.otherPartyNumber,
789
+ );
790
+ this.state = "collecting_code";
791
+ return vdeps;
792
+ }
793
+
794
+ /** Route a fully collected code to the active code-collection sub-flow. */
795
+ private submitCode(enteredCode: string): void {
796
+ this.codeSubmissionInFlight = true;
797
+ const handler =
798
+ this.codeMode === "invite_redemption"
799
+ ? this.handleInviteCodeEntry(enteredCode)
800
+ : this.codeMode === "callee_verification"
801
+ ? this.handleCalleeCode(enteredCode)
802
+ : this.handleVerificationCode(enteredCode);
803
+ handler
804
+ .catch((err) =>
805
+ log.error(
806
+ { err, callSessionId: this.callSessionId },
807
+ "Setup code handling failed",
808
+ ),
809
+ )
810
+ .finally(() => {
811
+ this.codeSubmissionInFlight = false;
812
+ });
813
+ }
814
+
815
+ /**
816
+ * Validate an entered code against the pending voice guardian challenge
817
+ * (inbound and outbound guardian verification).
818
+ */
819
+ private async handleVerificationCode(enteredCode: string): Promise<void> {
820
+ const vdeps = this.vdeps;
821
+ if (!vdeps) {
822
+ return;
823
+ }
824
+ const isOutbound = this.outboundVerificationSessionId != null;
825
+ const assistantId = this.verificationAssistantId;
826
+ const fromNumber = this.verificationFromNumber;
827
+
828
+ const result = await vdeps.attemptVerificationCode({
829
+ verificationAssistantId: assistantId,
830
+ verificationFromNumber: fromNumber,
831
+ enteredCode,
832
+ isOutbound,
833
+ codeDigits: this.codeLength,
834
+ verificationAttempts: this.verificationAttempts,
835
+ verificationMaxAttempts: this.verificationMaxAttempts,
836
+ });
837
+
838
+ // A concurrent attempt may have already reached a terminal result.
839
+ if (this.state === "completed") {
840
+ return;
841
+ }
842
+
843
+ if (result.outcome === "success") {
844
+ this.codeMode = null;
845
+ this.digitBuffer = "";
846
+ this.verificationAttempts = 0;
847
+
848
+ this.deps.recordCallEvent(this.callSessionId, result.eventName, {
849
+ verificationType: result.verificationType,
850
+ });
851
+
852
+ if (isOutbound) {
853
+ await this.completeOutboundVerificationSuccess(
854
+ vdeps,
855
+ assistantId,
856
+ fromNumber,
857
+ );
858
+ } else if (result.verificationType === "trusted_contact") {
859
+ await this.continueAfterTrustedContactActivation(vdeps, {
860
+ assistantId,
861
+ fromNumber,
862
+ activationReason: "trusted_contact_verified",
863
+ });
864
+ } else {
865
+ // Inbound guardian verification — proceed to the normal call flow
866
+ // under the upgraded trust context.
867
+ const trustContext = await this.resolveTrustWithDeferral(
868
+ vdeps,
869
+ assistantId,
870
+ fromNumber,
871
+ );
872
+ this.complete({
873
+ kind: "proceed-initial-greeting",
874
+ assistantId,
875
+ trustContext,
876
+ deferredTranscripts: this.drainDeferredTranscripts(),
877
+ });
878
+ }
879
+ return;
880
+ }
881
+
882
+ if (result.outcome === "failure") {
883
+ this.codeMode = null;
884
+ this.verificationAttempts = result.attempts;
885
+
886
+ this.deps.recordCallEvent(this.callSessionId, result.eventName, {
887
+ attempts: result.attempts,
888
+ });
889
+ this.deps.updateCallSession(this.callSessionId, {
890
+ status: "failed",
891
+ endedAt: Date.now(),
892
+ lastError: "Guardian voice verification failed — max attempts exceeded",
893
+ });
894
+
895
+ const session = vdeps.getCallSession(this.callSessionId);
896
+ if (session) {
897
+ this.finalizeOnce(vdeps.finalizeCall, session.conversationId);
898
+
899
+ if (isOutbound && session.initiatedFromConversationId) {
900
+ vdeps.postPointerMessage(
901
+ session.initiatedFromConversationId,
902
+ "verification_failed",
903
+ session.toNumber ?? "",
904
+ { channel: "phone", reason: "Max verification attempts exceeded" },
905
+ );
906
+ }
907
+ }
908
+
909
+ await this.deps.speakSystemPrompt(result.ttsMessage);
910
+ this.endSessionAfterPlayback("Verification failed — challenge rejected");
911
+ this.complete({
912
+ kind: "ended",
913
+ reason: "Verification failed — challenge rejected",
914
+ });
915
+ return;
916
+ }
917
+
918
+ // Retry — re-prompt and keep collecting.
919
+ this.verificationAttempts = result.attempt;
920
+ void this.deps.speakSystemPrompt(result.ttsMessage);
921
+ }
922
+
923
+ /** Compare an entered code against the generated callee verification code. */
924
+ private async handleCalleeCode(enteredCode: string): Promise<void> {
925
+ const vdeps = this.vdeps;
926
+ if (!vdeps || this.calleeVerificationCode == null) {
927
+ return;
928
+ }
929
+
930
+ if (enteredCode === this.calleeVerificationCode) {
931
+ this.codeMode = null;
932
+ this.calleeVerificationCode = null;
933
+ this.verificationAttempts = 0;
934
+
935
+ this.deps.recordCallEvent(
936
+ this.callSessionId,
937
+ "callee_verification_succeeded",
938
+ {},
939
+ );
940
+ this.complete({
941
+ kind: "proceed-initial-greeting",
942
+ assistantId: this.verificationAssistantId,
943
+ trustContext: this.initialTrustContext!,
944
+ });
945
+ return;
946
+ }
947
+
948
+ this.verificationAttempts++;
949
+
950
+ if (this.verificationAttempts >= this.verificationMaxAttempts) {
951
+ this.codeMode = null;
952
+
953
+ this.deps.recordCallEvent(
954
+ this.callSessionId,
955
+ "callee_verification_failed",
956
+ {
957
+ attempts: this.verificationAttempts,
958
+ },
959
+ );
960
+ // Mark failed immediately so a transport close during the goodbye TTS
961
+ // window cannot race this into a terminal "completed" status.
962
+ this.deps.updateCallSession(this.callSessionId, {
963
+ status: "failed",
964
+ endedAt: Date.now(),
965
+ lastError: "Callee verification failed — max attempts exceeded",
966
+ });
967
+
968
+ const session = vdeps.getCallSession(this.callSessionId);
969
+ if (session) {
970
+ this.finalizeOnce(vdeps.finalizeCall, session.conversationId);
971
+ if (session.initiatedFromConversationId) {
972
+ vdeps.postPointerMessage(
973
+ session.initiatedFromConversationId,
974
+ "failed",
975
+ session.toNumber ?? "",
976
+ { reason: "Callee verification failed" },
977
+ );
978
+ }
979
+ }
980
+
981
+ // Wait for synthesis to complete before starting the teardown timer
982
+ // so the caller hears the goodbye message.
983
+ try {
984
+ await this.deps.speakSystemPrompt("Verification failed. Goodbye.");
985
+ } catch (err) {
986
+ log.error(
987
+ { err, callSessionId: this.callSessionId },
988
+ "System prompt TTS failed during verification teardown",
989
+ );
990
+ }
991
+ this.endSessionAfterPlayback("Verification failed");
992
+ this.complete({ kind: "ended", reason: "Verification failed" });
993
+ return;
994
+ }
995
+
996
+ void this.deps.speakSystemPrompt(
997
+ "That code was incorrect. Please try again.",
998
+ );
999
+ }
1000
+
1001
+ /**
1002
+ * Outbound guardian verification success: pointer back to the originating
1003
+ * conversation, trust upgrade, session to in_progress, then hand off to
1004
+ * the post-verification greeting.
1005
+ */
1006
+ private async completeOutboundVerificationSuccess(
1007
+ vdeps: VerificationDeps,
1008
+ assistantId: string,
1009
+ fromNumber: string,
1010
+ ): Promise<void> {
1011
+ const session = vdeps.getCallSession(this.callSessionId);
1012
+ if (session?.initiatedFromConversationId) {
1013
+ vdeps.postPointerMessage(
1014
+ session.initiatedFromConversationId,
1015
+ "verification_succeeded",
1016
+ session.toNumber ?? "",
1017
+ { channel: "phone" },
1018
+ );
1019
+ }
1020
+
1021
+ const trustContext = await this.resolveTrustWithDeferral(
1022
+ vdeps,
1023
+ assistantId,
1024
+ fromNumber,
1025
+ );
1026
+ // Same teardown race as the activation handoff: don't resurrect a
1027
+ // session whose transport closed during trust re-resolution.
1028
+ if (this.state === "completed") {
1029
+ return;
1030
+ }
1031
+ this.deps.updateCallSession(this.callSessionId, { status: "in_progress" });
1032
+ this.complete({
1033
+ kind: "proceed-post-verification-greeting",
1034
+ assistantId,
1035
+ trustContext,
1036
+ deferredTranscripts: this.drainDeferredTranscripts(),
1037
+ });
1038
+ }
1039
+
1040
+ /**
1041
+ * Shared post-activation handoff for all trusted-contact success paths
1042
+ * (invite redemption, access-request approval, verification code).
1043
+ * Upgrades trust, delivers deterministic transition copy, and completes
1044
+ * with `proceed-handoff-spoken` so the controller marks the next caller
1045
+ * turn as an opening ack. Returns false when the flow reached a terminal
1046
+ * state during trust re-resolution and no handoff was delivered.
1047
+ */
1048
+ private async continueAfterTrustedContactActivation(
1049
+ adeps: ActivationDeps,
1050
+ params: {
1051
+ assistantId: string;
1052
+ fromNumber: string;
1053
+ activationReason?:
1054
+ | "invite_redeemed"
1055
+ | "access_approved"
1056
+ | "trusted_contact_verified";
1057
+ /**
1058
+ * Display name resolved from the bound contact (or, for outbound invite
1059
+ * calls, the session's recorded invitee name). Greeting uses only the
1060
+ * first whitespace-delimited token; an empty/blank value triggers the
1061
+ * neutral "Hi there" greeting rather than substituting the channel
1062
+ * address.
1063
+ */
1064
+ inviteeName?: string | null;
1065
+ },
1066
+ ): Promise<boolean> {
1067
+ const { assistantId, fromNumber } = params;
1068
+
1069
+ this.deps.updateCallSession(this.callSessionId, { status: "in_progress" });
1070
+
1071
+ const trustContext = await this.resolveTrustWithDeferral(
1072
+ adeps,
1073
+ assistantId,
1074
+ fromNumber,
1075
+ );
1076
+
1077
+ // A transport close during trust re-resolution tears the flow down;
1078
+ // never speak or record a synthetic handoff on a dead call.
1079
+ if (this.state === "completed") {
1080
+ return false;
1081
+ }
1082
+
1083
+ const guardianLabel = adeps.resolveGuardianLabel();
1084
+ let handoffText: string;
1085
+
1086
+ if (params.activationReason === "invite_redeemed") {
1087
+ const firstName = firstToken(params.inviteeName);
1088
+ const assistantName = adeps.resolveAssistantLabel();
1089
+ if (firstName) {
1090
+ handoffText = assistantName
1091
+ ? `Great, I've verified that you are ${firstName}. It's nice to meet you! I'm ${assistantName}, ${guardianLabel}'s assistant. How can I help?`
1092
+ : `Great, I've verified that you are ${firstName}. It's nice to meet you! How can I help?`;
1093
+ } else {
1094
+ handoffText = assistantName
1095
+ ? `Hi there! I'm ${assistantName}, ${guardianLabel}'s assistant. How can I help?`
1096
+ : `Hi there! How can I help?`;
1097
+ }
1098
+ } else {
1099
+ handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
1100
+ }
1101
+
1102
+ void this.deps.speakSystemPrompt(handoffText);
1103
+
1104
+ this.deps.recordCallEvent(this.callSessionId, "assistant_spoke", {
1105
+ text: handoffText,
1106
+ });
1107
+ const session = adeps.getCallSession(this.callSessionId);
1108
+ if (session) {
1109
+ adeps.fireCallTranscriptNotifier(
1110
+ session.conversationId,
1111
+ this.callSessionId,
1112
+ "assistant",
1113
+ handoffText,
1114
+ );
1115
+ }
1116
+
1117
+ this.complete({
1118
+ kind: "proceed-handoff-spoken",
1119
+ assistantId,
1120
+ trustContext,
1121
+ deferredTranscripts: this.drainDeferredTranscripts(),
1122
+ });
1123
+ return true;
1124
+ }
1125
+
1126
+ /**
1127
+ * Re-resolve mid-setup trust, deferring transcripts that arrive during the
1128
+ * async window (see pushTranscriptFinal). Falls back to the setup-time
1129
+ * trust context on failure so a resolution blip can never wedge the call.
1130
+ */
1131
+ private async resolveTrustWithDeferral(
1132
+ adeps: ActivationDeps,
1133
+ assistantId: string,
1134
+ fromNumber: string,
1135
+ ): Promise<TrustContext> {
1136
+ this.trustReResolving = true;
1137
+ try {
1138
+ return await adeps.resolveMidCallTrustContext(assistantId, fromNumber);
1139
+ } catch (err) {
1140
+ log.error(
1141
+ { err, callSessionId: this.callSessionId },
1142
+ "Mid-setup trust re-resolution failed — keeping setup-time trust",
1143
+ );
1144
+ return this.initialTrustContext!;
1145
+ } finally {
1146
+ this.trustReResolving = false;
1147
+ }
1148
+ }
1149
+
1150
+ /** Hand off transcripts buffered during trust re-resolution, in order. */
1151
+ private drainDeferredTranscripts(): string[] | undefined {
1152
+ if (this.deferredTranscripts.length === 0) {
1153
+ return undefined;
1154
+ }
1155
+ const drained = this.deferredTranscripts;
1156
+ this.deferredTranscripts = [];
1157
+ return drained;
1158
+ }
1159
+
1160
+ /** Resolve the verification deps, applying defaults for the pure logic. */
1161
+ private requireVerificationDeps(): VerificationDeps {
1162
+ return requireDeps("verification", this.deps, [
1163
+ "getCallSession",
1164
+ "finalizeCall",
1165
+ "addMessage",
1166
+ "postPointerMessage",
1167
+ "fireCallTranscriptNotifier",
1168
+ "resolveGuardianLabel",
1169
+ "resolveAssistantLabel",
1170
+ "attemptVerificationCode",
1171
+ "resolveMidCallTrustContext",
1172
+ ]);
1173
+ }
1174
+
1175
+ /** Resolve the invite-redemption deps, applying defaults for the pure logic. */
1176
+ private requireInviteDeps(): InviteDeps {
1177
+ return requireDeps("invite", this.deps, [
1178
+ "getCallSession",
1179
+ "finalizeCall",
1180
+ "fireCallTranscriptNotifier",
1181
+ "resolveGuardianLabel",
1182
+ "resolveAssistantLabel",
1183
+ "attemptInviteCodeRedemption",
1184
+ "resolveMidCallTrustContext",
1185
+ ]);
1186
+ }
1187
+
1188
+ /** Tear the session down once the terminal copy has had time to play. */
1189
+ private endSessionAfterPlayback(reason: string): void {
1190
+ if (this.disposed) {
1191
+ return;
1192
+ }
1193
+ this.endSessionTimer = setTimeout(
1194
+ () => this.transport.endSession(reason),
1195
+ this.deps.ttsPlaybackDelayMs ?? getTtsPlaybackDelayMs(),
1196
+ );
1197
+ }
1198
+
1199
+ /** Run `finalizeCall` at most once per flow, whichever terminal path wins. */
1200
+ private finalizeOnce(
1201
+ finalize: typeof finalizeCallFn,
1202
+ conversationId: string,
1203
+ ): void {
1204
+ if (this.finalized) {
1205
+ return;
1206
+ }
1207
+ this.finalized = true;
1208
+ finalize(this.callSessionId, conversationId);
1209
+ }
1210
+
1211
+ // ── Invite redemption ───────────────────────────────────────────────
1212
+
1213
+ /**
1214
+ * Enter the invite-redemption sub-flow for a caller with an active voice
1215
+ * invite. Prompts the caller to enter their 6-digit invite code via DTMF
1216
+ * or speech; the code is validated in a single attempt.
1217
+ */
1218
+ private startInviteRedemption(
1219
+ outcome: Extract<SetupOutcome, { action: "invite_redemption" }>,
1220
+ resolved: SetupResolved,
1221
+ ): void {
1222
+ const ideps = this.requireInviteDeps();
1223
+ this.ideps = ideps;
1224
+ this.codeMode = "invite_redemption";
1225
+ this.codeLength = INVITE_CODE_LENGTH;
1226
+ this.digitBuffer = "";
1227
+ this.invite = {
1228
+ assistantId: outcome.assistantId,
1229
+ fromNumber: outcome.fromNumber,
1230
+ inviteeName: outcome.inviteeName,
1231
+ };
1232
+ this.initialTrustContext = toTrustContext(
1233
+ resolved.actorTrust,
1234
+ resolved.otherPartyNumber,
1235
+ );
1236
+ this.state = "collecting_code";
1237
+
1238
+ this.deps.recordCallEvent(this.callSessionId, "invite_redemption_started", {
1239
+ assistantId: outcome.assistantId,
1240
+ codeLength: INVITE_CODE_LENGTH,
1241
+ maxAttempts: 1,
1242
+ });
1243
+
1244
+ const displayFriend = firstToken(outcome.inviteeName) ?? "there";
1245
+ const displayGuardian = ideps.resolveGuardianLabel();
1246
+
1247
+ let promptText: string;
1248
+ if (!resolved.isInbound) {
1249
+ const assistantName = ideps.resolveAssistantLabel();
1250
+ promptText = assistantName
1251
+ ? `Hi ${displayFriend}, this is ${assistantName}, ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`
1252
+ : `Hi ${displayFriend}, this is ${displayGuardian}'s assistant. To get started, please enter the 6-digit code that ${displayGuardian} shared with you.`;
1253
+ } else {
1254
+ promptText = `Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`;
1255
+ }
1256
+ void this.deps.speakSystemPrompt(promptText);
1257
+
1258
+ log.info(
1259
+ { callSessionId: this.callSessionId, assistantId: outcome.assistantId },
1260
+ `${resolved.isInbound ? "Inbound" : "Outbound"} voice invite redemption started`,
1261
+ );
1262
+ }
1263
+
1264
+ /**
1265
+ * Validate a fully-entered invite code against the gateway claim.
1266
+ * Concurrent attempts are deduped upstream by the shared
1267
+ * code-submission in-flight guard.
1268
+ */
1269
+ private async handleInviteCodeEntry(enteredCode: string): Promise<void> {
1270
+ const invite = this.invite;
1271
+ const ideps = this.ideps;
1272
+ if (!invite || !ideps) {
1273
+ return;
1274
+ }
1275
+
1276
+ const result = await ideps.attemptInviteCodeRedemption({
1277
+ inviteRedemptionFromNumber: invite.fromNumber,
1278
+ enteredCode,
1279
+ guardianLabel: ideps.resolveGuardianLabel(),
1280
+ });
1281
+
1282
+ // A concurrent path may have already reached a terminal result.
1283
+ if (this.state === "completed") {
1284
+ return;
1285
+ }
1286
+
1287
+ if (result.outcome === "success") {
1288
+ this.codeMode = null;
1289
+ this.digitBuffer = "";
1290
+
1291
+ this.deps.recordCallEvent(
1292
+ this.callSessionId,
1293
+ "invite_redemption_succeeded",
1294
+ { memberId: result.memberId, inviteId: result.inviteId },
1295
+ );
1296
+ log.info(
1297
+ {
1298
+ callSessionId: this.callSessionId,
1299
+ memberId: result.memberId,
1300
+ type: result.type,
1301
+ },
1302
+ "Voice invite redemption succeeded",
1303
+ );
1304
+
1305
+ await this.continueAfterTrustedContactActivation(ideps, {
1306
+ assistantId: invite.assistantId,
1307
+ fromNumber: invite.fromNumber,
1308
+ activationReason: "invite_redeemed",
1309
+ inviteeName: invite.inviteeName,
1310
+ });
1311
+ } else {
1312
+ this.codeMode = null;
1313
+
1314
+ this.deps.recordCallEvent(
1315
+ this.callSessionId,
1316
+ "invite_redemption_failed",
1317
+ { attempts: 1 },
1318
+ );
1319
+ log.warn(
1320
+ { callSessionId: this.callSessionId },
1321
+ "Voice invite redemption failed — invalid or expired code",
1322
+ );
1323
+
1324
+ this.deps.updateCallSession(this.callSessionId, {
1325
+ status: "failed",
1326
+ endedAt: Date.now(),
1327
+ lastError: "Voice invite redemption failed — invalid or expired code",
1328
+ });
1329
+
1330
+ const failSession = ideps.getCallSession(this.callSessionId);
1331
+ if (failSession) {
1332
+ this.finalizeOnce(ideps.finalizeCall, failSession.conversationId);
1333
+ }
1334
+
1335
+ await this.deps.speakSystemPrompt(result.ttsMessage);
1336
+ this.endSessionAfterPlayback("Invite redemption failed");
1337
+ this.complete({ kind: "ended", reason: "Invite redemption failed" });
1338
+ }
1339
+ }
1340
+
1341
+ // ── Name capture + guardian wait ────────────────────────────────────
1342
+
1343
+ /**
1344
+ * Enter the name-capture sub-flow for an unknown inbound caller: speak
1345
+ * the intro greeting and arm the capture timeout. The caller's next
1346
+ * final transcript is taken as their name.
1347
+ */
1348
+ private startNameCapture(
1349
+ outcome: Extract<SetupOutcome, { action: "name_capture" }>,
1350
+ resolved: SetupResolved,
1351
+ ): void {
1352
+ const ndeps = this.requireNameCaptureDeps();
1353
+ this.ndeps = ndeps;
1354
+ this.accessRequest = {
1355
+ assistantId: outcome.assistantId,
1356
+ fromNumber: outcome.fromNumber,
1357
+ callerName: null,
1358
+ requestId: null,
1359
+ };
1360
+ this.initialTrustContext = toTrustContext(
1361
+ resolved.actorTrust,
1362
+ resolved.otherPartyNumber,
1363
+ );
1364
+ this.state = "capturing_name";
1365
+
1366
+ this.deps.recordCallEvent(
1367
+ this.callSessionId,
1368
+ "inbound_acl_name_capture_started",
1369
+ {
1370
+ from: outcome.fromNumber,
1371
+ trustClass: resolved.actorTrust.trustClass,
1372
+ },
1373
+ );
1374
+
1375
+ const guardianLabel = ndeps.resolveGuardianLabel();
1376
+ const assistantName = ndeps.resolveAssistantLabel();
1377
+ const greeting = assistantName
1378
+ ? `Hi, this is ${assistantName}, ${guardianLabel}'s assistant. Sorry, I don't recognize this number. I'll let ${guardianLabel} know you called and see if I have permission to speak with you. Can I get your name?`
1379
+ : `Hi, this is ${guardianLabel}'s assistant. Sorry, I don't recognize this number. I'll let ${guardianLabel} know you called and see if I have permission to speak with you. Can I get your name?`;
1380
+ void this.deps.speakSystemPrompt(greeting);
1381
+
1382
+ const timeoutMs = this.deps.nameCaptureTimeoutMs ?? NAME_CAPTURE_TIMEOUT_MS;
1383
+ this.nameCaptureTimer = setTimeout(() => {
1384
+ if (this.state !== "capturing_name") {
1385
+ return;
1386
+ }
1387
+ void this.handleNameCaptureTimeout();
1388
+ }, timeoutMs);
1389
+
1390
+ log.info(
1391
+ {
1392
+ callSessionId: this.callSessionId,
1393
+ assistantId: outcome.assistantId,
1394
+ timeoutMs,
1395
+ },
1396
+ "Name capture started for unknown inbound caller",
1397
+ );
1398
+ }
1399
+
1400
+ /**
1401
+ * Handle the caller's name: create the canonical access request, notify
1402
+ * the guardian, and hand the wait off to a {@link GuardianWaitController}.
1403
+ * Fails closed to the timeout copy when no request id comes back.
1404
+ */
1405
+ private async handleNameCaptureResponse(callerName: string): Promise<void> {
1406
+ const ndeps = this.ndeps;
1407
+ const accessRequest = this.accessRequest;
1408
+ if (!ndeps || !accessRequest) {
1409
+ return;
1410
+ }
1411
+
1412
+ this.clearNameCaptureTimer();
1413
+ accessRequest.callerName = callerName;
1414
+
1415
+ this.deps.recordCallEvent(this.callSessionId, "inbound_acl_name_captured", {
1416
+ from: accessRequest.fromNumber,
1417
+ callerName,
1418
+ });
1419
+
1420
+ try {
1421
+ const accessResult = await ndeps.notifyGuardianOfAccessRequest({
1422
+ canonicalAssistantId: accessRequest.assistantId,
1423
+ sourceChannel: "phone",
1424
+ conversationExternalId: accessRequest.fromNumber,
1425
+ actorExternalId: accessRequest.fromNumber,
1426
+ actorDisplayName: callerName,
1427
+ });
1428
+
1429
+ if (accessResult.notified) {
1430
+ accessRequest.requestId = accessResult.requestId;
1431
+ log.info(
1432
+ {
1433
+ callSessionId: this.callSessionId,
1434
+ requestId: accessResult.requestId,
1435
+ callerName,
1436
+ },
1437
+ "Guardian notified of voice access request with caller name",
1438
+ );
1439
+ } else if (accessResult.reason === "already_denied") {
1440
+ // The guardian already denied this caller; they are intentionally not
1441
+ // re-notified. Deliver the denial copy rather than the "I'll let them
1442
+ // know" timeout copy, which would falsely promise a notification.
1443
+ log.info(
1444
+ { callSessionId: this.callSessionId },
1445
+ "Voice caller previously denied — suppressing re-notification, delivering denial",
1446
+ );
1447
+ await this.handleAccessRequestDenied(ndeps, null);
1448
+ return;
1449
+ } else {
1450
+ log.warn(
1451
+ { callSessionId: this.callSessionId },
1452
+ "Failed to notify guardian of voice access request — no sender ID",
1453
+ );
1454
+ }
1455
+ } catch (err) {
1456
+ log.error(
1457
+ { err, callSessionId: this.callSessionId },
1458
+ "Failed to create access request for voice caller",
1459
+ );
1460
+ }
1461
+
1462
+ // A transport close during the async notification tears the flow down;
1463
+ // don't start a wait (or speak timeout copy) on a dead transport.
1464
+ if (this.state === "completed") {
1465
+ return;
1466
+ }
1467
+
1468
+ // No request id (notify threw or returned notified: false) — fail closed
1469
+ // rather than leaving the caller stuck on hold with no poll target.
1470
+ const requestId = accessRequest.requestId;
1471
+ if (!requestId) {
1472
+ log.warn(
1473
+ { callSessionId: this.callSessionId },
1474
+ "Access request ID is null after notification attempt — failing closed",
1475
+ );
1476
+ await this.handleAccessRequestTimeout(ndeps, {
1477
+ requestId: null,
1478
+ callbackOptIn: false,
1479
+ });
1480
+ return;
1481
+ }
1482
+
1483
+ this.startAccessRequestWait(ndeps, requestId, accessRequest);
1484
+ }
1485
+
1486
+ /**
1487
+ * Hand the guardian-decision wait to a {@link GuardianWaitController}:
1488
+ * it owns the hold message, heartbeats, polling, and timeout; the flow
1489
+ * owns the resolution continuations.
1490
+ */
1491
+ private startAccessRequestWait(
1492
+ ndeps: NameCaptureDeps,
1493
+ requestId: string,
1494
+ accessRequest: AccessRequestState,
1495
+ ): void {
1496
+ this.state = "awaiting_guardian_decision";
1497
+
1498
+ this.guardianWait = ndeps.createGuardianWaitController(this.callSessionId, {
1499
+ speakSystemPrompt: this.deps.speakSystemPrompt,
1500
+ updateCallSession: this.deps.updateCallSession,
1501
+ recordCallEvent: this.deps.recordCallEvent,
1502
+ resolveGuardianLabel: ndeps.resolveGuardianLabel,
1503
+ firstHeartbeatDelayMs: this.deps.ttsPlaybackDelayMs,
1504
+ onApproved: (ctx) => this.handleAccessRequestApproved(ndeps, ctx),
1505
+ onDenied: (ctx) => this.handleAccessRequestDenied(ndeps, ctx.requestId),
1506
+ onTimeout: (ctx) => this.handleAccessRequestTimeout(ndeps, ctx),
1507
+ });
1508
+ this.guardianWait.start({
1509
+ requestId,
1510
+ assistantId: accessRequest.assistantId,
1511
+ fromNumber: accessRequest.fromNumber,
1512
+ callerName: accessRequest.callerName,
1513
+ });
1514
+ }
1515
+
1516
+ /**
1517
+ * Approved access request: the caller is now an activated trusted
1518
+ * contact — run the shared activation continuation and hand off.
1519
+ */
1520
+ private async handleAccessRequestApproved(
1521
+ ndeps: NameCaptureDeps,
1522
+ ctx: GuardianWaitResolutionContext,
1523
+ ): Promise<void> {
1524
+ this.deps.recordCallEvent(
1525
+ this.callSessionId,
1526
+ "inbound_acl_access_approved",
1527
+ {
1528
+ from: ctx.fromNumber,
1529
+ callerName: ctx.callerName,
1530
+ requestId: ctx.requestId,
1531
+ },
1532
+ );
1533
+
1534
+ log.info(
1535
+ { callSessionId: this.callSessionId, from: ctx.fromNumber },
1536
+ "Access request approved — caller activated and continuing call",
1537
+ );
1538
+
1539
+ const handoffSpoken = await this.continueAfterTrustedContactActivation(
1540
+ ndeps,
1541
+ {
1542
+ assistantId: ctx.assistantId,
1543
+ fromNumber: ctx.fromNumber,
1544
+ activationReason: "access_approved",
1545
+ },
1546
+ );
1547
+ if (!handoffSpoken) {
1548
+ return;
1549
+ }
1550
+
1551
+ this.deps.recordCallEvent(
1552
+ this.callSessionId,
1553
+ "inbound_acl_post_approval_handoff_spoken",
1554
+ { from: ctx.fromNumber },
1555
+ );
1556
+ }
1557
+
1558
+ /** Denied access request: deliver deterministic copy and hang up. */
1559
+ private async handleAccessRequestDenied(
1560
+ ndeps: NameCaptureDeps,
1561
+ requestId: string | null,
1562
+ ): Promise<void> {
1563
+ const guardianLabel = ndeps.resolveGuardianLabel();
1564
+
1565
+ this.deps.recordCallEvent(this.callSessionId, "inbound_acl_access_denied", {
1566
+ from: this.accessRequest?.fromNumber,
1567
+ requestId,
1568
+ });
1569
+
1570
+ this.deps.updateCallSession(this.callSessionId, {
1571
+ status: "failed",
1572
+ endedAt: Date.now(),
1573
+ lastError: "Inbound voice ACL: guardian denied access request",
1574
+ });
1575
+
1576
+ log.info(
1577
+ { callSessionId: this.callSessionId },
1578
+ "Access request denied — ending call",
1579
+ );
1580
+
1581
+ await this.deps.speakSystemPrompt(
1582
+ `Sorry, ${guardianLabel} says I'm not allowed to speak with you. Goodbye.`,
1583
+ );
1584
+ this.endSessionAfterPlayback("Access request denied");
1585
+ this.complete({ kind: "ended", reason: "Access request denied" });
1586
+ }
1587
+
1588
+ /**
1589
+ * Access-request wait timeout (or fail-closed creation failure): deliver
1590
+ * deterministic copy — including the callback note when the caller opted
1591
+ * in — and hang up. The wait controller emits the callback handoff
1592
+ * notification before invoking this.
1593
+ */
1594
+ private async handleAccessRequestTimeout(
1595
+ ndeps: NameCaptureDeps,
1596
+ params: {
1597
+ requestId: string | null;
1598
+ callbackOptIn: boolean;
1599
+ },
1600
+ ): Promise<void> {
1601
+ const guardianLabel = ndeps.resolveGuardianLabel();
1602
+
1603
+ this.deps.recordCallEvent(
1604
+ this.callSessionId,
1605
+ "inbound_acl_access_timeout",
1606
+ {
1607
+ from: this.accessRequest?.fromNumber,
1608
+ requestId: params.requestId,
1609
+ callbackOptIn: params.callbackOptIn,
1610
+ },
1611
+ );
1612
+
1613
+ const callbackNote = params.callbackOptIn
1614
+ ? ` I've noted that you'd like a callback — I'll pass that along to ${guardianLabel}.`
1615
+ : "";
1616
+
1617
+ this.deps.updateCallSession(this.callSessionId, {
1618
+ status: "failed",
1619
+ endedAt: Date.now(),
1620
+ lastError: "Inbound voice ACL: guardian approval wait timed out",
1621
+ });
1622
+
1623
+ log.info(
1624
+ { callSessionId: this.callSessionId },
1625
+ "Access request timed out — ending call",
1626
+ );
1627
+
1628
+ await this.deps.speakSystemPrompt(
1629
+ `Sorry, I can't get ahold of ${guardianLabel} right now. I'll let them know you called.${callbackNote}`,
1630
+ );
1631
+ this.endSessionAfterPlayback("Access request timed out");
1632
+ this.complete({ kind: "ended", reason: "Access request timed out" });
1633
+ }
1634
+
1635
+ /**
1636
+ * Name-capture timeout: the caller never provided their name within the
1637
+ * allotted window. Deliver deterministic copy and hang up.
1638
+ */
1639
+ private async handleNameCaptureTimeout(): Promise<void> {
1640
+ // A transcript racing the timeout must not start a second terminal path.
1641
+ this.nameCaptureBusy = true;
1642
+ this.clearNameCaptureTimer();
1643
+
1644
+ this.deps.recordCallEvent(
1645
+ this.callSessionId,
1646
+ "inbound_acl_name_capture_timeout",
1647
+ { from: this.accessRequest?.fromNumber },
1648
+ );
1649
+
1650
+ this.deps.updateCallSession(this.callSessionId, {
1651
+ status: "failed",
1652
+ endedAt: Date.now(),
1653
+ lastError: "Inbound voice ACL: name capture timed out",
1654
+ });
1655
+
1656
+ log.info(
1657
+ { callSessionId: this.callSessionId },
1658
+ "Name capture timed out — ending call",
1659
+ );
1660
+
1661
+ await this.deps.speakSystemPrompt(
1662
+ "Sorry, I didn't catch your name. Please try calling back. Goodbye.",
1663
+ );
1664
+ this.endSessionAfterPlayback("Name capture timed out");
1665
+ this.complete({ kind: "ended", reason: "Name capture timed out" });
1666
+ }
1667
+
1668
+ private clearNameCaptureTimer(): void {
1669
+ if (this.nameCaptureTimer) {
1670
+ clearTimeout(this.nameCaptureTimer);
1671
+ this.nameCaptureTimer = null;
1672
+ }
1673
+ }
1674
+
1675
+ /** Resolve the name-capture deps, applying defaults for the pure logic. */
1676
+ private requireNameCaptureDeps(): NameCaptureDeps {
1677
+ return requireDeps("name-capture", this.deps, [
1678
+ "getCallSession",
1679
+ "fireCallTranscriptNotifier",
1680
+ "resolveGuardianLabel",
1681
+ "resolveAssistantLabel",
1682
+ "notifyGuardianOfAccessRequest",
1683
+ "createGuardianWaitController",
1684
+ "resolveMidCallTrustContext",
1685
+ ]);
1686
+ }
1687
+
1688
+ // ── Unverified caller ───────────────────────────────────────────────
1689
+
1690
+ /** Speak verification guidance to a known-but-unverified caller, then disconnect. */
1691
+ private async runUnverifiedCaller(
1692
+ outcome: Extract<SetupOutcome, { action: "unverified_caller" }>,
1693
+ ): Promise<void> {
1694
+ this.deps.recordCallEvent(
1695
+ this.callSessionId,
1696
+ "inbound_acl_unverified_caller",
1697
+ {
1698
+ callSessionId: this.callSessionId,
1699
+ isGuardian: outcome.isGuardian,
1700
+ },
1701
+ );
1702
+ this.deps.updateCallSession(this.callSessionId, {
1703
+ status: "failed",
1704
+ endedAt: Date.now(),
1705
+ lastError: "Inbound voice ACL: caller channel unverified",
1706
+ });
1707
+ const action = outcome.isGuardian
1708
+ ? `To verify, open your assistant's contacts page, click Verify next to the phone channel, ` +
1709
+ `and follow the prompts. Then call back once the verification session is active.`
1710
+ : `Please reach out to the account guardian to start a new verification session, ` +
1711
+ `then call back once the verification session is active.`;
1712
+ const message =
1713
+ `This number is registered as ${outcome.displayName}'s phone but has not been verified yet. ` +
1714
+ action;
1715
+ await this.deps.speakSystemPrompt(message);
1716
+ this.endSessionAfterPlayback(
1717
+ "Inbound voice ACL: caller channel unverified",
1718
+ );
1719
+ this.complete({
1720
+ kind: "ended",
1721
+ reason: "Inbound voice ACL: caller channel unverified",
1722
+ });
1723
+ }
1724
+
1725
+ // ── Internals ───────────────────────────────────────────────────────
1726
+
1727
+ private acceptsInput(): boolean {
1728
+ return this.state !== "idle" && this.state !== "completed";
1729
+ }
1730
+
1731
+ /**
1732
+ * Deliver the terminal result exactly once. A flow already completed
1733
+ * (or disposed on transport close) swallows late completions from
1734
+ * racing terminal paths.
1735
+ */
1736
+ private complete(result: SetupFlowResult): void {
1737
+ if (this.state === "completed") {
1738
+ return;
1739
+ }
1740
+ this.state = "completed";
1741
+ this.deps.onComplete(result);
1742
+ }
1743
+ }