@vellumai/assistant 0.4.26 → 0.4.30

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 (1360) hide show
  1. package/.env.example +2 -2
  2. package/AGENTS.md +5 -0
  3. package/ARCHITECTURE.md +207 -105
  4. package/Dockerfile +1 -1
  5. package/README.md +111 -113
  6. package/bun.lock +0 -3
  7. package/docs/architecture/integrations.md +0 -1
  8. package/docs/architecture/memory.md +100 -63
  9. package/docs/error-handling.md +71 -0
  10. package/docs/runbook-trusted-contacts.md +89 -52
  11. package/docs/trusted-contact-access.md +48 -46
  12. package/package.json +3 -3
  13. package/scripts/compare-benchmarks.sh +12 -5
  14. package/scripts/ipc/check-swift-decoder-drift.ts +5 -3
  15. package/scripts/test.sh +89 -5
  16. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +50 -37
  17. package/src/__tests__/access-request-decision.test.ts +0 -1
  18. package/src/__tests__/account-registry.test.ts +1 -1
  19. package/src/__tests__/actor-token-service.test.ts +40 -26
  20. package/src/__tests__/agent-loop-thinking.test.ts +29 -13
  21. package/src/__tests__/agent-loop.test.ts +2 -1
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -1
  23. package/src/__tests__/app-executors.test.ts +7 -17
  24. package/src/__tests__/approval-routes-http.test.ts +2 -2
  25. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  26. package/src/__tests__/asset-search-tool.test.ts +7 -7
  27. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  28. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  29. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  30. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +218 -0
  31. package/src/__tests__/call-controller.test.ts +99 -69
  32. package/src/__tests__/call-start-guardian-guard.test.ts +1 -1
  33. package/src/__tests__/channel-approval-routes.test.ts +157 -114
  34. package/src/__tests__/channel-approval.test.ts +8 -0
  35. package/src/__tests__/channel-approvals.test.ts +39 -1
  36. package/src/__tests__/channel-guardian.test.ts +176 -275
  37. package/src/__tests__/channel-readiness-service.test.ts +6 -2
  38. package/src/__tests__/channel-reply-delivery.test.ts +33 -2
  39. package/src/__tests__/channel-retry-sweep.test.ts +14 -14
  40. package/src/__tests__/checker.test.ts +12 -31
  41. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -1
  42. package/src/__tests__/commit-message-enrichment-service.test.ts +71 -59
  43. package/src/__tests__/compaction.benchmark.test.ts +6 -2
  44. package/src/__tests__/computer-use-tools.test.ts +1 -1
  45. package/src/__tests__/config-schema.test.ts +66 -7
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -29
  47. package/src/__tests__/contacts-tools.test.ts +63 -2
  48. package/src/__tests__/context-overflow-approval.test.ts +141 -0
  49. package/src/__tests__/context-overflow-policy.test.ts +171 -0
  50. package/src/__tests__/context-overflow-reducer.test.ts +533 -0
  51. package/src/__tests__/context-window-manager.test.ts +97 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +38 -46
  53. package/src/__tests__/conversation-pairing.test.ts +2 -2
  54. package/src/__tests__/conversation-routes-guardian-reply.test.ts +214 -10
  55. package/src/__tests__/conversation-routes.test.ts +4 -7
  56. package/src/__tests__/credential-broker-browser-fill.test.ts +13 -2
  57. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  58. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  59. package/src/__tests__/credential-vault-unit.test.ts +1 -1
  60. package/src/__tests__/credential-vault.test.ts +11 -8
  61. package/src/__tests__/daemon-lifecycle.test.ts +2 -2
  62. package/src/__tests__/daemon-server-session-init.test.ts +6 -6
  63. package/src/__tests__/delete-managed-skill-tool.test.ts +1 -1
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -2
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  66. package/src/__tests__/emit-signal-routing-intent.test.ts +4 -0
  67. package/src/__tests__/encrypted-store.test.ts +10 -7
  68. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  69. package/src/__tests__/file-edit-tool.test.ts +1 -1
  70. package/src/__tests__/file-read-tool.test.ts +1 -1
  71. package/src/__tests__/file-write-tool.test.ts +1 -1
  72. package/src/__tests__/fixtures/credential-security-fixtures.ts +87 -64
  73. package/src/__tests__/fixtures/media-reuse-fixtures.ts +37 -31
  74. package/src/__tests__/fixtures/mock-signup-server.ts +171 -115
  75. package/src/__tests__/fixtures/proxy-fixtures.ts +39 -39
  76. package/src/__tests__/followup-tools.test.ts +1 -1
  77. package/src/__tests__/gateway-only-guard.test.ts +4 -0
  78. package/src/__tests__/gemini-image-service.test.ts +2 -2
  79. package/src/__tests__/guardian-actions-endpoint.test.ts +543 -1
  80. package/src/__tests__/guardian-control-plane-policy.test.ts +15 -15
  81. package/src/__tests__/guardian-dispatch.test.ts +79 -1
  82. package/src/__tests__/guardian-grant-minting.test.ts +20 -20
  83. package/src/__tests__/guardian-outbound-http.test.ts +1 -2
  84. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -41
  85. package/src/__tests__/guardian-routing-invariants.test.ts +36 -16
  86. package/src/__tests__/guardian-routing-state.test.ts +36 -52
  87. package/src/__tests__/guardian-verification-intent-routing.test.ts +4 -6
  88. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +6 -8
  89. package/src/__tests__/handle-user-message-secret-resume.test.ts +39 -1
  90. package/src/__tests__/handlers-cu-observation-blob.test.ts +21 -10
  91. package/src/__tests__/handlers-telegram-config.test.ts +14 -14
  92. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +23 -2
  93. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  94. package/src/__tests__/headless-browser-navigate.test.ts +1 -1
  95. package/src/__tests__/headless-browser-read-tools.test.ts +1 -1
  96. package/src/__tests__/headless-browser-snapshot.test.ts +1 -1
  97. package/src/__tests__/heartbeat-service.test.ts +45 -2
  98. package/src/__tests__/host-file-edit-tool.test.ts +1 -1
  99. package/src/__tests__/host-file-read-tool.test.ts +1 -1
  100. package/src/__tests__/host-file-write-tool.test.ts +1 -1
  101. package/src/__tests__/host-shell-tool.test.ts +1 -1
  102. package/src/__tests__/inbound-invite-redemption.test.ts +17 -19
  103. package/src/__tests__/ingress-reconcile.test.ts +2 -2
  104. package/src/__tests__/integrations-cli.test.ts +232 -0
  105. package/src/__tests__/intent-routing.test.ts +7 -5
  106. package/src/__tests__/invite-redemption-service.test.ts +5 -4
  107. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +42 -321
  108. package/src/__tests__/ipc-snapshot.test.ts +32 -31
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
  110. package/src/__tests__/mcp-cli.test.ts +136 -57
  111. package/src/__tests__/mcp-client-auth.test.ts +95 -0
  112. package/src/__tests__/media-generate-image.test.ts +2 -2
  113. package/src/__tests__/media-reuse-story.e2e.test.ts +8 -8
  114. package/src/__tests__/memory-regressions.test.ts +6 -6
  115. package/src/__tests__/messaging-send-tool.test.ts +1 -1
  116. package/src/__tests__/migration-cross-version-compatibility.test.ts +1855 -0
  117. package/src/__tests__/migration-export-http.test.ts +540 -0
  118. package/src/__tests__/migration-import-commit-http.test.ts +823 -0
  119. package/src/__tests__/migration-import-preflight-http.test.ts +755 -0
  120. package/src/__tests__/migration-parity-persistence.test.ts +1854 -0
  121. package/src/__tests__/migration-transport.test.ts +904 -0
  122. package/src/__tests__/migration-validate-http.test.ts +698 -0
  123. package/src/__tests__/migration-wizard.test.ts +1289 -0
  124. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  125. package/src/__tests__/non-member-access-request.test.ts +17 -17
  126. package/src/__tests__/notification-decision-strategy.test.ts +110 -2
  127. package/src/__tests__/notification-deep-link.test.ts +18 -0
  128. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  129. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  130. package/src/__tests__/oauth2-gateway-transport.test.ts +1 -1
  131. package/src/__tests__/playbook-execution.test.ts +1 -1
  132. package/src/__tests__/playbook-tools.test.ts +1 -1
  133. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  134. package/src/__tests__/provider-streaming.benchmark.test.ts +3 -1
  135. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  136. package/src/__tests__/qdrant-manager.test.ts +40 -11
  137. package/src/__tests__/rebind-secrets-screen.test.ts +839 -0
  138. package/src/__tests__/recording-handler.test.ts +2 -2
  139. package/src/__tests__/recording-intent-handler.test.ts +3 -3
  140. package/src/__tests__/recording-state-machine.test.ts +2 -2
  141. package/src/__tests__/relay-server.test.ts +507 -228
  142. package/src/__tests__/reminder-store.test.ts +8 -0
  143. package/src/__tests__/reminder.test.ts +8 -0
  144. package/src/__tests__/{resolve-guardian-trust-class.test.ts → resolve-trust-class.test.ts} +11 -17
  145. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  146. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
  147. package/src/__tests__/schedule-tools.test.ts +1 -1
  148. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  149. package/src/__tests__/script-proxy-connect-tunnel.test.ts +2 -3
  150. package/src/__tests__/script-proxy-decision-trace.test.ts +2 -2
  151. package/src/__tests__/script-proxy-http-forwarder.test.ts +1 -1
  152. package/src/__tests__/script-proxy-injection-runtime.test.ts +5 -5
  153. package/src/__tests__/script-proxy-mitm-handler.test.ts +4 -4
  154. package/src/__tests__/script-proxy-policy-runtime.test.ts +2 -2
  155. package/src/__tests__/script-proxy-policy.test.ts +2 -2
  156. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  157. package/src/__tests__/script-proxy-session-manager.test.ts +4 -7
  158. package/src/__tests__/script-proxy-session-runtime.test.ts +1 -6
  159. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  160. package/src/__tests__/secret-scanner-executor.test.ts +2 -2
  161. package/src/__tests__/send-endpoint-busy.test.ts +11 -9
  162. package/src/__tests__/send-notification-tool.test.ts +2 -2
  163. package/src/__tests__/session-abort-tool-results.test.ts +17 -2
  164. package/src/__tests__/session-agent-loop.test.ts +456 -35
  165. package/src/__tests__/session-confirmation-signals.test.ts +3 -2
  166. package/src/__tests__/session-conflict-gate.test.ts +20 -3
  167. package/src/__tests__/session-init.benchmark.test.ts +2 -2
  168. package/src/__tests__/session-load-history-repair.test.ts +7 -7
  169. package/src/__tests__/session-media-retry.test.ts +147 -0
  170. package/src/__tests__/session-pre-run-repair.test.ts +17 -2
  171. package/src/__tests__/session-profile-injection.test.ts +20 -3
  172. package/src/__tests__/session-provider-retry-repair.test.ts +86 -6
  173. package/src/__tests__/session-queue.test.ts +33 -18
  174. package/src/__tests__/session-runtime-assembly.test.ts +147 -1
  175. package/src/__tests__/session-runtime-workspace.test.ts +40 -0
  176. package/src/__tests__/session-slash-known.test.ts +21 -3
  177. package/src/__tests__/session-slash-queue.test.ts +17 -2
  178. package/src/__tests__/session-slash-unknown.test.ts +17 -2
  179. package/src/__tests__/session-surfaces-deselection.test.ts +208 -0
  180. package/src/__tests__/session-workspace-cache-state.test.ts +2 -2
  181. package/src/__tests__/session-workspace-injection.test.ts +17 -2
  182. package/src/__tests__/session-workspace-tool-tracking.test.ts +17 -2
  183. package/src/__tests__/shell-credential-ref.test.ts +1 -1
  184. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  185. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  186. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  187. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  188. package/src/__tests__/skill-load-tool.test.ts +1 -1
  189. package/src/__tests__/skill-script-runner-host.test.ts +1 -1
  190. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -1
  191. package/src/__tests__/skill-script-runner.test.ts +1 -1
  192. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  193. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  194. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  195. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  196. package/src/__tests__/slack-skill.test.ts +4 -2
  197. package/src/__tests__/starter-task-flow.test.ts +0 -1
  198. package/src/__tests__/subagent-tools.test.ts +3 -3
  199. package/src/__tests__/swarm-recursion.test.ts +1 -1
  200. package/src/__tests__/swarm-session-integration.test.ts +1 -1
  201. package/src/__tests__/swarm-tool.test.ts +1 -1
  202. package/src/__tests__/task-management-tools.test.ts +1 -1
  203. package/src/__tests__/task-tools.test.ts +1 -1
  204. package/src/__tests__/terminal-tools.test.ts +1 -1
  205. package/src/__tests__/test-support/browser-skill-harness.ts +39 -27
  206. package/src/__tests__/test-support/computer-use-skill-harness.ts +14 -14
  207. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  208. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
  209. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  210. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  211. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -1
  212. package/src/__tests__/tool-executor.test.ts +23 -182
  213. package/src/__tests__/tool-grant-request-escalation.test.ts +11 -11
  214. package/src/__tests__/tool-permission-simulate-handler.test.ts +4 -4
  215. package/src/__tests__/transfer-progress-screen.test.ts +1180 -0
  216. package/src/__tests__/trust-context-guards.test.ts +25 -29
  217. package/src/__tests__/trusted-contact-approval-notifier.test.ts +23 -21
  218. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +37 -40
  219. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +29 -25
  220. package/src/__tests__/trusted-contact-multichannel.test.ts +25 -24
  221. package/src/__tests__/trusted-contact-verification.test.ts +64 -76
  222. package/src/__tests__/turn-commit.test.ts +18 -18
  223. package/src/__tests__/twilio-provider.test.ts +7 -7
  224. package/src/__tests__/validation-results-screen.test.ts +1107 -0
  225. package/src/__tests__/view-image-tool.test.ts +1 -1
  226. package/src/__tests__/voice-invite-redemption.test.ts +4 -3
  227. package/src/__tests__/voice-scoped-grant-consumer.test.ts +12 -12
  228. package/src/__tests__/voice-session-bridge.test.ts +24 -24
  229. package/src/agent/attachments.ts +3 -1
  230. package/src/agent/loop.ts +13 -13
  231. package/src/agent/message-types.ts +13 -7
  232. package/src/amazon/cart.ts +59 -32
  233. package/src/amazon/checkout.ts +25 -14
  234. package/src/amazon/client.ts +61 -58
  235. package/src/amazon/product-details.ts +3 -3
  236. package/src/amazon/request-extractor.ts +46 -31
  237. package/src/amazon/search.ts +6 -4
  238. package/src/amazon/session.ts +33 -24
  239. package/src/approvals/AGENTS.md +26 -0
  240. package/src/approvals/approval-primitive.ts +87 -64
  241. package/src/approvals/guardian-decision-primitive.ts +172 -81
  242. package/src/approvals/guardian-request-resolvers.ts +262 -155
  243. package/src/autonomy/autonomy-resolver.ts +7 -5
  244. package/src/autonomy/autonomy-store.ts +34 -19
  245. package/src/autonomy/disposition-mapper.ts +5 -5
  246. package/src/autonomy/index.ts +6 -6
  247. package/src/autonomy/types.ts +7 -3
  248. package/src/browser-extension-relay/client.ts +50 -19
  249. package/src/browser-extension-relay/protocol.ts +11 -11
  250. package/src/browser-extension-relay/server.ts +45 -20
  251. package/src/bundler/app-bundler.ts +75 -50
  252. package/src/bundler/bundle-scanner.ts +145 -41
  253. package/src/bundler/bundle-signer.ts +16 -14
  254. package/src/bundler/signature-verifier.ts +36 -33
  255. package/src/calls/call-constants.ts +10 -3
  256. package/src/calls/call-controller.ts +473 -214
  257. package/src/calls/call-conversation-messages.ts +25 -15
  258. package/src/calls/call-domain.ts +401 -148
  259. package/src/calls/call-pointer-message-composer.ts +26 -21
  260. package/src/calls/call-pointer-messages.ts +52 -28
  261. package/src/calls/call-recovery.ts +53 -37
  262. package/src/calls/call-state-machine.ts +37 -7
  263. package/src/calls/call-state.ts +35 -13
  264. package/src/calls/call-store.ts +165 -77
  265. package/src/calls/elevenlabs-client.ts +39 -20
  266. package/src/calls/guardian-action-sweep.ts +42 -24
  267. package/src/calls/guardian-dispatch.ts +79 -56
  268. package/src/calls/guardian-question-copy.ts +28 -23
  269. package/src/calls/relay-server.ts +1149 -532
  270. package/src/calls/speaker-identification.ts +21 -15
  271. package/src/calls/twilio-config.ts +34 -17
  272. package/src/calls/twilio-provider.ts +108 -55
  273. package/src/calls/twilio-rest.ts +212 -100
  274. package/src/calls/twilio-routes.ts +165 -92
  275. package/src/calls/types.ts +55 -7
  276. package/src/calls/voice-quality.ts +6 -4
  277. package/src/calls/voice-session-bridge.ts +181 -133
  278. package/src/channels/config.ts +18 -14
  279. package/src/channels/types.ts +38 -10
  280. package/src/cli/amazon.ts +333 -227
  281. package/src/cli/config-commands.ts +236 -146
  282. package/src/cli/core-commands.ts +403 -329
  283. package/src/cli/email-guardrails.ts +38 -19
  284. package/src/cli/email.ts +207 -153
  285. package/src/cli/influencer.ts +58 -56
  286. package/src/cli/integrations.ts +306 -0
  287. package/src/cli/ipc-client.ts +24 -19
  288. package/src/cli/map.ts +176 -129
  289. package/src/cli/mcp.ts +260 -152
  290. package/src/cli/sequence.ts +165 -107
  291. package/src/cli/twitter.ts +302 -218
  292. package/src/cli.ts +418 -279
  293. package/src/commands/cc-command-registry.ts +52 -27
  294. package/src/config/agent-schema.ts +217 -134
  295. package/src/config/assistant-feature-flags.ts +23 -18
  296. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +19 -0
  297. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  298. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  299. package/src/config/bundled-skills/app-builder/tools/app-create.ts +7 -4
  300. package/src/config/bundled-skills/app-builder/tools/app-delete.ts +6 -3
  301. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +7 -4
  302. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +6 -3
  303. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +6 -3
  304. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +7 -4
  305. package/src/config/bundled-skills/app-builder/tools/app-list.ts +6 -3
  306. package/src/config/bundled-skills/app-builder/tools/app-query.ts +6 -3
  307. package/src/config/bundled-skills/app-builder/tools/app-update.ts +6 -3
  308. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  309. package/src/config/bundled-skills/browser/tools/browser-click.ts +5 -2
  310. package/src/config/bundled-skills/browser/tools/browser-close.ts +5 -2
  311. package/src/config/bundled-skills/browser/tools/browser-extract.ts +5 -2
  312. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +5 -2
  313. package/src/config/bundled-skills/browser/tools/browser-hover.ts +5 -2
  314. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +5 -2
  315. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +5 -2
  316. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +5 -2
  317. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +5 -2
  318. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +5 -2
  319. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +5 -2
  320. package/src/config/bundled-skills/browser/tools/browser-type.ts +5 -2
  321. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +13 -6
  322. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +5 -2
  323. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  324. package/src/config/bundled-skills/claude-code/TOOLS.json +4 -0
  325. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +5 -2
  326. package/src/config/bundled-skills/computer-use/SKILL.md +2 -2
  327. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  328. package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +6 -3
  329. package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +6 -3
  330. package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +10 -3
  331. package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +6 -3
  332. package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +6 -3
  333. package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +6 -3
  334. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +10 -3
  335. package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +6 -3
  336. package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +10 -3
  337. package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +10 -3
  338. package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +6 -3
  339. package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +6 -3
  340. package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +6 -3
  341. package/src/config/bundled-skills/configure-settings/SKILL.md +28 -14
  342. package/src/config/bundled-skills/contacts/SKILL.md +453 -15
  343. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  344. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +79 -20
  345. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -18
  346. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +64 -19
  347. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  348. package/src/config/bundled-skills/document/tools/document-create.ts +5 -2
  349. package/src/config/bundled-skills/document/tools/document-update.ts +5 -2
  350. package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -7
  351. package/src/config/bundled-skills/email-setup/SKILL.md +12 -9
  352. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  353. package/src/config/bundled-skills/followups/tools/followup-create.ts +5 -2
  354. package/src/config/bundled-skills/followups/tools/followup-list.ts +5 -2
  355. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +5 -2
  356. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  357. package/src/config/bundled-skills/google-calendar/calendar-client.ts +44 -32
  358. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +11 -5
  359. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +13 -7
  360. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +11 -5
  361. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +13 -7
  362. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +28 -12
  363. package/src/config/bundled-skills/google-calendar/tools/shared.ts +6 -4
  364. package/src/config/bundled-skills/google-calendar/types.ts +3 -3
  365. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +88 -33
  366. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  367. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +48 -25
  368. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  369. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +60 -35
  370. package/src/config/bundled-skills/mcp-setup/SKILL.md +75 -0
  371. package/src/config/bundled-skills/media-processing/SKILL.md +55 -15
  372. package/src/config/bundled-skills/media-processing/TOOLS.json +48 -2
  373. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +12 -10
  374. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +34 -19
  375. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +82 -66
  376. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +148 -0
  377. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +1 -1
  378. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +8 -3
  379. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +117 -53
  380. package/src/config/bundled-skills/media-processing/services/gemini-video.ts +273 -0
  381. package/src/config/bundled-skills/media-processing/services/preprocess.ts +185 -97
  382. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +32 -27
  383. package/src/config/bundled-skills/media-processing/services/reduce.ts +101 -24
  384. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +121 -55
  385. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +58 -24
  386. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +198 -92
  387. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +98 -70
  388. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +59 -19
  389. package/src/config/bundled-skills/media-processing/tools/media-status.ts +26 -10
  390. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +29 -14
  391. package/src/config/bundled-skills/messaging/SKILL.md +7 -5
  392. package/src/config/bundled-skills/messaging/TOOLS.json +232 -186
  393. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +31 -13
  394. package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +16 -10
  395. package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +18 -9
  396. package/src/config/bundled-skills/messaging/tools/gmail-download-attachment.ts +23 -16
  397. package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +28 -12
  398. package/src/config/bundled-skills/messaging/tools/gmail-filters.ts +41 -21
  399. package/src/config/bundled-skills/messaging/tools/gmail-follow-up.ts +44 -23
  400. package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +73 -33
  401. package/src/config/bundled-skills/messaging/tools/gmail-label.ts +15 -9
  402. package/src/config/bundled-skills/messaging/tools/gmail-list-attachments.ts +22 -14
  403. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +99 -50
  404. package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +14 -8
  405. package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +63 -44
  406. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +90 -46
  407. package/src/config/bundled-skills/messaging/tools/gmail-summarize-thread.ts +43 -22
  408. package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +15 -9
  409. package/src/config/bundled-skills/messaging/tools/gmail-triage.ts +51 -22
  410. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +62 -26
  411. package/src/config/bundled-skills/messaging/tools/gmail-vacation.ts +34 -19
  412. package/src/config/bundled-skills/messaging/tools/google-contacts.ts +32 -16
  413. package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +10 -4
  414. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +91 -47
  415. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +21 -9
  416. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +9 -3
  417. package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +30 -17
  418. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +10 -4
  419. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +14 -6
  420. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +16 -5
  421. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +63 -36
  422. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +10 -4
  423. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +30 -12
  424. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +48 -29
  425. package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +20 -6
  426. package/src/config/bundled-skills/messaging/tools/send-notification.ts +1 -1
  427. package/src/config/bundled-skills/messaging/tools/sequence-analytics.ts +59 -22
  428. package/src/config/bundled-skills/messaging/tools/sequence-cancel.ts +13 -7
  429. package/src/config/bundled-skills/messaging/tools/sequence-create.ts +27 -12
  430. package/src/config/bundled-skills/messaging/tools/sequence-delete.ts +14 -6
  431. package/src/config/bundled-skills/messaging/tools/sequence-enroll.ts +30 -11
  432. package/src/config/bundled-skills/messaging/tools/sequence-enrollment-list.ts +16 -8
  433. package/src/config/bundled-skills/messaging/tools/sequence-get.ts +31 -13
  434. package/src/config/bundled-skills/messaging/tools/sequence-import.ts +38 -22
  435. package/src/config/bundled-skills/messaging/tools/sequence-list.ts +16 -7
  436. package/src/config/bundled-skills/messaging/tools/sequence-pause.ts +29 -10
  437. package/src/config/bundled-skills/messaging/tools/sequence-resume.ts +16 -8
  438. package/src/config/bundled-skills/messaging/tools/sequence-update.ts +35 -16
  439. package/src/config/bundled-skills/messaging/tools/shared.ts +26 -12
  440. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  441. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  442. package/src/config/bundled-skills/notifications/tools/send-notification.ts +69 -34
  443. package/src/config/bundled-skills/notifications/tools/shared.ts +1 -1
  444. package/src/config/bundled-skills/phone-calls/SKILL.md +46 -48
  445. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  446. package/src/config/bundled-skills/phone-calls/tools/call-end.ts +1 -1
  447. package/src/config/bundled-skills/phone-calls/tools/call-start.ts +1 -1
  448. package/src/config/bundled-skills/phone-calls/tools/call-status.ts +1 -1
  449. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  450. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +91 -51
  451. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +30 -16
  452. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +66 -27
  453. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +89 -42
  454. package/src/config/bundled-skills/public-ingress/SKILL.md +26 -19
  455. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  456. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +5 -2
  457. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +5 -2
  458. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +5 -2
  459. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  460. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  461. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +5 -2
  462. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +5 -2
  463. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +5 -2
  464. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +5 -2
  465. package/src/config/bundled-skills/screen-recording/SKILL.md +11 -3
  466. package/src/config/bundled-skills/self-upgrade/SKILL.md +9 -8
  467. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  468. package/src/config/bundled-skills/slack/TOOLS.json +122 -17
  469. package/src/config/bundled-skills/slack/tools/shared.ts +7 -5
  470. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +11 -5
  471. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +11 -5
  472. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  473. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +46 -16
  474. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +11 -5
  475. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +28 -0
  476. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +12 -6
  477. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  478. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  479. package/src/config/bundled-skills/sms-setup/SKILL.md +5 -8
  480. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  481. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +5 -2
  482. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +5 -2
  483. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +5 -2
  484. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +5 -2
  485. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +5 -2
  486. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  487. package/src/config/bundled-skills/tasks/tools/task-delete.ts +5 -2
  488. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +5 -2
  489. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +5 -2
  490. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +5 -2
  491. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +5 -2
  492. package/src/config/bundled-skills/tasks/tools/task-list.ts +5 -2
  493. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +5 -2
  494. package/src/config/bundled-skills/tasks/tools/task-run.ts +5 -2
  495. package/src/config/bundled-skills/tasks/tools/task-save.ts +5 -2
  496. package/src/config/bundled-skills/telegram-setup/SKILL.md +7 -8
  497. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  498. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +232 -127
  499. package/src/config/bundled-skills/twilio-setup/SKILL.md +7 -12
  500. package/src/config/bundled-skills/twitter/SKILL.md +19 -2
  501. package/src/config/bundled-skills/voice-setup/SKILL.md +5 -5
  502. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  503. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +5 -2
  504. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +5 -2
  505. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +5 -2
  506. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +5 -2
  507. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +5 -2
  508. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  509. package/src/config/bundled-skills/weather/tools/get-weather.ts +5 -2
  510. package/src/config/bundled-tool-registry.ts +2 -0
  511. package/src/config/calls-schema.ts +108 -63
  512. package/src/config/channel-permission-profiles.ts +155 -0
  513. package/src/config/computer-use-prompt.ts +7 -7
  514. package/src/config/core-schema.ts +239 -155
  515. package/src/config/defaults.ts +2 -2
  516. package/src/config/elevenlabs-schema.ts +15 -15
  517. package/src/config/env-registry.ts +33 -33
  518. package/src/config/env.ts +4 -1
  519. package/src/config/feature-flag-registry.json +31 -7
  520. package/src/config/loader.ts +118 -58
  521. package/src/config/mcp-schema.ts +29 -15
  522. package/src/config/memory-schema.ts +434 -229
  523. package/src/config/notifications-schema.ts +4 -4
  524. package/src/config/sandbox-schema.ts +2 -2
  525. package/src/config/schema.ts +12 -2
  526. package/src/config/skill-state.ts +27 -15
  527. package/src/config/skills-schema.ts +72 -23
  528. package/src/config/skills.ts +303 -143
  529. package/src/config/system-prompt.ts +25 -6
  530. package/src/config/types.ts +1 -1
  531. package/src/config/update-bulletin-format.ts +3 -3
  532. package/src/config/update-bulletin-state.ts +15 -6
  533. package/src/config/update-bulletin-template-path.ts +8 -4
  534. package/src/config/update-bulletin.ts +33 -14
  535. package/src/config/user-reference.ts +8 -8
  536. package/src/contacts/contact-events.ts +21 -0
  537. package/src/contacts/contact-store.ts +813 -100
  538. package/src/contacts/contacts-write.ts +287 -0
  539. package/src/contacts/index.ts +13 -4
  540. package/src/contacts/startup-migration.ts +21 -0
  541. package/src/contacts/types.ts +73 -2
  542. package/src/context/token-estimator.ts +54 -31
  543. package/src/context/tool-result-truncation.ts +41 -7
  544. package/src/context/window-manager.ts +225 -120
  545. package/src/daemon/approval-generators.ts +83 -55
  546. package/src/daemon/approved-devices-store.ts +33 -20
  547. package/src/daemon/assistant-attachments.ts +157 -101
  548. package/src/daemon/auth-manager.ts +17 -15
  549. package/src/daemon/classifier.ts +117 -46
  550. package/src/daemon/computer-use-session.ts +316 -187
  551. package/src/daemon/config-watcher.ts +91 -44
  552. package/src/daemon/connection-policy.ts +18 -10
  553. package/src/daemon/context-overflow-approval.ts +48 -0
  554. package/src/daemon/context-overflow-policy.ts +50 -0
  555. package/src/daemon/context-overflow-reducer.ts +300 -0
  556. package/src/daemon/daemon-control.ts +79 -51
  557. package/src/daemon/date-context.ts +119 -69
  558. package/src/daemon/dictation-profile-store.ts +94 -48
  559. package/src/daemon/dictation-text-processing.ts +33 -12
  560. package/src/daemon/doordash-steps.ts +92 -49
  561. package/src/daemon/guardian-action-generators.ts +62 -46
  562. package/src/daemon/guardian-verification-intent.ts +35 -19
  563. package/src/daemon/handlers/apps.ts +258 -113
  564. package/src/daemon/handlers/avatar.ts +20 -15
  565. package/src/daemon/handlers/computer-use.ts +82 -39
  566. package/src/daemon/handlers/config-channels.ts +146 -69
  567. package/src/daemon/handlers/config-heartbeat.ts +114 -59
  568. package/src/daemon/handlers/config-inbox.ts +213 -160
  569. package/src/daemon/handlers/config-ingress.ts +127 -55
  570. package/src/daemon/handlers/config-integrations.ts +145 -88
  571. package/src/daemon/handlers/config-model.ts +58 -22
  572. package/src/daemon/handlers/config-platform.ts +40 -16
  573. package/src/daemon/handlers/config-scheduling.ts +109 -48
  574. package/src/daemon/handlers/config-slack-channel.ts +67 -35
  575. package/src/daemon/handlers/config-slack.ts +21 -20
  576. package/src/daemon/handlers/config-telegram.ts +100 -70
  577. package/src/daemon/handlers/config-tools.ts +103 -55
  578. package/src/daemon/handlers/config-trust.ts +50 -20
  579. package/src/daemon/handlers/config.ts +72 -24
  580. package/src/daemon/handlers/contacts.ts +163 -0
  581. package/src/daemon/handlers/diagnostics.ts +90 -48
  582. package/src/daemon/handlers/documents.ts +74 -46
  583. package/src/daemon/handlers/guardian-actions.ts +57 -77
  584. package/src/daemon/handlers/home-base.ts +19 -16
  585. package/src/daemon/handlers/identity.ts +65 -45
  586. package/src/daemon/handlers/index.ts +78 -54
  587. package/src/daemon/handlers/misc.ts +664 -234
  588. package/src/daemon/handlers/navigate-settings.ts +14 -11
  589. package/src/daemon/handlers/oauth-connect.ts +48 -35
  590. package/src/daemon/handlers/open-bundle-handler.ts +31 -24
  591. package/src/daemon/handlers/pairing.ts +51 -25
  592. package/src/daemon/handlers/publish.ts +55 -33
  593. package/src/daemon/handlers/recording.ts +378 -162
  594. package/src/daemon/handlers/sessions.ts +922 -423
  595. package/src/daemon/handlers/shared.ts +202 -117
  596. package/src/daemon/handlers/signing.ts +25 -6
  597. package/src/daemon/handlers/subagents.ts +117 -56
  598. package/src/daemon/handlers/twitter-auth.ts +70 -49
  599. package/src/daemon/handlers/work-items.ts +264 -112
  600. package/src/daemon/handlers/workspace-files.ts +27 -20
  601. package/src/daemon/handlers.ts +2 -2
  602. package/src/daemon/history-repair.ts +16 -15
  603. package/src/daemon/identity-helpers.ts +4 -4
  604. package/src/daemon/install-cli-launchers.ts +33 -22
  605. package/src/daemon/ipc-blob-store.ts +38 -24
  606. package/src/daemon/ipc-contract/apps.ts +61 -50
  607. package/src/daemon/ipc-contract/computer-use.ts +47 -37
  608. package/src/daemon/ipc-contract/contacts.ts +69 -0
  609. package/src/daemon/ipc-contract/diagnostics.ts +14 -14
  610. package/src/daemon/ipc-contract/documents.ts +8 -8
  611. package/src/daemon/ipc-contract/guardian-actions.ts +4 -4
  612. package/src/daemon/ipc-contract/inbox.ts +12 -71
  613. package/src/daemon/ipc-contract/integrations.ts +57 -44
  614. package/src/daemon/ipc-contract/memory.ts +3 -5
  615. package/src/daemon/ipc-contract/messages.ts +95 -69
  616. package/src/daemon/ipc-contract/notifications.ts +10 -6
  617. package/src/daemon/ipc-contract/pairing.ts +8 -8
  618. package/src/daemon/ipc-contract/schedules.ts +20 -20
  619. package/src/daemon/ipc-contract/sessions.ts +89 -57
  620. package/src/daemon/ipc-contract/settings.ts +12 -7
  621. package/src/daemon/ipc-contract/shared.ts +9 -7
  622. package/src/daemon/ipc-contract/skills.ts +46 -26
  623. package/src/daemon/ipc-contract/subagents.ts +9 -9
  624. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  625. package/src/daemon/ipc-contract/trust.ts +11 -11
  626. package/src/daemon/ipc-contract/work-items.ts +33 -28
  627. package/src/daemon/ipc-contract/workspace.ts +28 -21
  628. package/src/daemon/ipc-contract-inventory.json +10 -4
  629. package/src/daemon/ipc-contract-inventory.ts +29 -26
  630. package/src/daemon/ipc-contract.ts +111 -44
  631. package/src/daemon/ipc-handler.ts +27 -19
  632. package/src/daemon/ipc-protocol.ts +22 -12
  633. package/src/daemon/ipc-validate.ts +91 -46
  634. package/src/daemon/lifecycle.ts +39 -3
  635. package/src/daemon/main.ts +10 -8
  636. package/src/daemon/media-visibility-policy.ts +3 -1
  637. package/src/daemon/pairing-store.ts +72 -40
  638. package/src/daemon/providers-setup.ts +35 -25
  639. package/src/daemon/recording-executor.ts +37 -30
  640. package/src/daemon/recording-intent-fallback.ts +58 -28
  641. package/src/daemon/recording-intent.ts +71 -61
  642. package/src/daemon/ride-shotgun-handler.ts +201 -121
  643. package/src/daemon/seed-files.ts +28 -17
  644. package/src/daemon/server.ts +23 -14
  645. package/src/daemon/session-agent-loop-handlers.ts +270 -135
  646. package/src/daemon/session-agent-loop.ts +796 -253
  647. package/src/daemon/session-attachments.ts +109 -40
  648. package/src/daemon/session-conflict-gate.ts +72 -28
  649. package/src/daemon/session-dynamic-profile.ts +36 -22
  650. package/src/daemon/session-error.ts +68 -45
  651. package/src/daemon/session-evictor.ts +17 -10
  652. package/src/daemon/session-history.ts +201 -89
  653. package/src/daemon/session-lifecycle.ts +80 -44
  654. package/src/daemon/session-media-retry.ts +104 -42
  655. package/src/daemon/session-memory.ts +77 -55
  656. package/src/daemon/session-messaging.ts +261 -111
  657. package/src/daemon/session-notifiers.ts +57 -45
  658. package/src/daemon/session-process.ts +370 -154
  659. package/src/daemon/session-queue-manager.ts +30 -13
  660. package/src/daemon/session-runtime-assembly.ts +61 -15
  661. package/src/daemon/session-skill-tools.ts +84 -36
  662. package/src/daemon/session-slash.ts +178 -113
  663. package/src/daemon/session-surfaces.ts +498 -212
  664. package/src/daemon/session-tool-setup.ts +24 -16
  665. package/src/daemon/session-usage.ts +26 -13
  666. package/src/daemon/session-workspace.ts +7 -4
  667. package/src/daemon/session.ts +18 -19
  668. package/src/daemon/shutdown-handlers.ts +36 -33
  669. package/src/daemon/tls-certs.ts +90 -57
  670. package/src/daemon/tool-side-effects.ts +97 -65
  671. package/src/daemon/trace-emitter.ts +8 -7
  672. package/src/daemon/video-thumbnail.ts +55 -25
  673. package/src/daemon/watch-handler.ts +164 -86
  674. package/src/email/provider.ts +1 -1
  675. package/src/email/providers/agentmail.ts +87 -45
  676. package/src/email/providers/index.ts +19 -14
  677. package/src/email/service.ts +52 -24
  678. package/src/email/types.ts +2 -2
  679. package/src/errors.ts +1 -1
  680. package/src/events/bus.ts +30 -10
  681. package/src/events/domain-events.ts +20 -13
  682. package/src/events/index.ts +6 -6
  683. package/src/events/tool-audit-listener.ts +34 -20
  684. package/src/events/tool-domain-event-publisher.ts +22 -20
  685. package/src/events/tool-metrics-listener.ts +26 -21
  686. package/src/events/tool-notification-listener.ts +5 -5
  687. package/src/events/tool-profiling-listener.ts +33 -23
  688. package/src/events/tool-trace-listener.ts +70 -46
  689. package/src/export/formatter.ts +38 -32
  690. package/src/followups/followup-store.ts +43 -36
  691. package/src/followups/index.ts +2 -2
  692. package/src/followups/types.ts +1 -1
  693. package/src/gallery/default-gallery.ts +37 -34
  694. package/src/gallery/gallery-manifest.ts +9 -9
  695. package/src/heartbeat/heartbeat-service.ts +59 -37
  696. package/src/home-base/app-link-store.ts +14 -12
  697. package/src/home-base/bootstrap.ts +14 -8
  698. package/src/home-base/prebuilt/seed.ts +34 -26
  699. package/src/home-base/prebuilt-home-base-updater.ts +14 -8
  700. package/src/hooks/cli.ts +56 -43
  701. package/src/hooks/config.ts +27 -14
  702. package/src/hooks/discovery.ts +53 -33
  703. package/src/hooks/manager.ts +50 -26
  704. package/src/hooks/runner.ts +35 -29
  705. package/src/hooks/templates.ts +38 -15
  706. package/src/hooks/types.ts +13 -13
  707. package/src/inbound/platform-callback-registration.ts +21 -15
  708. package/src/inbound/public-ingress-urls.ts +9 -6
  709. package/src/index.ts +20 -19
  710. package/src/influencer/client.ts +261 -117
  711. package/src/instrument.ts +3 -1
  712. package/src/logfire.ts +64 -39
  713. package/src/mcp/client.ts +107 -55
  714. package/src/mcp/manager.ts +45 -18
  715. package/src/mcp/mcp-oauth-provider.ts +114 -62
  716. package/src/media/gemini-image-service.ts +75 -23
  717. package/src/memory/account-store.ts +16 -9
  718. package/src/memory/admin.ts +87 -57
  719. package/src/memory/app-git-service.ts +77 -47
  720. package/src/memory/app-store.ts +148 -78
  721. package/src/memory/attachments-store.ts +123 -53
  722. package/src/memory/canonical-guardian-store.ts +190 -48
  723. package/src/memory/channel-delivery-store.ts +5 -5
  724. package/src/memory/channel-guardian-store.ts +31 -16
  725. package/src/memory/checkpoints.ts +14 -7
  726. package/src/memory/clarification-resolver.ts +219 -104
  727. package/src/memory/conflict-intent.ts +74 -23
  728. package/src/memory/conflict-policy.ts +20 -7
  729. package/src/memory/conflict-store.ts +144 -94
  730. package/src/memory/contradiction-checker.ts +257 -132
  731. package/src/memory/conversation-attention-store.ts +74 -32
  732. package/src/memory/conversation-bootstrap.ts +28 -0
  733. package/src/memory/conversation-crud.ts +12 -5
  734. package/src/memory/conversation-display-order-migration.ts +7 -7
  735. package/src/memory/conversation-key-store.ts +18 -13
  736. package/src/memory/conversation-queries.ts +130 -52
  737. package/src/memory/conversation-store.ts +43 -26
  738. package/src/memory/conversation-title-service.ts +89 -66
  739. package/src/memory/db-init.ts +94 -2
  740. package/src/memory/db.ts +10 -3
  741. package/src/memory/delivery-channels.ts +12 -6
  742. package/src/memory/delivery-crud.ts +26 -12
  743. package/src/memory/delivery-status.ts +19 -16
  744. package/src/memory/embedding-backend.ts +205 -77
  745. package/src/memory/embedding-gemini.ts +23 -10
  746. package/src/memory/embedding-local.ts +89 -44
  747. package/src/memory/embedding-ollama.ts +25 -13
  748. package/src/memory/embedding-openai.ts +20 -11
  749. package/src/memory/embedding-runtime-manager.ts +163 -90
  750. package/src/memory/entity-extractor.ts +185 -123
  751. package/src/memory/external-conversation-store.ts +30 -12
  752. package/src/memory/fingerprint.ts +2 -2
  753. package/src/memory/fts-reconciler.ts +57 -28
  754. package/src/memory/guardian-action-store.ts +162 -100
  755. package/src/memory/guardian-approvals.ts +63 -129
  756. package/src/memory/guardian-rate-limits.ts +20 -9
  757. package/src/memory/guardian-verification.ts +82 -35
  758. package/src/memory/indexer.ts +96 -55
  759. package/src/memory/{ingress-invite-store.ts → invite-store.ts} +28 -169
  760. package/src/memory/items-extractor.ts +313 -157
  761. package/src/memory/job-handlers/backfill.ts +116 -63
  762. package/src/memory/job-handlers/cleanup.ts +64 -41
  763. package/src/memory/job-handlers/conflict.ts +90 -49
  764. package/src/memory/job-handlers/embedding.ts +32 -17
  765. package/src/memory/job-handlers/extraction.ts +58 -33
  766. package/src/memory/job-handlers/index-maintenance.ts +31 -17
  767. package/src/memory/job-handlers/media-processing.ts +65 -24
  768. package/src/memory/job-handlers/summarization.ts +186 -128
  769. package/src/memory/job-utils.ts +100 -57
  770. package/src/memory/jobs-store.ts +235 -142
  771. package/src/memory/jobs-worker.ts +167 -83
  772. package/src/memory/llm-request-log-store.ts +13 -11
  773. package/src/memory/llm-usage-store.ts +35 -26
  774. package/src/memory/media-store.ts +151 -44
  775. package/src/memory/message-content.ts +28 -18
  776. package/src/memory/migrations/001-job-deferrals.ts +11 -5
  777. package/src/memory/migrations/002-tool-invocations-fk.ts +14 -6
  778. package/src/memory/migrations/003-memory-fts-backfill.ts +11 -5
  779. package/src/memory/migrations/004-entity-relation-dedup.ts +17 -11
  780. package/src/memory/migrations/005-fingerprint-scope-unique.ts +36 -21
  781. package/src/memory/migrations/006-scope-salted-fingerprints.ts +35 -20
  782. package/src/memory/migrations/007-assistant-id-to-self.ts +40 -27
  783. package/src/memory/migrations/008-remove-assistant-id-columns.ts +58 -36
  784. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +36 -22
  785. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +21 -11
  786. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +30 -15
  787. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +4 -2
  788. package/src/memory/migrations/013-guardian-action-tables.ts +29 -11
  789. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +35 -21
  790. package/src/memory/migrations/015-drop-active-search-index.ts +17 -11
  791. package/src/memory/migrations/016-memory-segments-indexes.ts +7 -3
  792. package/src/memory/migrations/017-memory-items-indexes.ts +4 -2
  793. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -5
  794. package/src/memory/migrations/019-notification-tables-schema-migration.ts +34 -20
  795. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +87 -53
  796. package/src/memory/migrations/021-conversation-status-indexes.ts +7 -3
  797. package/src/memory/migrations/022-add-origin-interface.ts +4 -2
  798. package/src/memory/migrations/023-memory-item-sources-indexes.ts +4 -2
  799. package/src/memory/migrations/024-embedding-vector-blob.ts +34 -18
  800. package/src/memory/migrations/025-messages-fts-backfill.ts +11 -5
  801. package/src/memory/migrations/026-guardian-verification-sessions.ts +80 -14
  802. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +42 -26
  803. package/src/memory/migrations/027-notification-delivery-pairing-columns.ts +22 -8
  804. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +11 -3
  805. package/src/memory/migrations/028-call-session-mode.ts +13 -3
  806. package/src/memory/migrations/028-notification-delivery-client-ack.ts +22 -8
  807. package/src/memory/migrations/029-channel-inbound-delivered-segments.ts +7 -3
  808. package/src/memory/migrations/030-guardian-action-followup.ts +46 -8
  809. package/src/memory/migrations/030-guardian-verification-purpose.ts +4 -2
  810. package/src/memory/migrations/031-conversations-thread-type-index.ts +4 -2
  811. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +4 -2
  812. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +22 -8
  813. package/src/memory/migrations/033-scoped-approval-grants.ts +1 -1
  814. package/src/memory/migrations/034-guardian-action-tool-metadata.ts +15 -3
  815. package/src/memory/migrations/035-guardian-action-supersession.ts +15 -3
  816. package/src/memory/migrations/036-normalize-phone-identities.ts +101 -87
  817. package/src/memory/migrations/037-voice-invite-columns.ts +22 -4
  818. package/src/memory/migrations/038-actor-token-records.ts +5 -9
  819. package/src/memory/migrations/039-actor-refresh-token-records.ts +7 -13
  820. package/src/memory/migrations/100-core-tables.ts +1 -1
  821. package/src/memory/migrations/101-watchers-and-logs.ts +1 -1
  822. package/src/memory/migrations/103-complex-migrations.ts +9 -9
  823. package/src/memory/migrations/104-core-indexes.ts +188 -64
  824. package/src/memory/migrations/105-contacts-and-triage.ts +28 -10
  825. package/src/memory/migrations/106-call-sessions.ts +58 -16
  826. package/src/memory/migrations/107-followups.ts +16 -6
  827. package/src/memory/migrations/108-tasks-and-work-items.ts +43 -11
  828. package/src/memory/migrations/109-external-conversation-bindings.ts +11 -5
  829. package/src/memory/migrations/110-channel-guardian.ts +48 -10
  830. package/src/memory/migrations/111-media-assets.ts +52 -18
  831. package/src/memory/migrations/112-assistant-inbox.ts +32 -12
  832. package/src/memory/migrations/113-late-migrations.ts +12 -12
  833. package/src/memory/migrations/114-notifications.ts +28 -12
  834. package/src/memory/migrations/115-sequences.ts +10 -4
  835. package/src/memory/migrations/116-messages-fts.ts +1 -1
  836. package/src/memory/migrations/117-conversation-attention.ts +16 -6
  837. package/src/memory/migrations/118-reminder-routing-intent.ts +7 -3
  838. package/src/memory/migrations/119-schema-indexes-and-columns.ts +35 -15
  839. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +36 -17
  840. package/src/memory/migrations/121-canonical-guardian-requests.ts +25 -9
  841. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +11 -3
  842. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +4 -2
  843. package/src/memory/migrations/124-voice-invite-display-metadata.ts +15 -3
  844. package/src/memory/migrations/125-guardian-principal-id-columns.ts +22 -4
  845. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +174 -126
  846. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +58 -42
  847. package/src/memory/migrations/128-contacts-role-principal.ts +26 -0
  848. package/src/memory/migrations/129-contact-channels-access-fields.ts +105 -0
  849. package/src/memory/migrations/130-contact-channels-type-ext-chat-id-index.ts +15 -0
  850. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +134 -0
  851. package/src/memory/migrations/132-contacts-assistant-id.ts +21 -0
  852. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  853. package/src/memory/migrations/index.ts +83 -73
  854. package/src/memory/migrations/registry.ts +53 -37
  855. package/src/memory/migrations/validate-migration-state.ts +73 -46
  856. package/src/memory/profile-compiler.ts +58 -24
  857. package/src/memory/published-pages-store.ts +12 -16
  858. package/src/memory/qdrant-circuit-breaker.ts +28 -20
  859. package/src/memory/qdrant-client.ts +99 -63
  860. package/src/memory/qdrant-manager.ts +89 -57
  861. package/src/memory/query-builder.ts +9 -7
  862. package/src/memory/raw-query.ts +63 -14
  863. package/src/memory/recall-cache.ts +15 -8
  864. package/src/memory/retrieval-budget.ts +0 -1
  865. package/src/memory/retriever.ts +385 -192
  866. package/src/memory/schema-migration.ts +1 -1
  867. package/src/memory/schema.ts +56 -56
  868. package/src/memory/scoped-approval-grants.ts +99 -45
  869. package/src/memory/search/entity.ts +102 -40
  870. package/src/memory/search/formatting.ts +70 -52
  871. package/src/memory/search/lexical.ts +82 -43
  872. package/src/memory/search/ranking.ts +103 -39
  873. package/src/memory/search/semantic.ts +59 -35
  874. package/src/memory/search/types.ts +8 -8
  875. package/src/memory/segmenter.ts +20 -12
  876. package/src/memory/shared-app-links-store.ts +21 -16
  877. package/src/memory/slack-thread-store.ts +187 -0
  878. package/src/memory/task-memory-cleanup.ts +18 -8
  879. package/src/memory/tool-usage-store.ts +27 -19
  880. package/src/memory/validation.ts +4 -2
  881. package/src/messaging/activity-analyzer.ts +7 -7
  882. package/src/messaging/draft-store.ts +13 -10
  883. package/src/messaging/email-classifier.ts +73 -37
  884. package/src/messaging/index.ts +3 -3
  885. package/src/messaging/outreach-classifier.ts +76 -38
  886. package/src/messaging/provider-types.ts +2 -4
  887. package/src/messaging/provider.ts +37 -8
  888. package/src/messaging/providers/gmail/adapter.ts +183 -66
  889. package/src/messaging/providers/gmail/client.ts +3 -1
  890. package/src/messaging/providers/gmail/mime-builder.ts +21 -19
  891. package/src/messaging/providers/gmail/people-client.ts +22 -9
  892. package/src/messaging/providers/gmail/types.ts +6 -6
  893. package/src/messaging/providers/slack/adapter.ts +93 -43
  894. package/src/messaging/providers/slack/client.ts +165 -48
  895. package/src/messaging/providers/slack/types.ts +10 -0
  896. package/src/messaging/providers/sms/adapter.ts +76 -40
  897. package/src/messaging/providers/sms/client.ts +4 -4
  898. package/src/messaging/providers/telegram-bot/adapter.ts +52 -30
  899. package/src/messaging/providers/telegram-bot/client.ts +7 -7
  900. package/src/messaging/providers/whatsapp/adapter.ts +58 -31
  901. package/src/messaging/providers/whatsapp/client.ts +4 -4
  902. package/src/messaging/registry.ts +9 -5
  903. package/src/messaging/style-analyzer.ts +69 -39
  904. package/src/messaging/thread-summarizer.ts +101 -53
  905. package/src/messaging/triage-engine.ts +111 -82
  906. package/src/messaging/types.ts +10 -10
  907. package/src/migrations/config-merge.ts +18 -10
  908. package/src/migrations/data-layout.ts +35 -22
  909. package/src/migrations/data-merge.ts +17 -7
  910. package/src/migrations/hooks-merge.ts +43 -16
  911. package/src/migrations/index.ts +6 -6
  912. package/src/migrations/log.ts +9 -5
  913. package/src/migrations/skills-merge.ts +17 -7
  914. package/src/migrations/workspace-layout.ts +39 -25
  915. package/src/notifications/AGENTS.md +5 -0
  916. package/src/notifications/adapters/macos.ts +21 -14
  917. package/src/notifications/adapters/slack.ts +90 -0
  918. package/src/notifications/adapters/sms.ts +28 -15
  919. package/src/notifications/adapters/telegram.ts +24 -15
  920. package/src/notifications/broadcaster.ts +108 -52
  921. package/src/notifications/conversation-pairing.ts +64 -29
  922. package/src/notifications/copy-composer.ts +165 -95
  923. package/src/notifications/decision-engine.ts +353 -147
  924. package/src/notifications/decisions-store.ts +26 -10
  925. package/src/notifications/deliveries-store.ts +23 -13
  926. package/src/notifications/destination-resolver.ts +83 -24
  927. package/src/notifications/deterministic-checks.ts +78 -27
  928. package/src/notifications/emit-signal.ts +95 -41
  929. package/src/notifications/events-store.ts +13 -7
  930. package/src/notifications/guardian-question-mode.ts +125 -75
  931. package/src/notifications/preference-extractor.ts +85 -53
  932. package/src/notifications/preference-summary.ts +31 -18
  933. package/src/notifications/preferences-store.ts +29 -18
  934. package/src/notifications/runtime-dispatch.ts +22 -12
  935. package/src/notifications/signal.ts +4 -4
  936. package/src/notifications/thread-candidates.ts +59 -23
  937. package/src/notifications/thread-seed-composer.ts +45 -27
  938. package/src/notifications/types.ts +19 -10
  939. package/src/oauth/connect-orchestrator.ts +105 -54
  940. package/src/oauth/connect-types.ts +3 -3
  941. package/src/oauth/provider-profiles.ts +102 -59
  942. package/src/oauth/scope-policy.ts +5 -2
  943. package/src/oauth/token-persistence.ts +58 -24
  944. package/src/outbound-proxy/certs.ts +284 -0
  945. package/src/outbound-proxy/config.ts +94 -0
  946. package/src/outbound-proxy/connect-tunnel.ts +84 -0
  947. package/src/outbound-proxy/health.ts +62 -0
  948. package/src/outbound-proxy/host-pattern-match.ts +67 -0
  949. package/src/outbound-proxy/http-forwarder.ts +162 -0
  950. package/src/outbound-proxy/index.ts +80 -0
  951. package/src/outbound-proxy/logging.ts +193 -0
  952. package/src/outbound-proxy/mitm-handler.ts +292 -0
  953. package/src/outbound-proxy/policy.ts +172 -0
  954. package/src/outbound-proxy/router.ts +64 -0
  955. package/src/outbound-proxy/server.ts +145 -0
  956. package/src/outbound-proxy/types.ts +150 -0
  957. package/src/permissions/checker.ts +481 -189
  958. package/src/permissions/defaults.ts +135 -108
  959. package/src/permissions/prompter.ts +53 -27
  960. package/src/permissions/secret-prompter.ts +21 -15
  961. package/src/permissions/shell-identity.ts +47 -16
  962. package/src/permissions/trust-store.ts +185 -73
  963. package/src/permissions/types.ts +22 -12
  964. package/src/permissions/workspace-policy.ts +47 -38
  965. package/src/playbooks/index.ts +10 -2
  966. package/src/playbooks/playbook-compiler.ts +30 -24
  967. package/src/playbooks/types.ts +11 -8
  968. package/src/providers/anthropic/client.ts +328 -168
  969. package/src/providers/failover.ts +57 -22
  970. package/src/providers/fireworks/client.ts +9 -5
  971. package/src/providers/gemini/client.ts +61 -39
  972. package/src/providers/model-intents.ts +40 -33
  973. package/src/providers/ollama/client.ts +7 -7
  974. package/src/providers/openai/client.ts +109 -68
  975. package/src/providers/openrouter/client.ts +9 -5
  976. package/src/providers/provider-send-message.ts +59 -27
  977. package/src/providers/ratelimit.ts +25 -8
  978. package/src/providers/registry.ts +86 -38
  979. package/src/providers/retry.ts +93 -37
  980. package/src/providers/stream-timeout.ts +5 -3
  981. package/src/providers/types.ts +7 -6
  982. package/src/runtime/AGENTS.md +42 -0
  983. package/src/runtime/access-request-helper.ts +118 -68
  984. package/src/runtime/actor-refresh-token-store.ts +21 -16
  985. package/src/runtime/actor-token-store.ts +25 -18
  986. package/src/runtime/actor-trust-resolver.ts +191 -80
  987. package/src/runtime/approval-conversation-turn.ts +39 -26
  988. package/src/runtime/approval-message-composer.ts +116 -84
  989. package/src/runtime/assistant-event-hub.ts +25 -6
  990. package/src/runtime/assistant-event.ts +4 -4
  991. package/src/runtime/assistant-scope.ts +1 -1
  992. package/src/runtime/auth/__tests__/guard-tests.test.ts +36 -14
  993. package/src/runtime/auth/context.ts +8 -7
  994. package/src/runtime/auth/credential-service.ts +60 -38
  995. package/src/runtime/auth/external-assistant-id.ts +16 -8
  996. package/src/runtime/auth/index.ts +23 -16
  997. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  998. package/src/runtime/auth/route-policy.ts +166 -104
  999. package/src/runtime/auth/scopes.ts +22 -29
  1000. package/src/runtime/auth/subject.ts +19 -13
  1001. package/src/runtime/auth/token-service.ts +3 -3
  1002. package/src/runtime/auth/types.ts +23 -23
  1003. package/src/runtime/channel-approval-parser.ts +37 -14
  1004. package/src/runtime/channel-approval-types.ts +30 -4
  1005. package/src/runtime/channel-approvals.ts +49 -23
  1006. package/src/runtime/channel-guardian-service.ts +144 -103
  1007. package/src/runtime/channel-invite-transport.ts +5 -3
  1008. package/src/runtime/channel-invite-transports/telegram.ts +16 -10
  1009. package/src/runtime/channel-invite-transports/voice.ts +7 -7
  1010. package/src/runtime/channel-readiness-service.ts +139 -90
  1011. package/src/runtime/channel-readiness-types.ts +4 -2
  1012. package/src/runtime/channel-reply-delivery.ts +83 -14
  1013. package/src/runtime/channel-retry-sweep.ts +111 -62
  1014. package/src/runtime/confirmation-request-guardian-bridge.ts +73 -54
  1015. package/src/runtime/gateway-client.ts +122 -55
  1016. package/src/runtime/gateway-internal-client.ts +86 -0
  1017. package/src/runtime/guardian-action-conversation-turn.ts +34 -18
  1018. package/src/runtime/guardian-action-followup-executor.ts +115 -45
  1019. package/src/runtime/guardian-action-grant-minter.ts +40 -24
  1020. package/src/runtime/guardian-action-message-composer.ts +105 -84
  1021. package/src/runtime/guardian-action-service.ts +127 -0
  1022. package/src/runtime/guardian-decision-types.ts +28 -13
  1023. package/src/runtime/guardian-outbound-actions.ts +9 -0
  1024. package/src/runtime/guardian-reply-router.ts +274 -145
  1025. package/src/runtime/guardian-vellum-migration.ts +38 -24
  1026. package/src/runtime/guardian-verification-templates.ts +24 -12
  1027. package/src/runtime/http-router.ts +175 -0
  1028. package/src/runtime/http-server.ts +913 -680
  1029. package/src/runtime/http-types.ts +2 -2
  1030. package/src/runtime/invite-redemption-service.ts +211 -134
  1031. package/src/runtime/invite-redemption-templates.ts +18 -11
  1032. package/src/runtime/{ingress-service.ts → invite-service.ts} +92 -151
  1033. package/src/runtime/local-actor-identity.ts +73 -55
  1034. package/src/runtime/middleware/auth.ts +25 -14
  1035. package/src/runtime/middleware/error-handler.ts +15 -11
  1036. package/src/runtime/middleware/rate-limiter.ts +23 -17
  1037. package/src/runtime/middleware/request-logger.ts +4 -4
  1038. package/src/runtime/middleware/twilio-validation.ts +29 -20
  1039. package/src/runtime/migrations/migration-transport.ts +575 -0
  1040. package/src/runtime/migrations/migration-wizard.ts +715 -0
  1041. package/src/runtime/migrations/rebind-secrets-screen.ts +351 -0
  1042. package/src/runtime/migrations/transfer-progress-screen.ts +321 -0
  1043. package/src/runtime/migrations/validation-results-screen.ts +467 -0
  1044. package/src/runtime/migrations/vbundle-builder.ts +295 -0
  1045. package/src/runtime/migrations/vbundle-import-analyzer.ts +212 -0
  1046. package/src/runtime/migrations/vbundle-importer.ts +339 -0
  1047. package/src/runtime/migrations/vbundle-validator.ts +356 -0
  1048. package/src/runtime/nl-approval-parser.ts +138 -0
  1049. package/src/runtime/pending-interactions.ts +16 -7
  1050. package/src/runtime/routes/access-request-decision.ts +73 -52
  1051. package/src/runtime/routes/app-routes.ts +56 -38
  1052. package/src/runtime/routes/approval-routes.ts +144 -92
  1053. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +930 -0
  1054. package/src/runtime/routes/approval-strategies/guardian-legacy-fallback-strategy.ts +82 -0
  1055. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +151 -0
  1056. package/src/runtime/routes/attachment-routes.ts +59 -48
  1057. package/src/runtime/routes/brain-graph-routes.ts +85 -69
  1058. package/src/runtime/routes/call-routes.ts +79 -38
  1059. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +10 -10
  1060. package/src/runtime/routes/channel-delivery-routes.ts +19 -14
  1061. package/src/runtime/routes/channel-guardian-routes.ts +3 -3
  1062. package/src/runtime/routes/channel-inbound-routes.ts +2 -2
  1063. package/src/runtime/routes/channel-readiness-routes.ts +12 -6
  1064. package/src/runtime/routes/channel-route-shared.ts +67 -25
  1065. package/src/runtime/routes/channel-routes.ts +4 -6
  1066. package/src/runtime/routes/contact-routes.ts +374 -17
  1067. package/src/runtime/routes/conversation-attention-routes.ts +57 -28
  1068. package/src/runtime/routes/conversation-routes.ts +321 -174
  1069. package/src/runtime/routes/debug-routes.ts +14 -10
  1070. package/src/runtime/routes/events-routes.ts +90 -57
  1071. package/src/runtime/routes/global-search-routes.ts +266 -0
  1072. package/src/runtime/routes/guardian-action-routes.ts +112 -113
  1073. package/src/runtime/routes/guardian-approval-interception.ts +325 -874
  1074. package/src/runtime/routes/guardian-approval-prompt.ts +40 -24
  1075. package/src/runtime/routes/guardian-approval-reply-helpers.ts +135 -0
  1076. package/src/runtime/routes/guardian-bootstrap-routes.ts +55 -36
  1077. package/src/runtime/routes/guardian-expiry-sweep.ts +63 -37
  1078. package/src/runtime/routes/guardian-refresh-routes.ts +40 -19
  1079. package/src/runtime/routes/identity-routes.ts +71 -42
  1080. package/src/runtime/routes/inbound-conversation.ts +17 -11
  1081. package/src/runtime/routes/inbound-message-handler.ts +305 -1459
  1082. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +880 -0
  1083. package/src/runtime/routes/inbound-stages/background-dispatch.ts +600 -0
  1084. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +214 -0
  1085. package/src/runtime/routes/inbound-stages/edit-intercept.ts +116 -0
  1086. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +167 -0
  1087. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +185 -0
  1088. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +132 -0
  1089. package/src/runtime/routes/inbound-stages/verification-intercept.ts +340 -0
  1090. package/src/runtime/routes/integration-routes.ts +60 -21
  1091. package/src/runtime/routes/invite-routes.ts +140 -0
  1092. package/src/runtime/routes/migration-routes.ts +434 -0
  1093. package/src/runtime/routes/pairing-routes.ts +157 -79
  1094. package/src/runtime/routes/secret-routes.ts +6 -2
  1095. package/src/runtime/routes/twilio-routes.ts +443 -249
  1096. package/src/runtime/slack-block-formatting.ts +176 -0
  1097. package/src/runtime/tool-grant-request-helper.ts +36 -27
  1098. package/src/runtime/{guardian-context-resolver.ts → trust-context-resolver.ts} +29 -41
  1099. package/src/schedule/integration-status.ts +44 -9
  1100. package/src/schedule/recurrence-engine.ts +47 -24
  1101. package/src/schedule/recurrence-types.ts +12 -7
  1102. package/src/schedule/schedule-store.ts +166 -83
  1103. package/src/schedule/scheduler.ts +37 -24
  1104. package/src/security/encrypted-store.ts +68 -38
  1105. package/src/security/keychain.ts +183 -120
  1106. package/src/security/oauth-callback-registry.ts +3 -3
  1107. package/src/security/oauth2.ts +226 -138
  1108. package/src/security/redaction.ts +24 -24
  1109. package/src/security/secret-allowlist.ts +46 -21
  1110. package/src/security/secret-ingress.ts +15 -7
  1111. package/src/security/secret-scanner.ts +193 -104
  1112. package/src/security/secure-keys.ts +9 -3
  1113. package/src/security/token-manager.ts +99 -40
  1114. package/src/security/tool-approval-digest.ts +3 -3
  1115. package/src/sequence/analytics.ts +52 -27
  1116. package/src/sequence/engine.ts +135 -72
  1117. package/src/sequence/guardrails.ts +32 -20
  1118. package/src/sequence/importer.ts +75 -37
  1119. package/src/sequence/reply-matcher.ts +36 -18
  1120. package/src/sequence/store.ts +137 -75
  1121. package/src/sequence/types.ts +30 -16
  1122. package/src/services/published-app-updater.ts +26 -16
  1123. package/src/services/vercel-deploy.ts +19 -15
  1124. package/src/skills/active-skill-tools.ts +3 -3
  1125. package/src/skills/clawhub.ts +178 -90
  1126. package/src/skills/include-graph.ts +24 -17
  1127. package/src/skills/managed-store.ts +89 -42
  1128. package/src/skills/path-classifier.ts +10 -10
  1129. package/src/skills/remote-skill-policy.ts +31 -22
  1130. package/src/skills/slash-commands.ts +36 -30
  1131. package/src/skills/tool-manifest.ts +60 -31
  1132. package/src/skills/version-hash.ts +25 -15
  1133. package/src/slack/slack-webhook.ts +19 -15
  1134. package/src/subagent/index.ts +4 -8
  1135. package/src/subagent/manager.ts +119 -69
  1136. package/src/subagent/types.ts +9 -12
  1137. package/src/swarm/backend-claude-code.ts +124 -45
  1138. package/src/swarm/checkpoint.ts +36 -16
  1139. package/src/swarm/graph-utils.ts +1 -3
  1140. package/src/swarm/index.ts +38 -19
  1141. package/src/swarm/limits.ts +13 -4
  1142. package/src/swarm/orchestrator.ts +108 -57
  1143. package/src/swarm/plan-validator.ts +23 -17
  1144. package/src/swarm/router-planner.ts +51 -22
  1145. package/src/swarm/router-prompts.ts +4 -1
  1146. package/src/swarm/synthesizer.ts +26 -18
  1147. package/src/swarm/types.ts +14 -4
  1148. package/src/swarm/worker-backend.ts +36 -26
  1149. package/src/swarm/worker-prompts.ts +13 -9
  1150. package/src/swarm/worker-runner.ts +40 -34
  1151. package/src/tasks/candidate-store.ts +14 -6
  1152. package/src/tasks/ephemeral-permissions.ts +9 -5
  1153. package/src/tasks/task-compiler.ts +41 -38
  1154. package/src/tasks/task-runner.ts +54 -26
  1155. package/src/tasks/task-scheduler.ts +1 -1
  1156. package/src/tasks/task-store.ts +20 -7
  1157. package/src/tasks/tool-sanitizer.ts +3 -3
  1158. package/src/tools/apps/definitions.ts +23 -15
  1159. package/src/tools/apps/executors.ts +122 -40
  1160. package/src/tools/apps/open-proxy.ts +5 -5
  1161. package/src/tools/apps/registry.ts +2 -2
  1162. package/src/tools/assets/materialize.ts +59 -41
  1163. package/src/tools/assets/search.ts +86 -48
  1164. package/src/tools/browser/api-map.ts +52 -36
  1165. package/src/tools/browser/auth-cache.ts +21 -18
  1166. package/src/tools/browser/auth-detector.ts +43 -28
  1167. package/src/tools/browser/auto-navigate.ts +149 -68
  1168. package/src/tools/browser/browser-execution.ts +9 -3
  1169. package/src/tools/browser/headless-browser.ts +287 -150
  1170. package/src/tools/browser/jit-auth.ts +37 -21
  1171. package/src/tools/browser/network-recorder.ts +138 -56
  1172. package/src/tools/browser/recording-store.ts +22 -15
  1173. package/src/tools/browser/runtime-check.ts +8 -5
  1174. package/src/tools/browser/x-auto-navigate.ts +88 -47
  1175. package/src/tools/calls/call-end.ts +10 -7
  1176. package/src/tools/calls/call-start.ts +30 -20
  1177. package/src/tools/calls/call-status.ts +8 -5
  1178. package/src/tools/claude-code/claude-code.ts +301 -165
  1179. package/src/tools/computer-use/definitions.ts +175 -130
  1180. package/src/tools/computer-use/registry.ts +2 -2
  1181. package/src/tools/computer-use/request-computer-control.ts +21 -13
  1182. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
  1183. package/src/tools/credentials/account-registry.ts +52 -35
  1184. package/src/tools/credentials/broker-types.ts +1 -1
  1185. package/src/tools/credentials/broker.ts +97 -55
  1186. package/src/tools/credentials/domain-policy.ts +5 -2
  1187. package/src/tools/credentials/host-pattern-match.ts +15 -8
  1188. package/src/tools/credentials/metadata-store.ts +93 -43
  1189. package/src/tools/credentials/policy-types.ts +5 -2
  1190. package/src/tools/credentials/policy-validate.ts +21 -14
  1191. package/src/tools/credentials/post-connect-hooks.ts +18 -7
  1192. package/src/tools/credentials/resolve.ts +11 -10
  1193. package/src/tools/credentials/selection.ts +30 -25
  1194. package/src/tools/credentials/tool-policy.ts +5 -2
  1195. package/src/tools/credentials/vault.ts +538 -185
  1196. package/src/tools/document/document-tool.ts +23 -17
  1197. package/src/tools/document/editor-template.ts +12 -7
  1198. package/src/tools/execution-target.ts +13 -10
  1199. package/src/tools/execution-timeout.ts +6 -5
  1200. package/src/tools/executor.ts +141 -74
  1201. package/src/tools/filesystem/edit.ts +82 -45
  1202. package/src/tools/filesystem/fuzzy-match.ts +70 -32
  1203. package/src/tools/filesystem/read.ts +46 -28
  1204. package/src/tools/filesystem/view-image.ts +86 -42
  1205. package/src/tools/filesystem/write.ts +53 -32
  1206. package/src/tools/followups/followup_create.ts +43 -17
  1207. package/src/tools/followups/followup_list.ts +28 -13
  1208. package/src/tools/followups/followup_resolve.ts +9 -6
  1209. package/src/tools/guardian-control-plane-policy.ts +15 -14
  1210. package/src/tools/host-filesystem/edit.ts +77 -42
  1211. package/src/tools/host-filesystem/read.ts +52 -33
  1212. package/src/tools/host-filesystem/write.ts +50 -29
  1213. package/src/tools/host-terminal/host-shell.ts +97 -61
  1214. package/src/tools/mcp/mcp-tool-factory.ts +21 -14
  1215. package/src/tools/memory/definitions.ts +60 -28
  1216. package/src/tools/memory/handlers.ts +149 -77
  1217. package/src/tools/memory/register.ts +39 -16
  1218. package/src/tools/network/__tests__/web-search.test.ts +236 -177
  1219. package/src/tools/network/domain-normalize.ts +13 -9
  1220. package/src/tools/network/script-proxy/__tests__/logging.test.ts +193 -123
  1221. package/src/tools/network/script-proxy/__tests__/policy.test.ts +225 -127
  1222. package/src/tools/network/script-proxy/index.ts +1 -17
  1223. package/src/tools/network/script-proxy/session-manager.ts +178 -86
  1224. package/src/tools/network/url-safety.ts +56 -34
  1225. package/src/tools/network/web-fetch.ts +273 -155
  1226. package/src/tools/network/web-search.ts +166 -81
  1227. package/src/tools/permission-checker.ts +24 -25
  1228. package/src/tools/policy-context.ts +8 -5
  1229. package/src/tools/registry.ts +73 -46
  1230. package/src/tools/reminder/reminder-store.ts +65 -44
  1231. package/src/tools/reminder/reminder.ts +76 -35
  1232. package/src/tools/schedule/create.ts +44 -21
  1233. package/src/tools/schedule/delete.ts +8 -5
  1234. package/src/tools/schedule/list.ts +39 -19
  1235. package/src/tools/schedule/update.ts +49 -26
  1236. package/src/tools/secret-detection-handler.ts +130 -49
  1237. package/src/tools/sensitive-output-placeholders.ts +15 -8
  1238. package/src/tools/shared/filesystem/edit-engine.ts +45 -14
  1239. package/src/tools/shared/filesystem/errors.ts +18 -18
  1240. package/src/tools/shared/filesystem/file-ops-service.ts +59 -32
  1241. package/src/tools/shared/filesystem/format-diff.ts +21 -11
  1242. package/src/tools/shared/filesystem/path-policy.ts +17 -13
  1243. package/src/tools/shared/filesystem/size-guard.ts +8 -4
  1244. package/src/tools/shared/filesystem/types.ts +2 -2
  1245. package/src/tools/shared/shell-output.ts +4 -3
  1246. package/src/tools/side-effects.ts +36 -28
  1247. package/src/tools/skills/delete-managed.ts +30 -17
  1248. package/src/tools/skills/load.ts +88 -46
  1249. package/src/tools/skills/sandbox-runner.ts +62 -46
  1250. package/src/tools/skills/scaffold-managed.ts +98 -48
  1251. package/src/tools/skills/script-contract.ts +5 -2
  1252. package/src/tools/skills/skill-script-runner.ts +29 -13
  1253. package/src/tools/skills/skill-tool-factory.ts +20 -10
  1254. package/src/tools/subagent/abort.ts +10 -4
  1255. package/src/tools/subagent/message.ts +14 -8
  1256. package/src/tools/subagent/read.ts +20 -11
  1257. package/src/tools/subagent/spawn.ts +14 -6
  1258. package/src/tools/subagent/status.ts +7 -4
  1259. package/src/tools/swarm/delegate.ts +75 -49
  1260. package/src/tools/system/avatar-generator.ts +46 -33
  1261. package/src/tools/system/navigate-settings.ts +29 -19
  1262. package/src/tools/system/open-system-settings.ts +30 -20
  1263. package/src/tools/system/request-permission.ts +59 -44
  1264. package/src/tools/system/version.ts +27 -16
  1265. package/src/tools/system/voice-config.ts +116 -53
  1266. package/src/tools/tasks/index.ts +8 -8
  1267. package/src/tools/tasks/task-delete.ts +61 -22
  1268. package/src/tools/tasks/task-list.ts +23 -11
  1269. package/src/tools/tasks/task-run.ts +41 -16
  1270. package/src/tools/tasks/task-save.ts +27 -10
  1271. package/src/tools/tasks/work-item-enqueue.ts +114 -48
  1272. package/src/tools/tasks/work-item-list.ts +20 -10
  1273. package/src/tools/tasks/work-item-remove.ts +49 -15
  1274. package/src/tools/tasks/work-item-run.ts +34 -13
  1275. package/src/tools/tasks/work-item-update.ts +84 -31
  1276. package/src/tools/terminal/backends/native.ts +64 -35
  1277. package/src/tools/terminal/backends/types.ts +6 -2
  1278. package/src/tools/terminal/parser.ts +200 -125
  1279. package/src/tools/terminal/safe-env.ts +27 -21
  1280. package/src/tools/terminal/sandbox-diagnostics.ts +31 -13
  1281. package/src/tools/terminal/sandbox.ts +10 -6
  1282. package/src/tools/terminal/shell.ts +134 -68
  1283. package/src/tools/tool-approval-handler.ts +239 -140
  1284. package/src/tools/types.ts +79 -22
  1285. package/src/tools/ui-surface/definitions.ts +124 -89
  1286. package/src/tools/ui-surface/registry.ts +2 -2
  1287. package/src/tools/watch/screen-watch.ts +50 -32
  1288. package/src/tools/watch/watch-state.ts +41 -15
  1289. package/src/tools/watcher/create.ts +37 -15
  1290. package/src/tools/watcher/delete.ts +9 -6
  1291. package/src/tools/watcher/digest.ts +10 -6
  1292. package/src/tools/watcher/list.ts +37 -14
  1293. package/src/tools/watcher/update.ts +33 -18
  1294. package/src/tools/weather/service.ts +331 -174
  1295. package/src/twitter/client.ts +261 -138
  1296. package/src/twitter/oauth-client.ts +17 -13
  1297. package/src/twitter/router.ts +51 -23
  1298. package/src/twitter/session.ts +27 -18
  1299. package/src/types/qrcode.d.ts +6 -3
  1300. package/src/usage/actors.ts +16 -16
  1301. package/src/usage/types.ts +3 -3
  1302. package/src/util/bundled-asset.ts +10 -6
  1303. package/src/util/canonicalize-identity.ts +11 -4
  1304. package/src/util/clipboard.ts +7 -7
  1305. package/src/util/content-id.ts +3 -3
  1306. package/src/util/debounce.ts +3 -2
  1307. package/src/util/diff.ts +55 -33
  1308. package/src/util/errors.ts +31 -27
  1309. package/src/util/fs.ts +8 -2
  1310. package/src/util/log-redact.ts +12 -12
  1311. package/src/util/logger.ts +112 -51
  1312. package/src/util/network-info.ts +13 -5
  1313. package/src/util/object.ts +4 -2
  1314. package/src/util/phone.ts +4 -4
  1315. package/src/util/platform.ts +80 -58
  1316. package/src/util/pricing.ts +49 -31
  1317. package/src/util/retry.ts +39 -7
  1318. package/src/util/row-mapper.ts +7 -4
  1319. package/src/util/silently.ts +7 -4
  1320. package/src/util/spawn.ts +48 -0
  1321. package/src/util/spinner.ts +9 -7
  1322. package/src/util/time.ts +16 -3
  1323. package/src/util/truncate.ts +1 -1
  1324. package/src/util/voice-code.ts +6 -4
  1325. package/src/util/xml.ts +5 -1
  1326. package/src/version.ts +12 -8
  1327. package/src/watcher/engine.ts +71 -44
  1328. package/src/watcher/provider-registry.ts +1 -1
  1329. package/src/watcher/providers/github.ts +40 -23
  1330. package/src/watcher/providers/gmail.ts +59 -38
  1331. package/src/watcher/providers/google-calendar.ts +62 -48
  1332. package/src/watcher/providers/linear.ts +219 -150
  1333. package/src/watcher/providers/slack.ts +125 -29
  1334. package/src/watcher/watcher-store.ts +75 -55
  1335. package/src/work-items/work-item-runner.ts +62 -29
  1336. package/src/work-items/work-item-store.ts +137 -47
  1337. package/src/workspace/commit-message-enrichment-service.ts +65 -25
  1338. package/src/workspace/commit-message-provider.ts +14 -12
  1339. package/src/workspace/git-service.ts +355 -239
  1340. package/src/workspace/heartbeat-service.ts +74 -37
  1341. package/src/workspace/provider-commit-message-generator.ts +95 -70
  1342. package/src/workspace/top-level-renderer.ts +10 -8
  1343. package/src/workspace/top-level-scanner.ts +9 -3
  1344. package/src/workspace/turn-commit.ts +63 -36
  1345. package/src/__tests__/ingress-member-store.test.ts +0 -294
  1346. package/src/__tests__/script-proxy-router.test.ts +0 -215
  1347. package/src/config/bundled-skills/trusted-contacts/SKILL.md +0 -372
  1348. package/src/memory/guardian-bindings.ts +0 -158
  1349. package/src/memory/ingress-member-store.ts +0 -352
  1350. package/src/runtime/routes/ingress-routes.ts +0 -229
  1351. package/src/tools/network/script-proxy/__tests__/router.test.ts +0 -77
  1352. package/src/tools/network/script-proxy/certs.ts +0 -7
  1353. package/src/tools/network/script-proxy/connect-tunnel.ts +0 -1
  1354. package/src/tools/network/script-proxy/http-forwarder.ts +0 -2
  1355. package/src/tools/network/script-proxy/logging.ts +0 -12
  1356. package/src/tools/network/script-proxy/mitm-handler.ts +0 -2
  1357. package/src/tools/network/script-proxy/policy.ts +0 -4
  1358. package/src/tools/network/script-proxy/router.ts +0 -2
  1359. package/src/tools/network/script-proxy/server.ts +0 -5
  1360. package/src/tools/network/script-proxy/types.ts +0 -19
@@ -6,38 +6,46 @@
6
6
  * from Twilio and can send text tokens back for TTS.
7
7
  */
8
8
 
9
- import { randomInt } from 'node:crypto';
10
-
11
- import type { ServerWebSocket } from 'bun';
12
-
13
- import { getConfig } from '../config/loader.js';
14
- import { resolveUserReference } from '../config/user-reference.js';
15
- import { getAssistantName } from '../daemon/identity-helpers.js';
16
- import { getCanonicalGuardianRequest } from '../memory/canonical-guardian-store.js';
17
- import { listActiveBindingsByAssistant } from '../memory/channel-guardian-store.js';
18
- import * as conversationStore from '../memory/conversation-store.js';
19
- import { findActiveVoiceInvites } from '../memory/ingress-invite-store.js';
20
- import { findMember, upsertMember } from '../memory/ingress-member-store.js';
21
- import { revokeScopedApprovalGrantsForContext } from '../memory/scoped-approval-grants.js';
22
- import { emitNotificationSignal } from '../notifications/emit-signal.js';
23
- import { notifyGuardianOfAccessRequest } from '../runtime/access-request-helper.js';
9
+ import { randomInt } from "node:crypto";
10
+
11
+ import type { ServerWebSocket } from "bun";
12
+
13
+ import { getConfig } from "../config/loader.js";
14
+ import { resolveUserReference } from "../config/user-reference.js";
15
+ import {
16
+ findContactChannel,
17
+ findGuardianForChannel,
18
+ listGuardianChannels,
19
+ } from "../contacts/contact-store.js";
20
+ import {
21
+ createGuardianBinding,
22
+ revokeGuardianBinding,
23
+ upsertMember,
24
+ } from "../contacts/contacts-write.js";
25
+ import { getAssistantName } from "../daemon/identity-helpers.js";
26
+ import { getCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
27
+ import * as conversationStore from "../memory/conversation-store.js";
28
+ import { findActiveVoiceInvites } from "../memory/invite-store.js";
29
+ import { revokeScopedApprovalGrantsForContext } from "../memory/scoped-approval-grants.js";
30
+ import { emitNotificationSignal } from "../notifications/emit-signal.js";
31
+ import { notifyGuardianOfAccessRequest } from "../runtime/access-request-helper.js";
24
32
  import {
25
33
  resolveActorTrust,
26
- toGuardianRuntimeContextFromTrust,
27
- } from '../runtime/actor-trust-resolver.js';
28
- import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
34
+ toTrustContext,
35
+ } from "../runtime/actor-trust-resolver.js";
36
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
29
37
  import {
30
38
  getGuardianBinding,
31
39
  getPendingChallenge,
32
40
  validateAndConsumeChallenge,
33
- } from '../runtime/channel-guardian-service.js';
41
+ } from "../runtime/channel-guardian-service.js";
34
42
  import {
35
43
  composeVerificationVoice,
36
44
  GUARDIAN_VERIFY_TEMPLATE_KEYS,
37
- } from '../runtime/guardian-verification-templates.js';
38
- import { redeemVoiceInviteCode } from '../runtime/ingress-service.js';
39
- import { parseJsonSafe } from '../util/json.js';
40
- import { getLogger } from '../util/logger.js';
45
+ } from "../runtime/guardian-verification-templates.js";
46
+ import { redeemVoiceInviteCode } from "../runtime/invite-service.js";
47
+ import { parseJsonSafe } from "../util/json.js";
48
+ import { getLogger } from "../util/logger.js";
41
49
  import {
42
50
  getAccessRequestPollIntervalMs,
43
51
  getGuardianWaitUpdateInitialIntervalMs,
@@ -46,31 +54,34 @@ import {
46
54
  getGuardianWaitUpdateSteadyMinIntervalMs,
47
55
  getTtsPlaybackDelayMs,
48
56
  getUserConsultationTimeoutMs,
49
- } from './call-constants.js';
50
- import { CallController } from './call-controller.js';
51
- import { persistCallCompletionMessage } from './call-conversation-messages.js';
52
- import { addPointerMessage, formatDuration } from './call-pointer-messages.js';
53
- import { fireCallCompletionNotifier,fireCallTranscriptNotifier } from './call-state.js';
54
- import { isTerminalState } from './call-state-machine.js';
57
+ } from "./call-constants.js";
58
+ import { CallController } from "./call-controller.js";
59
+ import { persistCallCompletionMessage } from "./call-conversation-messages.js";
60
+ import { addPointerMessage, formatDuration } from "./call-pointer-messages.js";
61
+ import {
62
+ fireCallCompletionNotifier,
63
+ fireCallTranscriptNotifier,
64
+ } from "./call-state.js";
65
+ import { isTerminalState } from "./call-state-machine.js";
55
66
  import {
56
67
  expirePendingQuestions,
57
68
  getCallSession,
58
69
  recordCallEvent,
59
70
  updateCallSession,
60
- } from './call-store.js';
71
+ } from "./call-store.js";
61
72
  import {
62
73
  extractPromptSpeakerMetadata,
63
74
  type PromptSpeakerContext,
64
75
  SpeakerIdentityTracker,
65
- } from './speaker-identification.js';
76
+ } from "./speaker-identification.js";
66
77
 
67
- const log = getLogger('relay-server');
78
+ const log = getLogger("relay-server");
68
79
 
69
80
  // ── ConversationRelay message types ──────────────────────────────────
70
81
 
71
82
  // Messages FROM Twilio
72
83
  export interface RelaySetupMessage {
73
- type: 'setup';
84
+ type: "setup";
74
85
  callSid: string;
75
86
  from: string;
76
87
  to: string;
@@ -78,7 +89,7 @@ export interface RelaySetupMessage {
78
89
  }
79
90
 
80
91
  export interface RelayPromptMessage {
81
- type: 'prompt';
92
+ type: "prompt";
82
93
  voicePrompt: string;
83
94
  lang: string;
84
95
  last: boolean;
@@ -102,17 +113,17 @@ export interface RelayPromptMessage {
102
113
  }
103
114
 
104
115
  export interface RelayInterruptMessage {
105
- type: 'interrupt';
116
+ type: "interrupt";
106
117
  utteranceUntilInterrupt: string;
107
118
  }
108
119
 
109
120
  export interface RelayDtmfMessage {
110
- type: 'dtmf';
121
+ type: "dtmf";
111
122
  digit: string;
112
123
  }
113
124
 
114
125
  export interface RelayErrorMessage {
115
- type: 'error';
126
+ type: "error";
116
127
  description: string;
117
128
  }
118
129
 
@@ -125,13 +136,13 @@ export type RelayInboundMessage =
125
136
 
126
137
  // Messages TO Twilio
127
138
  export interface RelayTextMessage {
128
- type: 'text';
139
+ type: "text";
129
140
  token: string;
130
141
  last: boolean;
131
142
  }
132
143
 
133
144
  export interface RelayEndMessage {
134
- type: 'end';
145
+ type: "end";
135
146
  handoffData?: string;
136
147
  }
137
148
 
@@ -147,10 +158,14 @@ export interface RelayWebSocketData {
147
158
  export const activeRelayConnections = new Map<string, RelayConnection>();
148
159
 
149
160
  /** Module-level broadcast function, set by the HTTP server during startup. */
150
- let globalBroadcast: ((msg: import('../daemon/ipc-contract.js').ServerMessage) => void) | undefined;
161
+ let globalBroadcast:
162
+ | ((msg: import("../daemon/ipc-contract.js").ServerMessage) => void)
163
+ | undefined;
151
164
 
152
165
  /** Register a broadcast function so RelayConnection can forward IPC events. */
153
- export function setRelayBroadcast(fn: (msg: import('../daemon/ipc-contract.js').ServerMessage) => void): void {
166
+ export function setRelayBroadcast(
167
+ fn: (msg: import("../daemon/ipc-contract.js").ServerMessage) => void,
168
+ ): void {
154
169
  globalBroadcast = fn;
155
170
  }
156
171
 
@@ -159,13 +174,18 @@ export function setRelayBroadcast(fn: (msg: import('../daemon/ipc-contract.js').
159
174
  /**
160
175
  * Manages a single WebSocket connection for one call.
161
176
  */
162
- export type RelayConnectionState = 'connected' | 'verification_pending' | 'awaiting_name' | 'awaiting_guardian_decision' | 'disconnecting';
177
+ export type RelayConnectionState =
178
+ | "connected"
179
+ | "verification_pending"
180
+ | "awaiting_name"
181
+ | "awaiting_guardian_decision"
182
+ | "disconnecting";
163
183
 
164
184
  export class RelayConnection {
165
185
  private ws: ServerWebSocket<RelayWebSocketData>;
166
186
  private callSessionId: string;
167
187
  private conversationHistory: Array<{
168
- role: 'caller' | 'assistant';
188
+ role: "caller" | "assistant";
169
189
  text: string;
170
190
  timestamp: number;
171
191
  speaker?: PromptSpeakerContext;
@@ -175,12 +195,12 @@ export class RelayConnection {
175
195
  private speakerIdentityTracker: SpeakerIdentityTracker;
176
196
 
177
197
  // Verification state (outbound callee verification)
178
- private connectionState: RelayConnectionState = 'connected';
198
+ private connectionState: RelayConnectionState = "connected";
179
199
  private verificationCode: string | null = null;
180
200
  private verificationAttempts = 0;
181
201
  private verificationMaxAttempts = 3;
182
202
  private verificationCodeLength = 6;
183
- private dtmfBuffer = '';
203
+ private dtmfBuffer = "";
184
204
 
185
205
  // Inbound voice guardian verification state
186
206
  private guardianVerificationActive = false;
@@ -204,14 +224,16 @@ export class RelayConnection {
204
224
  private accessRequestAssistantId: string | null = null;
205
225
  private accessRequestFromNumber: string | null = null;
206
226
  private accessRequestPollTimer: ReturnType<typeof setInterval> | null = null;
207
- private accessRequestTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
227
+ private accessRequestTimeoutTimer: ReturnType<typeof setTimeout> | null =
228
+ null;
208
229
  private accessRequestCallerName: string | null = null;
209
230
 
210
231
  // Name capture timeout (unknown inbound callers)
211
232
  private nameCaptureTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
212
233
 
213
234
  // Guardian wait heartbeat state
214
- private accessRequestHeartbeatTimer: ReturnType<typeof setTimeout> | null = null;
235
+ private accessRequestHeartbeatTimer: ReturnType<typeof setTimeout> | null =
236
+ null;
215
237
  private accessRequestWaitStartedAt: number = 0;
216
238
  private heartbeatSequence = 0;
217
239
 
@@ -259,28 +281,37 @@ export class RelayConnection {
259
281
  async handleMessage(data: string): Promise<void> {
260
282
  const parsed = parseJsonSafe<RelayInboundMessage>(data);
261
283
  if (!parsed) {
262
- log.warn({ callSessionId: this.callSessionId, data }, 'Failed to parse relay message');
284
+ log.warn(
285
+ { callSessionId: this.callSessionId, data },
286
+ "Failed to parse relay message",
287
+ );
263
288
  return;
264
289
  }
265
290
 
266
291
  switch (parsed.type) {
267
- case 'setup':
292
+ case "setup":
268
293
  await this.handleSetup(parsed);
269
294
  break;
270
- case 'prompt':
295
+ case "prompt":
271
296
  await this.handlePrompt(parsed);
272
297
  break;
273
- case 'interrupt':
298
+ case "interrupt":
274
299
  this.handleInterrupt(parsed);
275
300
  break;
276
- case 'dtmf':
301
+ case "dtmf":
277
302
  this.handleDtmf(parsed);
278
303
  break;
279
- case 'error':
304
+ case "error":
280
305
  this.handleError(parsed);
281
306
  break;
282
307
  default:
283
- log.warn({ callSessionId: this.callSessionId, type: (parsed as { type: unknown }).type }, 'Unknown relay message type');
308
+ log.warn(
309
+ {
310
+ callSessionId: this.callSessionId,
311
+ type: (parsed as { type: unknown }).type,
312
+ },
313
+ "Unknown relay message type",
314
+ );
284
315
  }
285
316
  }
286
317
 
@@ -288,11 +319,14 @@ export class RelayConnection {
288
319
  * Send a text token to the caller for TTS playback.
289
320
  */
290
321
  sendTextToken(token: string, last: boolean): void {
291
- const message: RelayTextMessage = { type: 'text', token, last };
322
+ const message: RelayTextMessage = { type: "text", token, last };
292
323
  try {
293
324
  this.ws.send(JSON.stringify(message));
294
325
  } catch (err) {
295
- log.error({ err, callSessionId: this.callSessionId }, 'Failed to send text token');
326
+ log.error(
327
+ { err, callSessionId: this.callSessionId },
328
+ "Failed to send text token",
329
+ );
296
330
  }
297
331
  }
298
332
 
@@ -300,22 +334,33 @@ export class RelayConnection {
300
334
  * End the ConversationRelay session.
301
335
  */
302
336
  endSession(reason?: string): void {
303
- const message: RelayEndMessage = { type: 'end' };
337
+ const message: RelayEndMessage = { type: "end" };
304
338
  if (reason) {
305
339
  message.handoffData = JSON.stringify({ reason });
306
340
  }
307
341
  try {
308
342
  this.ws.send(JSON.stringify(message));
309
343
  } catch (err) {
310
- log.error({ err, callSessionId: this.callSessionId }, 'Failed to send end message');
344
+ log.error(
345
+ { err, callSessionId: this.callSessionId },
346
+ "Failed to send end message",
347
+ );
311
348
  }
312
349
  }
313
350
 
314
351
  /**
315
352
  * Get the conversation history for context.
316
353
  */
317
- getConversationHistory(): Array<{ role: string; text: string; speaker?: PromptSpeakerContext }> {
318
- return this.conversationHistory.map(({ role, text, speaker }) => ({ role, text, speaker }));
354
+ getConversationHistory(): Array<{
355
+ role: string;
356
+ text: string;
357
+ speaker?: PromptSpeakerContext;
358
+ }> {
359
+ return this.conversationHistory.map(({ role, text, speaker }) => ({
360
+ role,
361
+ text,
362
+ speaker,
363
+ }));
319
364
  }
320
365
 
321
366
  /**
@@ -365,7 +410,10 @@ export class RelayConnection {
365
410
  }
366
411
  this.accessRequestWaitActive = false;
367
412
  this.abortController.abort();
368
- log.info({ callSessionId: this.callSessionId }, 'RelayConnection destroyed');
413
+ log.info(
414
+ { callSessionId: this.callSessionId },
415
+ "RelayConnection destroyed",
416
+ );
369
417
  }
370
418
 
371
419
  /**
@@ -378,7 +426,7 @@ export class RelayConnection {
378
426
  // If the call was still in guardian-wait with callback opt-in, emit the
379
427
  // handoff notification before cleaning up wait state.
380
428
  if (this.accessRequestWaitActive && this.callbackOptIn) {
381
- this.emitAccessRequestCallbackHandoff('transport_closed');
429
+ this.emitAccessRequestCallbackHandoff("transport_closed");
382
430
  }
383
431
 
384
432
  // Clean up access request wait state on disconnect to stop polling
@@ -395,41 +443,60 @@ export class RelayConnection {
395
443
  const isNormalClose = code === 1000;
396
444
  if (isNormalClose) {
397
445
  updateCallSession(this.callSessionId, {
398
- status: 'completed',
446
+ status: "completed",
399
447
  endedAt: Date.now(),
400
448
  });
401
- recordCallEvent(this.callSessionId, 'call_ended', {
402
- reason: reason || 'relay_closed',
449
+ recordCallEvent(this.callSessionId, "call_ended", {
450
+ reason: reason || "relay_closed",
403
451
  closeCode: code,
404
452
  });
405
453
 
406
454
  // Post a pointer message in the initiating conversation
407
455
  if (session.initiatedFromConversationId) {
408
- const durationMs = session.startedAt ? Date.now() - session.startedAt : 0;
409
- addPointerMessage(session.initiatedFromConversationId, 'completed', session.toNumber, {
410
- duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
411
- }).catch((err) => {
412
- log.warn({ conversationId: session.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
456
+ const durationMs = session.startedAt
457
+ ? Date.now() - session.startedAt
458
+ : 0;
459
+ addPointerMessage(
460
+ session.initiatedFromConversationId,
461
+ "completed",
462
+ session.toNumber,
463
+ {
464
+ duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
465
+ },
466
+ ).catch((err) => {
467
+ log.warn(
468
+ { conversationId: session.initiatedFromConversationId, err },
469
+ "Skipping pointer write — origin conversation may no longer exist",
470
+ );
413
471
  });
414
472
  }
415
473
  } else {
416
- const detail = reason || (code ? `relay_closed_${code}` : 'relay_closed_abnormal');
474
+ const detail =
475
+ reason || (code ? `relay_closed_${code}` : "relay_closed_abnormal");
417
476
  updateCallSession(this.callSessionId, {
418
- status: 'failed',
477
+ status: "failed",
419
478
  endedAt: Date.now(),
420
479
  lastError: `Relay websocket closed unexpectedly: ${detail}`,
421
480
  });
422
- recordCallEvent(this.callSessionId, 'call_failed', {
481
+ recordCallEvent(this.callSessionId, "call_failed", {
423
482
  reason: detail,
424
483
  closeCode: code,
425
484
  });
426
485
 
427
486
  // Post a failure pointer message in the initiating conversation
428
487
  if (session.initiatedFromConversationId) {
429
- addPointerMessage(session.initiatedFromConversationId, 'failed', session.toNumber, {
430
- reason: detail,
431
- }).catch((err) => {
432
- log.warn({ conversationId: session.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
488
+ addPointerMessage(
489
+ session.initiatedFromConversationId,
490
+ "failed",
491
+ session.toNumber,
492
+ {
493
+ reason: detail,
494
+ },
495
+ ).catch((err) => {
496
+ log.warn(
497
+ { conversationId: session.initiatedFromConversationId, err },
498
+ "Skipping pointer write — origin conversation may no longer exist",
499
+ );
433
500
  });
434
501
  }
435
502
  }
@@ -441,14 +508,31 @@ export class RelayConnection {
441
508
  // guardian-approval-interception minting path sets callSessionId: null
442
509
  // but always sets conversationId.
443
510
  try {
444
- revokeScopedApprovalGrantsForContext({ callSessionId: this.callSessionId });
445
- revokeScopedApprovalGrantsForContext({ conversationId: session.conversationId });
511
+ revokeScopedApprovalGrantsForContext({
512
+ callSessionId: this.callSessionId,
513
+ });
514
+ revokeScopedApprovalGrantsForContext({
515
+ conversationId: session.conversationId,
516
+ });
446
517
  } catch (err) {
447
- log.warn({ err, callSessionId: this.callSessionId }, 'Failed to revoke scoped grants on transport close');
518
+ log.warn(
519
+ { err, callSessionId: this.callSessionId },
520
+ "Failed to revoke scoped grants on transport close",
521
+ );
448
522
  }
449
523
 
450
- persistCallCompletionMessage(session.conversationId, this.callSessionId).catch((err) => {
451
- log.error({ err, conversationId: session.conversationId, callSessionId: this.callSessionId }, 'Failed to persist call completion message');
524
+ persistCallCompletionMessage(
525
+ session.conversationId,
526
+ this.callSessionId,
527
+ ).catch((err) => {
528
+ log.error(
529
+ {
530
+ err,
531
+ conversationId: session.conversationId,
532
+ callSessionId: this.callSessionId,
533
+ },
534
+ "Failed to persist call completion message",
535
+ );
452
536
  });
453
537
  fireCallCompletionNotifier(session.conversationId, this.callSessionId);
454
538
  }
@@ -457,8 +541,13 @@ export class RelayConnection {
457
541
 
458
542
  private async handleSetup(msg: RelaySetupMessage): Promise<void> {
459
543
  log.info(
460
- { callSessionId: this.callSessionId, callSid: msg.callSid, from: msg.from, to: msg.to },
461
- 'ConversationRelay setup received',
544
+ {
545
+ callSessionId: this.callSessionId,
546
+ callSid: msg.callSid,
547
+ from: msg.from,
548
+ to: msg.to,
549
+ },
550
+ "ConversationRelay setup received",
462
551
  );
463
552
 
464
553
  // Store the callSid association on the call session
@@ -467,8 +556,12 @@ export class RelayConnection {
467
556
  const updates: Parameters<typeof updateCallSession>[1] = {
468
557
  providerCallSid: msg.callSid,
469
558
  };
470
- if (!isTerminalState(session.status) && session.status !== 'in_progress' && session.status !== 'waiting_on_user') {
471
- updates.status = 'in_progress';
559
+ if (
560
+ !isTerminalState(session.status) &&
561
+ session.status !== "in_progress" &&
562
+ session.status !== "waiting_on_user"
563
+ ) {
564
+ updates.status = "in_progress";
472
565
  if (!session.startedAt) {
473
566
  updates.startedAt = Date.now();
474
567
  }
@@ -481,12 +574,12 @@ export class RelayConnection {
481
574
  const safeCustomParameters = msg.customParameters
482
575
  ? Object.fromEntries(
483
576
  Object.entries(msg.customParameters).filter(
484
- ([key]) => !key.toLowerCase().includes('secret'),
577
+ ([key]) => !key.toLowerCase().includes("secret"),
485
578
  ),
486
579
  )
487
580
  : undefined;
488
581
 
489
- recordCallEvent(this.callSessionId, 'call_connected', {
582
+ recordCallEvent(this.callSessionId, "call_connected", {
490
583
  callSid: msg.callSid,
491
584
  from: msg.from,
492
585
  to: msg.to,
@@ -511,17 +604,25 @@ export class RelayConnection {
511
604
  const otherPartyNumber = isInbound ? msg.from : msg.to;
512
605
  const initialActorTrust = resolveActorTrust({
513
606
  assistantId,
514
- sourceChannel: 'voice',
607
+ sourceChannel: "voice",
515
608
  conversationExternalId: otherPartyNumber,
516
609
  actorExternalId: otherPartyNumber || undefined,
517
610
  });
518
- const initialGuardianContext = toGuardianRuntimeContextFromTrust(initialActorTrust, otherPartyNumber);
611
+ const initialTrustContext = toTrustContext(
612
+ initialActorTrust,
613
+ otherPartyNumber,
614
+ );
519
615
 
520
- const controller = new CallController(this.callSessionId, this, session?.task ?? null, {
521
- broadcast: globalBroadcast,
522
- assistantId,
523
- guardianContext: initialGuardianContext,
524
- });
616
+ const controller = new CallController(
617
+ this.callSessionId,
618
+ this,
619
+ session?.task ?? null,
620
+ {
621
+ broadcast: globalBroadcast,
622
+ assistantId,
623
+ trustContext: initialTrustContext,
624
+ },
625
+ );
525
626
  this.setController(controller);
526
627
 
527
628
  // Detect outbound guardian verification call from persisted call session
@@ -529,21 +630,37 @@ export class RelayConnection {
529
630
  // as secondary signal for backward compatibility and observability.
530
631
  const persistedMode = session?.callMode;
531
632
  const persistedGvSessionId = session?.guardianVerificationSessionId;
532
- const customParamGvSessionId = msg.customParameters?.guardianVerificationSessionId;
533
- const guardianVerificationSessionId = persistedGvSessionId ?? customParamGvSessionId;
534
-
535
- if (persistedMode === 'guardian_verification' && guardianVerificationSessionId) {
536
- this.startOutboundGuardianVerification(assistantId, guardianVerificationSessionId, msg.to);
633
+ const customParamGvSessionId =
634
+ msg.customParameters?.guardianVerificationSessionId;
635
+ const guardianVerificationSessionId =
636
+ persistedGvSessionId ?? customParamGvSessionId;
637
+
638
+ if (
639
+ persistedMode === "guardian_verification" &&
640
+ guardianVerificationSessionId
641
+ ) {
642
+ this.startOutboundGuardianVerification(
643
+ assistantId,
644
+ guardianVerificationSessionId,
645
+ msg.to,
646
+ );
537
647
  return;
538
648
  }
539
649
 
540
650
  // Secondary signal: custom parameter without persisted mode (pre-migration sessions)
541
651
  if (!persistedMode && customParamGvSessionId) {
542
652
  log.warn(
543
- { callSessionId: this.callSessionId, guardianVerificationSessionId: customParamGvSessionId },
544
- 'Guardian verification detected via setup custom parameter (no persisted call_mode) — entering verification path',
653
+ {
654
+ callSessionId: this.callSessionId,
655
+ guardianVerificationSessionId: customParamGvSessionId,
656
+ },
657
+ "Guardian verification detected via setup custom parameter (no persisted call_mode) — entering verification path",
658
+ );
659
+ this.startOutboundGuardianVerification(
660
+ assistantId,
661
+ customParamGvSessionId,
662
+ msg.to,
545
663
  );
546
- this.startOutboundGuardianVerification(assistantId, customParamGvSessionId, msg.to);
547
664
  return;
548
665
  }
549
666
 
@@ -562,7 +679,7 @@ export class RelayConnection {
562
679
  // verification flow.
563
680
  const actorTrust = resolveActorTrust({
564
681
  assistantId,
565
- sourceChannel: 'voice',
682
+ sourceChannel: "voice",
566
683
  conversationExternalId: msg.from,
567
684
  actorExternalId: msg.from || undefined,
568
685
  });
@@ -570,9 +687,9 @@ export class RelayConnection {
570
687
  // Check for a pending voice guardian challenge before the ACL deny
571
688
  // gate. An unknown caller with a pending challenge is expected —
572
689
  // they need to complete verification to establish a binding.
573
- const pendingChallenge = getPendingChallenge(assistantId, 'voice');
690
+ const pendingChallenge = getPendingChallenge(assistantId, "voice");
574
691
 
575
- if (actorTrust.trustClass === 'unknown' && !pendingChallenge) {
692
+ if (actorTrust.trustClass === "unknown" && !pendingChallenge) {
576
693
  // Before entering the name capture flow, check if there is an
577
694
  // active voice invite bound to the caller's phone number. If so,
578
695
  // enter the invite redemption subflow instead.
@@ -583,42 +700,54 @@ export class RelayConnection {
583
700
  expectedExternalUserId: msg.from,
584
701
  });
585
702
  } catch (err) {
586
- log.warn({ err, callSessionId: this.callSessionId }, 'Failed to check voice invites for unknown caller');
703
+ log.warn(
704
+ { err, callSessionId: this.callSessionId },
705
+ "Failed to check voice invites for unknown caller",
706
+ );
587
707
  }
588
708
 
589
709
  // Exclude invites that are past their expiresAt even if the DB
590
710
  // status hasn't been lazily flipped to 'expired' yet.
591
711
  const now = Date.now();
592
- const nonExpiredInvites = voiceInvites.filter(i => !i.expiresAt || i.expiresAt > now);
712
+ const nonExpiredInvites = voiceInvites.filter(
713
+ (i) => !i.expiresAt || i.expiresAt > now,
714
+ );
593
715
 
594
716
  // Blocked members get immediate denial — the guardian already made
595
717
  // an explicit decision to block them. This must be checked before
596
718
  // invite redemption so a blocked caller cannot bypass the block by
597
719
  // redeeming an active invite.
598
- if (actorTrust.memberRecord?.status === 'blocked') {
720
+ if (actorTrust.memberRecord?.channel.status === "blocked") {
599
721
  log.info(
600
- { callSessionId: this.callSessionId, from: msg.from, trustClass: actorTrust.trustClass },
601
- 'Inbound voice ACL: blocked caller denied',
722
+ {
723
+ callSessionId: this.callSessionId,
724
+ from: msg.from,
725
+ trustClass: actorTrust.trustClass,
726
+ },
727
+ "Inbound voice ACL: blocked caller denied",
602
728
  );
603
729
 
604
- recordCallEvent(this.callSessionId, 'inbound_acl_denied', {
730
+ recordCallEvent(this.callSessionId, "inbound_acl_denied", {
605
731
  from: msg.from,
606
732
  trustClass: actorTrust.trustClass,
607
733
  denialReason: actorTrust.denialReason,
608
734
  });
609
735
 
610
- this.sendTextToken('This number is not authorized to use this assistant.', true);
736
+ this.sendTextToken(
737
+ "This number is not authorized to use this assistant.",
738
+ true,
739
+ );
611
740
 
612
- this.connectionState = 'disconnecting';
741
+ this.connectionState = "disconnecting";
613
742
 
614
743
  updateCallSession(this.callSessionId, {
615
- status: 'failed',
744
+ status: "failed",
616
745
  endedAt: Date.now(),
617
- lastError: 'Inbound voice ACL: caller blocked',
746
+ lastError: "Inbound voice ACL: caller blocked",
618
747
  });
619
748
 
620
749
  setTimeout(() => {
621
- this.endSession('Inbound voice ACL denied — blocked');
750
+ this.endSession("Inbound voice ACL denied — blocked");
622
751
  }, getTtsPlaybackDelayMs());
623
752
  return;
624
753
  }
@@ -628,23 +757,36 @@ export class RelayConnection {
628
757
  const matchedInvite = nonExpiredInvites[0];
629
758
  log.info(
630
759
  { callSessionId: this.callSessionId, from: msg.from },
631
- 'Inbound voice ACL: unknown caller has active voice invite — entering redemption flow',
760
+ "Inbound voice ACL: unknown caller has active voice invite — entering redemption flow",
761
+ );
762
+ this.startInviteRedemption(
763
+ assistantId,
764
+ msg.from,
765
+ matchedInvite.friendName,
766
+ matchedInvite.guardianName,
632
767
  );
633
- this.startInviteRedemption(assistantId, msg.from, matchedInvite.friendName, matchedInvite.guardianName);
634
768
  return;
635
769
  }
636
770
 
637
771
  // Unknown/revoked/pending callers enter the name capture + guardian
638
772
  // approval wait flow instead of being hard-rejected.
639
773
  log.info(
640
- { callSessionId: this.callSessionId, from: msg.from, trustClass: actorTrust.trustClass },
641
- 'Inbound voice ACL: unknown caller — entering name capture flow',
774
+ {
775
+ callSessionId: this.callSessionId,
776
+ from: msg.from,
777
+ trustClass: actorTrust.trustClass,
778
+ },
779
+ "Inbound voice ACL: unknown caller — entering name capture flow",
642
780
  );
643
781
 
644
- recordCallEvent(this.callSessionId, 'inbound_acl_name_capture_started', {
645
- from: msg.from,
646
- trustClass: actorTrust.trustClass,
647
- });
782
+ recordCallEvent(
783
+ this.callSessionId,
784
+ "inbound_acl_name_capture_started",
785
+ {
786
+ from: msg.from,
787
+ trustClass: actorTrust.trustClass,
788
+ },
789
+ );
648
790
 
649
791
  this.startNameCapture(assistantId, msg.from);
650
792
  return;
@@ -653,31 +795,39 @@ export class RelayConnection {
653
795
  // Members with policy: 'deny' have status: 'active' so resolveActorTrust
654
796
  // classifies them as trusted_contact, but the guardian has explicitly
655
797
  // denied their access. Block them the same way the text-channel path does.
656
- if (actorTrust.memberRecord?.policy === 'deny') {
798
+ if (actorTrust.memberRecord?.channel.policy === "deny") {
657
799
  log.info(
658
- { callSessionId: this.callSessionId, from: msg.from, memberId: actorTrust.memberRecord.id, trustClass: actorTrust.trustClass },
659
- 'Inbound voice ACL: member policy deny',
800
+ {
801
+ callSessionId: this.callSessionId,
802
+ from: msg.from,
803
+ channelId: actorTrust.memberRecord.channel.id,
804
+ trustClass: actorTrust.trustClass,
805
+ },
806
+ "Inbound voice ACL: member policy deny",
660
807
  );
661
808
 
662
- recordCallEvent(this.callSessionId, 'inbound_acl_denied', {
809
+ recordCallEvent(this.callSessionId, "inbound_acl_denied", {
663
810
  from: msg.from,
664
811
  trustClass: actorTrust.trustClass,
665
- memberId: actorTrust.memberRecord.id,
666
- memberPolicy: actorTrust.memberRecord.policy,
812
+ channelId: actorTrust.memberRecord.channel.id,
813
+ memberPolicy: actorTrust.memberRecord.channel.policy,
667
814
  });
668
815
 
669
- this.sendTextToken('This number is not authorized to use this assistant.', true);
816
+ this.sendTextToken(
817
+ "This number is not authorized to use this assistant.",
818
+ true,
819
+ );
670
820
 
671
- this.connectionState = 'disconnecting';
821
+ this.connectionState = "disconnecting";
672
822
 
673
823
  updateCallSession(this.callSessionId, {
674
- status: 'failed',
824
+ status: "failed",
675
825
  endedAt: Date.now(),
676
- lastError: 'Inbound voice ACL: member policy deny',
826
+ lastError: "Inbound voice ACL: member policy deny",
677
827
  });
678
828
 
679
829
  setTimeout(() => {
680
- this.endSession('Inbound voice ACL: member policy deny');
830
+ this.endSession("Inbound voice ACL: member policy deny");
681
831
  }, getTtsPlaybackDelayMs());
682
832
  return;
683
833
  }
@@ -685,31 +835,40 @@ export class RelayConnection {
685
835
  // Members with policy: 'escalate' require guardian approval, but a live
686
836
  // voice call cannot be paused for async approval. Fail-closed by denying
687
837
  // the call with an appropriate message — mirrors the deny block above.
688
- if (actorTrust.memberRecord?.policy === 'escalate') {
838
+ if (actorTrust.memberRecord?.channel.policy === "escalate") {
689
839
  log.info(
690
- { callSessionId: this.callSessionId, from: msg.from, memberId: actorTrust.memberRecord.id, trustClass: actorTrust.trustClass },
691
- 'Inbound voice ACL: member policy escalate — cannot hold live call for guardian approval',
840
+ {
841
+ callSessionId: this.callSessionId,
842
+ from: msg.from,
843
+ channelId: actorTrust.memberRecord.channel.id,
844
+ trustClass: actorTrust.trustClass,
845
+ },
846
+ "Inbound voice ACL: member policy escalate — cannot hold live call for guardian approval",
692
847
  );
693
848
 
694
- recordCallEvent(this.callSessionId, 'inbound_acl_denied', {
849
+ recordCallEvent(this.callSessionId, "inbound_acl_denied", {
695
850
  from: msg.from,
696
851
  trustClass: actorTrust.trustClass,
697
- memberId: actorTrust.memberRecord.id,
698
- memberPolicy: actorTrust.memberRecord.policy,
852
+ channelId: actorTrust.memberRecord.channel.id,
853
+ memberPolicy: actorTrust.memberRecord.channel.policy,
699
854
  });
700
855
 
701
- this.sendTextToken('This number requires guardian approval for calls. Please have the account guardian update your permissions.', true);
856
+ this.sendTextToken(
857
+ "This number requires guardian approval for calls. Please have the account guardian update your permissions.",
858
+ true,
859
+ );
702
860
 
703
- this.connectionState = 'disconnecting';
861
+ this.connectionState = "disconnecting";
704
862
 
705
863
  updateCallSession(this.callSessionId, {
706
- status: 'failed',
864
+ status: "failed",
707
865
  endedAt: Date.now(),
708
- lastError: 'Inbound voice ACL: member policy escalate — voice calls cannot await guardian approval',
866
+ lastError:
867
+ "Inbound voice ACL: member policy escalate — voice calls cannot await guardian approval",
709
868
  });
710
869
 
711
870
  setTimeout(() => {
712
- this.endSession('Inbound voice ACL: member policy escalate');
871
+ this.endSession("Inbound voice ACL: member policy escalate");
713
872
  }, getTtsPlaybackDelayMs());
714
873
  return;
715
874
  }
@@ -717,9 +876,9 @@ export class RelayConnection {
717
876
  // Guardian and trusted-contact callers proceed normally.
718
877
  // Update the controller's guardian context with the trust-resolved
719
878
  // context so downstream policy gates have accurate actor metadata.
720
- if (this.controller && actorTrust.trustClass !== 'unknown') {
721
- const resolvedGuardianContext = toGuardianRuntimeContextFromTrust(actorTrust, msg.from);
722
- this.controller.setGuardianContext(resolvedGuardianContext);
879
+ if (this.controller && actorTrust.trustClass !== "unknown") {
880
+ const resolvedTrustContext = toTrustContext(actorTrust, msg.from);
881
+ this.controller.setTrustContext(resolvedTrustContext);
723
882
  }
724
883
 
725
884
  if (pendingChallenge) {
@@ -742,22 +901,27 @@ export class RelayConnection {
742
901
  this.verificationMaxAttempts = verificationConfig.maxAttempts;
743
902
  this.verificationCodeLength = verificationConfig.codeLength;
744
903
  this.verificationAttempts = 0;
745
- this.dtmfBuffer = '';
904
+ this.dtmfBuffer = "";
746
905
 
747
906
  // Generate a random numeric code
748
907
  const maxValue = Math.pow(10, this.verificationCodeLength);
749
- const code = randomInt(0, maxValue).toString().padStart(this.verificationCodeLength, '0');
908
+ const code = randomInt(0, maxValue)
909
+ .toString()
910
+ .padStart(this.verificationCodeLength, "0");
750
911
  this.verificationCode = code;
751
- this.connectionState = 'verification_pending';
912
+ this.connectionState = "verification_pending";
752
913
 
753
- recordCallEvent(this.callSessionId, 'callee_verification_started', {
914
+ recordCallEvent(this.callSessionId, "callee_verification_started", {
754
915
  codeLength: this.verificationCodeLength,
755
916
  maxAttempts: this.verificationMaxAttempts,
756
917
  });
757
918
 
758
919
  // Send a TTS prompt with the code spoken digit by digit
759
- const spokenCode = code.split('').join('. ');
760
- this.sendTextToken(`Please enter the verification code: ${spokenCode}.`, true);
920
+ const spokenCode = code.split("").join(". ");
921
+ this.sendTextToken(
922
+ `Please enter the verification code: ${spokenCode}.`,
923
+ true,
924
+ );
761
925
 
762
926
  // Post the verification code to the initiating conversation so the
763
927
  // guardian (user) can share it with the callee.
@@ -765,15 +929,23 @@ export class RelayConnection {
765
929
  const codeMsg = `\u{1F510} Verification code for call to ${session.toNumber}: ${code}`;
766
930
  await conversationStore.addMessage(
767
931
  session.initiatedFromConversationId,
768
- 'assistant',
769
- JSON.stringify([{ type: 'text', text: codeMsg }]),
770
- { userMessageChannel: 'voice', assistantMessageChannel: 'voice', userMessageInterface: 'voice', assistantMessageInterface: 'voice' },
932
+ "assistant",
933
+ JSON.stringify([{ type: "text", text: codeMsg }]),
934
+ {
935
+ userMessageChannel: "voice",
936
+ assistantMessageChannel: "voice",
937
+ userMessageInterface: "voice",
938
+ assistantMessageInterface: "voice",
939
+ },
771
940
  );
772
941
  }
773
942
 
774
943
  log.info(
775
- { callSessionId: this.callSessionId, codeLength: this.verificationCodeLength },
776
- 'Callee verification started',
944
+ {
945
+ callSessionId: this.callSessionId,
946
+ codeLength: this.verificationCodeLength,
947
+ },
948
+ "Callee verification started",
777
949
  );
778
950
  }
779
951
 
@@ -781,13 +953,91 @@ export class RelayConnection {
781
953
  * Start normal call flow — fire the controller greeting unless a
782
954
  * static welcome greeting is configured.
783
955
  */
784
- private startNormalCallFlow(controller: CallController, isInbound: boolean): void {
956
+ private startNormalCallFlow(
957
+ controller: CallController,
958
+ isInbound: boolean,
959
+ ): void {
785
960
  const hasStaticGreeting = !!process.env.CALL_WELCOME_GREETING?.trim();
786
961
  if (!hasStaticGreeting) {
787
- controller.startInitialGreeting().catch((err) =>
788
- log.error({ err, callSessionId: this.callSessionId }, `Failed to start initial ${isInbound ? 'inbound' : 'outbound'} greeting`),
962
+ controller
963
+ .startInitialGreeting()
964
+ .catch((err) =>
965
+ log.error(
966
+ { err, callSessionId: this.callSessionId },
967
+ `Failed to start initial ${isInbound ? "inbound" : "outbound"} greeting`,
968
+ ),
969
+ );
970
+ }
971
+ }
972
+
973
+ /**
974
+ * Shared post-activation handoff for all trusted-contact success paths
975
+ * (access-request approval, invite redemption, verification code).
976
+ * Activates the caller, updates guardian context, delivers deterministic
977
+ * transition copy, and marks the next utterance as opening-ack so the
978
+ * LLM continues naturally.
979
+ */
980
+ private continueCallAfterTrustedContactActivation(params: {
981
+ assistantId: string;
982
+ fromNumber: string;
983
+ callerName?: string;
984
+ skipMemberActivation?: boolean;
985
+ }): void {
986
+ const { assistantId, fromNumber, callerName } = params;
987
+
988
+ if (!params.skipMemberActivation) {
989
+ try {
990
+ upsertMember({
991
+ assistantId,
992
+ sourceChannel: "voice",
993
+ externalUserId: fromNumber,
994
+ externalChatId: fromNumber,
995
+ displayName: callerName,
996
+ status: "active",
997
+ policy: "allow",
998
+ });
999
+ } catch (err) {
1000
+ log.error(
1001
+ { err, callSessionId: this.callSessionId },
1002
+ "Failed to activate voice caller as trusted contact",
1003
+ );
1004
+ }
1005
+ }
1006
+
1007
+ const updatedTrust = resolveActorTrust({
1008
+ assistantId,
1009
+ sourceChannel: "voice",
1010
+ conversationExternalId: fromNumber,
1011
+ actorExternalId: fromNumber,
1012
+ });
1013
+
1014
+ if (this.controller) {
1015
+ this.controller.setTrustContext(toTrustContext(updatedTrust, fromNumber));
1016
+ }
1017
+
1018
+ this.connectionState = "connected";
1019
+ updateCallSession(this.callSessionId, { status: "in_progress" });
1020
+
1021
+ const guardianLabel = this.resolveGuardianLabel();
1022
+ const handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
1023
+ this.sendTextToken(handoffText, true);
1024
+
1025
+ recordCallEvent(this.callSessionId, "assistant_spoke", {
1026
+ text: handoffText,
1027
+ });
1028
+ const session = getCallSession(this.callSessionId);
1029
+ if (session) {
1030
+ fireCallTranscriptNotifier(
1031
+ session.conversationId,
1032
+ this.callSessionId,
1033
+ "assistant",
1034
+ handoffText,
789
1035
  );
790
1036
  }
1037
+
1038
+ if (this.controller) {
1039
+ this.controller.markNextCallerTurnAsOpeningAck();
1040
+ }
791
1041
  }
792
1042
 
793
1043
  /**
@@ -795,29 +1045,32 @@ export class RelayConnection {
795
1045
  * voice guardian challenge. Prompts the caller to enter their six-digit
796
1046
  * verification code via DTMF or by speaking it.
797
1047
  */
798
- private startInboundGuardianVerification(assistantId: string, fromNumber: string): void {
1048
+ private startInboundGuardianVerification(
1049
+ assistantId: string,
1050
+ fromNumber: string,
1051
+ ): void {
799
1052
  this.guardianVerificationActive = true;
800
1053
  this.guardianChallengeAssistantId = assistantId;
801
1054
  this.guardianVerificationFromNumber = fromNumber;
802
- this.connectionState = 'verification_pending';
1055
+ this.connectionState = "verification_pending";
803
1056
  this.verificationAttempts = 0;
804
1057
  this.verificationMaxAttempts = 3;
805
1058
  this.verificationCodeLength = 6;
806
- this.dtmfBuffer = '';
1059
+ this.dtmfBuffer = "";
807
1060
 
808
- recordCallEvent(this.callSessionId, 'guardian_voice_verification_started', {
1061
+ recordCallEvent(this.callSessionId, "guardian_voice_verification_started", {
809
1062
  assistantId,
810
1063
  maxAttempts: this.verificationMaxAttempts,
811
1064
  });
812
1065
 
813
1066
  this.sendTextToken(
814
- 'Welcome. Please enter your six-digit verification code using your keypad, or speak the digits now.',
1067
+ "Welcome. Please enter your six-digit verification code using your keypad, or speak the digits now.",
815
1068
  true,
816
1069
  );
817
1070
 
818
1071
  log.info(
819
1072
  { callSessionId: this.callSessionId, assistantId },
820
- 'Inbound guardian voice verification started',
1073
+ "Inbound guardian voice verification started",
821
1074
  );
822
1075
  }
823
1076
 
@@ -836,17 +1089,21 @@ export class RelayConnection {
836
1089
  this.guardianChallengeAssistantId = assistantId;
837
1090
  // For outbound guardian calls, the "to" number is the guardian's phone
838
1091
  this.guardianVerificationFromNumber = toNumber;
839
- this.connectionState = 'verification_pending';
1092
+ this.connectionState = "verification_pending";
840
1093
  this.verificationAttempts = 0;
841
1094
  this.verificationMaxAttempts = 3;
842
1095
  this.verificationCodeLength = 6;
843
- this.dtmfBuffer = '';
1096
+ this.dtmfBuffer = "";
844
1097
 
845
- recordCallEvent(this.callSessionId, 'outbound_guardian_voice_verification_started', {
846
- assistantId,
847
- guardianVerificationSessionId,
848
- maxAttempts: this.verificationMaxAttempts,
849
- });
1098
+ recordCallEvent(
1099
+ this.callSessionId,
1100
+ "outbound_guardian_voice_verification_started",
1101
+ {
1102
+ assistantId,
1103
+ guardianVerificationSessionId,
1104
+ maxAttempts: this.verificationMaxAttempts,
1105
+ },
1106
+ );
850
1107
 
851
1108
  const introText = composeVerificationVoice(
852
1109
  GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_CALL_INTRO,
@@ -855,8 +1112,12 @@ export class RelayConnection {
855
1112
  this.sendTextToken(introText, true);
856
1113
 
857
1114
  log.info(
858
- { callSessionId: this.callSessionId, assistantId, guardianVerificationSessionId },
859
- 'Outbound guardian voice verification started',
1115
+ {
1116
+ callSessionId: this.callSessionId,
1117
+ assistantId,
1118
+ guardianVerificationSessionId,
1119
+ },
1120
+ "Outbound guardian voice verification started",
860
1121
  );
861
1122
  }
862
1123
 
@@ -866,16 +1127,24 @@ export class RelayConnection {
866
1127
  */
867
1128
  private static parseDigitsFromSpeech(transcript: string): string {
868
1129
  const wordToDigit: Record<string, string> = {
869
- zero: '0', oh: '0', o: '0',
870
- one: '1', won: '1',
871
- two: '2', too: '2', to: '2',
872
- three: '3',
873
- four: '4', for: '4', fore: '4',
874
- five: '5',
875
- six: '6',
876
- seven: '7',
877
- eight: '8', ate: '8',
878
- nine: '9',
1130
+ zero: "0",
1131
+ oh: "0",
1132
+ o: "0",
1133
+ one: "1",
1134
+ won: "1",
1135
+ two: "2",
1136
+ too: "2",
1137
+ to: "2",
1138
+ three: "3",
1139
+ four: "4",
1140
+ for: "4",
1141
+ fore: "4",
1142
+ five: "5",
1143
+ six: "6",
1144
+ seven: "7",
1145
+ eight: "8",
1146
+ ate: "8",
1147
+ nine: "9",
879
1148
  };
880
1149
 
881
1150
  const digits: string[] = [];
@@ -890,11 +1159,11 @@ export class RelayConnection {
890
1159
  digits.push(wordToDigit[token]);
891
1160
  } else if (/^\d+$/.test(token)) {
892
1161
  // Multi-digit number like "123456" — split into individual digits
893
- digits.push(...token.split(''));
1162
+ digits.push(...token.split(""));
894
1163
  }
895
1164
  }
896
1165
 
897
- return digits.join('');
1166
+ return digits.join("");
898
1167
  }
899
1168
 
900
1169
  /**
@@ -906,7 +1175,10 @@ export class RelayConnection {
906
1175
  * On failure, enforces max attempts and terminates the call if exhausted.
907
1176
  */
908
1177
  private attemptGuardianCodeVerification(enteredCode: string): void {
909
- if (!this.guardianChallengeAssistantId || !this.guardianVerificationFromNumber) {
1178
+ if (
1179
+ !this.guardianChallengeAssistantId ||
1180
+ !this.guardianVerificationFromNumber
1181
+ ) {
910
1182
  return;
911
1183
  }
912
1184
 
@@ -915,37 +1187,77 @@ export class RelayConnection {
915
1187
 
916
1188
  const result = validateAndConsumeChallenge(
917
1189
  this.guardianChallengeAssistantId,
918
- 'voice',
1190
+ "voice",
919
1191
  enteredCode,
920
1192
  this.guardianVerificationFromNumber,
921
1193
  this.guardianVerificationFromNumber,
922
1194
  );
923
1195
 
924
1196
  if (result.success) {
925
- // Guardian binding was created by validateAndConsumeChallenge
926
- this.connectionState = 'connected';
1197
+ this.connectionState = "connected";
927
1198
  this.guardianVerificationActive = false;
928
1199
  this.verificationAttempts = 0;
929
- this.dtmfBuffer = '';
1200
+ this.dtmfBuffer = "";
930
1201
 
931
1202
  const eventName = isOutbound
932
- ? 'outbound_guardian_voice_verification_succeeded'
933
- : 'guardian_voice_verification_succeeded';
1203
+ ? "outbound_guardian_voice_verification_succeeded"
1204
+ : "guardian_voice_verification_succeeded";
934
1205
 
935
1206
  recordCallEvent(this.callSessionId, eventName, {
936
- bindingId: 'bindingId' in result ? result.bindingId : undefined,
1207
+ verificationType: result.verificationType,
937
1208
  });
938
1209
  log.info(
939
1210
  { callSessionId: this.callSessionId, isOutbound },
940
- 'Guardian voice verification succeeded',
1211
+ "Guardian voice verification succeeded",
941
1212
  );
942
1213
 
1214
+ // Create the guardian binding now that verification succeeded.
1215
+ if (result.verificationType === "guardian") {
1216
+ const existingBinding = getGuardianBinding(
1217
+ this.guardianChallengeAssistantId,
1218
+ "voice",
1219
+ );
1220
+ if (
1221
+ existingBinding &&
1222
+ existingBinding.guardianExternalUserId !==
1223
+ this.guardianVerificationFromNumber
1224
+ ) {
1225
+ log.warn(
1226
+ {
1227
+ callSessionId: this.callSessionId,
1228
+ existingGuardian: existingBinding.guardianExternalUserId,
1229
+ },
1230
+ "Guardian binding conflict: another user already holds the voice binding",
1231
+ );
1232
+ } else {
1233
+ revokeGuardianBinding(this.guardianChallengeAssistantId, "voice");
1234
+
1235
+ // Unify all channel bindings onto the canonical (vellum) principal
1236
+ const vellumBinding = getGuardianBinding(
1237
+ this.guardianChallengeAssistantId,
1238
+ "vellum",
1239
+ );
1240
+ const canonicalPrincipal =
1241
+ vellumBinding?.guardianPrincipalId ??
1242
+ this.guardianVerificationFromNumber;
1243
+
1244
+ createGuardianBinding({
1245
+ assistantId: this.guardianChallengeAssistantId,
1246
+ channel: "voice",
1247
+ guardianExternalUserId: this.guardianVerificationFromNumber,
1248
+ guardianDeliveryChatId: this.guardianVerificationFromNumber,
1249
+ guardianPrincipalId: canonicalPrincipal,
1250
+ verifiedVia: "challenge",
1251
+ });
1252
+ }
1253
+ }
1254
+
943
1255
  if (isOutbound) {
944
1256
  // Outbound guardian verification: play success and hang up.
945
1257
  // There is no normal conversation to transition to.
946
1258
  // Set disconnecting to ignore any further DTMF/speech input
947
1259
  // during the brief delay before the session ends.
948
- this.connectionState = 'disconnecting';
1260
+ this.connectionState = "disconnecting";
949
1261
 
950
1262
  const successText = composeVerificationVoice(
951
1263
  GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_SUCCESS,
@@ -954,7 +1266,7 @@ export class RelayConnection {
954
1266
  this.sendTextToken(successText, true);
955
1267
 
956
1268
  updateCallSession(this.callSessionId, {
957
- status: 'completed',
1269
+ status: "completed",
958
1270
  endedAt: Date.now(),
959
1271
  });
960
1272
 
@@ -964,28 +1276,83 @@ export class RelayConnection {
964
1276
  if (successSession?.initiatedFromConversationId) {
965
1277
  addPointerMessage(
966
1278
  successSession.initiatedFromConversationId,
967
- 'guardian_verification_succeeded',
1279
+ "guardian_verification_succeeded",
968
1280
  successSession.toNumber,
969
- { channel: 'voice' },
1281
+ { channel: "voice" },
970
1282
  ).catch((err) => {
971
- log.warn({ conversationId: successSession.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
1283
+ log.warn(
1284
+ {
1285
+ conversationId: successSession.initiatedFromConversationId,
1286
+ err,
1287
+ },
1288
+ "Skipping pointer write — origin conversation may no longer exist",
1289
+ );
972
1290
  });
973
1291
  }
974
1292
 
975
1293
  setTimeout(() => {
976
- this.endSession('Guardian verification succeeded');
1294
+ this.endSession("Verified guardian challenge passed");
977
1295
  }, getTtsPlaybackDelayMs());
1296
+ } else if (result.verificationType === "trusted_contact") {
1297
+ // Inbound trusted-contact verification: activate and continue
1298
+ // the live call with the shared handoff primitive.
1299
+ this.continueCallAfterTrustedContactActivation({
1300
+ assistantId: this.guardianChallengeAssistantId,
1301
+ fromNumber: this.guardianVerificationFromNumber,
1302
+ });
978
1303
  } else {
979
- // Inbound: proceed to normal call flow
1304
+ // Inbound guardian verification: create/update binding, then proceed
1305
+ // to normal call flow. Mirrors the binding creation logic in
1306
+ // verification-intercept.ts for the inbound channel path.
1307
+ const guardianAssistantId = this.guardianChallengeAssistantId;
1308
+ const callerNumber = this.guardianVerificationFromNumber;
1309
+
1310
+ const existingBinding = getGuardianBinding(
1311
+ guardianAssistantId,
1312
+ "voice",
1313
+ );
1314
+ if (
1315
+ existingBinding &&
1316
+ existingBinding.guardianExternalUserId !== callerNumber
1317
+ ) {
1318
+ log.warn(
1319
+ {
1320
+ sourceChannel: "voice",
1321
+ existingGuardian: existingBinding.guardianExternalUserId,
1322
+ },
1323
+ "Guardian binding conflict: another user already holds the voice channel binding",
1324
+ );
1325
+ } else {
1326
+ revokeGuardianBinding(guardianAssistantId, "voice");
1327
+
1328
+ // Resolve canonical principal from the vellum channel binding
1329
+ // so all channel bindings share a single principal identity.
1330
+ const vellumBinding = getGuardianBinding(
1331
+ guardianAssistantId,
1332
+ "vellum",
1333
+ );
1334
+ const canonicalPrincipal =
1335
+ vellumBinding?.guardianPrincipalId ?? callerNumber;
1336
+
1337
+ createGuardianBinding({
1338
+ assistantId: guardianAssistantId,
1339
+ channel: "voice",
1340
+ guardianExternalUserId: callerNumber,
1341
+ guardianDeliveryChatId: callerNumber,
1342
+ guardianPrincipalId: canonicalPrincipal,
1343
+ verifiedVia: "challenge",
1344
+ });
1345
+ }
1346
+
980
1347
  if (this.controller) {
981
1348
  const verifiedActorTrust = resolveActorTrust({
982
- assistantId: this.guardianChallengeAssistantId,
983
- sourceChannel: 'voice',
984
- conversationExternalId: this.guardianVerificationFromNumber,
985
- actorExternalId: this.guardianVerificationFromNumber,
1349
+ assistantId: guardianAssistantId,
1350
+ sourceChannel: "voice",
1351
+ conversationExternalId: callerNumber,
1352
+ actorExternalId: callerNumber,
986
1353
  });
987
- this.controller.setGuardianContext(
988
- toGuardianRuntimeContextFromTrust(verifiedActorTrust, this.guardianVerificationFromNumber),
1354
+ this.controller.setTrustContext(
1355
+ toTrustContext(verifiedActorTrust, callerNumber),
989
1356
  );
990
1357
  this.startNormalCallFlow(this.controller, true);
991
1358
  }
@@ -999,61 +1366,99 @@ export class RelayConnection {
999
1366
  this.guardianVerificationActive = false;
1000
1367
 
1001
1368
  const failEventName = isOutbound
1002
- ? 'outbound_guardian_voice_verification_failed'
1003
- : 'guardian_voice_verification_failed';
1369
+ ? "outbound_guardian_voice_verification_failed"
1370
+ : "guardian_voice_verification_failed";
1004
1371
 
1005
1372
  recordCallEvent(this.callSessionId, failEventName, {
1006
1373
  attempts: this.verificationAttempts,
1007
1374
  });
1008
1375
  log.warn(
1009
- { callSessionId: this.callSessionId, attempts: this.verificationAttempts, isOutbound },
1010
- 'Guardian voice verification failed — max attempts reached',
1376
+ {
1377
+ callSessionId: this.callSessionId,
1378
+ attempts: this.verificationAttempts,
1379
+ isOutbound,
1380
+ },
1381
+ "Guardian voice verification failed — max attempts reached",
1011
1382
  );
1012
1383
 
1013
1384
  const failureText = isOutbound
1014
- ? composeVerificationVoice(GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_FAILURE, { codeDigits })
1015
- : 'Verification failed. Goodbye.';
1385
+ ? composeVerificationVoice(
1386
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_FAILURE,
1387
+ { codeDigits },
1388
+ )
1389
+ : "Verification failed. Goodbye.";
1016
1390
  this.sendTextToken(failureText, true);
1017
1391
 
1018
1392
  updateCallSession(this.callSessionId, {
1019
- status: 'failed',
1393
+ status: "failed",
1020
1394
  endedAt: Date.now(),
1021
- lastError: 'Guardian voice verification failed — max attempts exceeded',
1395
+ lastError:
1396
+ "Guardian voice verification failed — max attempts exceeded",
1022
1397
  });
1023
1398
 
1024
1399
  const failSession = getCallSession(this.callSessionId);
1025
1400
  if (failSession) {
1026
1401
  expirePendingQuestions(this.callSessionId);
1027
- persistCallCompletionMessage(failSession.conversationId, this.callSessionId).catch((err) => {
1028
- log.error({ err, conversationId: failSession.conversationId, callSessionId: this.callSessionId }, 'Failed to persist call completion message');
1402
+ persistCallCompletionMessage(
1403
+ failSession.conversationId,
1404
+ this.callSessionId,
1405
+ ).catch((err) => {
1406
+ log.error(
1407
+ {
1408
+ err,
1409
+ conversationId: failSession.conversationId,
1410
+ callSessionId: this.callSessionId,
1411
+ },
1412
+ "Failed to persist call completion message",
1413
+ );
1029
1414
  });
1030
- fireCallCompletionNotifier(failSession.conversationId, this.callSessionId);
1415
+ fireCallCompletionNotifier(
1416
+ failSession.conversationId,
1417
+ this.callSessionId,
1418
+ );
1031
1419
 
1032
1420
  // Emit a pointer message to the origin conversation so the
1033
1421
  // requesting chat sees a deterministic failure notice.
1034
1422
  if (isOutbound && failSession.initiatedFromConversationId) {
1035
1423
  addPointerMessage(
1036
1424
  failSession.initiatedFromConversationId,
1037
- 'guardian_verification_failed',
1425
+ "guardian_verification_failed",
1038
1426
  failSession.toNumber,
1039
- { channel: 'voice', reason: 'Max verification attempts exceeded' },
1427
+ {
1428
+ channel: "voice",
1429
+ reason: "Max verification attempts exceeded",
1430
+ },
1040
1431
  ).catch((err) => {
1041
- log.warn({ conversationId: failSession.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
1432
+ log.warn(
1433
+ {
1434
+ conversationId: failSession.initiatedFromConversationId,
1435
+ err,
1436
+ },
1437
+ "Skipping pointer write — origin conversation may no longer exist",
1438
+ );
1042
1439
  });
1043
1440
  }
1044
1441
  }
1045
1442
 
1046
1443
  setTimeout(() => {
1047
- this.endSession('Guardian verification failed');
1444
+ this.endSession("Verification failed — challenge rejected");
1048
1445
  }, getTtsPlaybackDelayMs());
1049
1446
  } else {
1050
1447
  const retryText = isOutbound
1051
- ? composeVerificationVoice(GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_RETRY, { codeDigits })
1052
- : 'That code was incorrect. Please try again.';
1448
+ ? composeVerificationVoice(
1449
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_RETRY,
1450
+ { codeDigits },
1451
+ )
1452
+ : "That code was incorrect. Please try again.";
1053
1453
 
1054
1454
  log.info(
1055
- { callSessionId: this.callSessionId, attempt: this.verificationAttempts, maxAttempts: this.verificationMaxAttempts, isOutbound },
1056
- 'Guardian voice verification attempt failed — retrying',
1455
+ {
1456
+ callSessionId: this.callSessionId,
1457
+ attempt: this.verificationAttempts,
1458
+ maxAttempts: this.verificationMaxAttempts,
1459
+ isOutbound,
1460
+ },
1461
+ "Guardian voice verification attempt failed — retrying",
1057
1462
  );
1058
1463
  this.sendTextToken(retryText, true);
1059
1464
  }
@@ -1065,26 +1470,31 @@ export class RelayConnection {
1065
1470
  * who has an active voice invite. Prompts the caller to enter their
1066
1471
  * invite code via DTMF or speech.
1067
1472
  */
1068
- private startInviteRedemption(assistantId: string, fromNumber: string, friendName: string | null, guardianName: string | null): void {
1473
+ private startInviteRedemption(
1474
+ assistantId: string,
1475
+ fromNumber: string,
1476
+ friendName: string | null,
1477
+ guardianName: string | null,
1478
+ ): void {
1069
1479
  this.inviteRedemptionActive = true;
1070
1480
  this.inviteRedemptionAssistantId = assistantId;
1071
1481
  this.inviteRedemptionFromNumber = fromNumber;
1072
1482
  this.inviteRedemptionFriendName = friendName;
1073
1483
  this.inviteRedemptionGuardianName = guardianName;
1074
- this.connectionState = 'verification_pending';
1484
+ this.connectionState = "verification_pending";
1075
1485
  this.verificationAttempts = 0;
1076
1486
  this.verificationMaxAttempts = 1;
1077
1487
  this.inviteRedemptionCodeLength = 6;
1078
- this.dtmfBuffer = '';
1488
+ this.dtmfBuffer = "";
1079
1489
 
1080
- recordCallEvent(this.callSessionId, 'invite_redemption_started', {
1490
+ recordCallEvent(this.callSessionId, "invite_redemption_started", {
1081
1491
  assistantId,
1082
1492
  codeLength: 6,
1083
1493
  maxAttempts: this.verificationMaxAttempts,
1084
1494
  });
1085
1495
 
1086
- const displayFriend = friendName ?? 'there';
1087
- const displayGuardian = guardianName ?? 'your contact';
1496
+ const displayFriend = friendName ?? "there";
1497
+ const displayGuardian = guardianName ?? "your contact";
1088
1498
  this.sendTextToken(
1089
1499
  `Welcome ${displayFriend}. Please enter the 6-digit code that ${displayGuardian} provided you to verify your identity.`,
1090
1500
  true,
@@ -1092,7 +1502,7 @@ export class RelayConnection {
1092
1502
 
1093
1503
  log.info(
1094
1504
  { callSessionId: this.callSessionId, assistantId },
1095
- 'Inbound voice invite redemption started',
1505
+ "Inbound voice invite redemption started",
1096
1506
  );
1097
1507
  }
1098
1508
 
@@ -1104,7 +1514,7 @@ export class RelayConnection {
1104
1514
  private startNameCapture(assistantId: string, fromNumber: string): void {
1105
1515
  this.accessRequestAssistantId = assistantId;
1106
1516
  this.accessRequestFromNumber = fromNumber;
1107
- this.connectionState = 'awaiting_name';
1517
+ this.connectionState = "awaiting_name";
1108
1518
 
1109
1519
  const guardianLabel = this.resolveGuardianLabel();
1110
1520
  const assistantName = this.resolveAssistantLabel();
@@ -1120,13 +1530,17 @@ export class RelayConnection {
1120
1530
  // to avoid wasting resources on callers who never respond.
1121
1531
  const NAME_CAPTURE_TIMEOUT_MS = 30_000;
1122
1532
  this.nameCaptureTimeoutTimer = setTimeout(() => {
1123
- if (this.connectionState !== 'awaiting_name') return;
1533
+ if (this.connectionState !== "awaiting_name") return;
1124
1534
  this.handleNameCaptureTimeout();
1125
1535
  }, NAME_CAPTURE_TIMEOUT_MS);
1126
1536
 
1127
1537
  log.info(
1128
- { callSessionId: this.callSessionId, assistantId, timeoutMs: NAME_CAPTURE_TIMEOUT_MS },
1129
- 'Name capture started for unknown inbound caller',
1538
+ {
1539
+ callSessionId: this.callSessionId,
1540
+ assistantId,
1541
+ timeoutMs: NAME_CAPTURE_TIMEOUT_MS,
1542
+ },
1543
+ "Name capture started for unknown inbound caller",
1130
1544
  );
1131
1545
  }
1132
1546
 
@@ -1148,7 +1562,7 @@ export class RelayConnection {
1148
1562
 
1149
1563
  this.accessRequestCallerName = callerName;
1150
1564
 
1151
- recordCallEvent(this.callSessionId, 'inbound_acl_name_captured', {
1565
+ recordCallEvent(this.callSessionId, "inbound_acl_name_captured", {
1152
1566
  from: this.accessRequestFromNumber,
1153
1567
  callerName,
1154
1568
  });
@@ -1158,7 +1572,7 @@ export class RelayConnection {
1158
1572
  try {
1159
1573
  const accessResult = notifyGuardianOfAccessRequest({
1160
1574
  canonicalAssistantId: this.accessRequestAssistantId,
1161
- sourceChannel: 'voice',
1575
+ sourceChannel: "voice",
1162
1576
  conversationExternalId: this.accessRequestFromNumber,
1163
1577
  actorExternalId: this.accessRequestFromNumber,
1164
1578
  actorDisplayName: callerName,
@@ -1167,17 +1581,24 @@ export class RelayConnection {
1167
1581
  if (accessResult.notified) {
1168
1582
  this.accessRequestId = accessResult.requestId;
1169
1583
  log.info(
1170
- { callSessionId: this.callSessionId, requestId: accessResult.requestId, callerName },
1171
- 'Guardian notified of voice access request with caller name',
1584
+ {
1585
+ callSessionId: this.callSessionId,
1586
+ requestId: accessResult.requestId,
1587
+ callerName,
1588
+ },
1589
+ "Guardian notified of voice access request with caller name",
1172
1590
  );
1173
1591
  } else {
1174
1592
  log.warn(
1175
1593
  { callSessionId: this.callSessionId },
1176
- 'Failed to notify guardian of voice access request — no sender ID',
1594
+ "Failed to notify guardian of voice access request — no sender ID",
1177
1595
  );
1178
1596
  }
1179
1597
  } catch (err) {
1180
- log.error({ err, callSessionId: this.callSessionId }, 'Failed to create access request for voice caller');
1598
+ log.error(
1599
+ { err, callSessionId: this.callSessionId },
1600
+ "Failed to create access request for voice caller",
1601
+ );
1181
1602
  }
1182
1603
 
1183
1604
  // If the access request was not successfully created (notifyGuardianOfAccessRequest
@@ -1186,7 +1607,7 @@ export class RelayConnection {
1186
1607
  if (!this.accessRequestId) {
1187
1608
  log.warn(
1188
1609
  { callSessionId: this.callSessionId },
1189
- 'Access request ID is null after notification attempt — failing closed',
1610
+ "Access request ID is null after notification attempt — failing closed",
1190
1611
  );
1191
1612
  this.handleAccessRequestTimeout();
1192
1613
  return;
@@ -1202,7 +1623,7 @@ export class RelayConnection {
1202
1623
  */
1203
1624
  private startAccessRequestWait(): void {
1204
1625
  this.accessRequestWaitActive = true;
1205
- this.connectionState = 'awaiting_guardian_decision';
1626
+ this.connectionState = "awaiting_guardian_decision";
1206
1627
 
1207
1628
  const timeoutMs = getUserConsultationTimeoutMs();
1208
1629
  const pollIntervalMs = getAccessRequestPollIntervalMs();
@@ -1213,12 +1634,17 @@ export class RelayConnection {
1213
1634
  true,
1214
1635
  );
1215
1636
 
1216
- updateCallSession(this.callSessionId, { status: 'waiting_on_user' });
1637
+ updateCallSession(this.callSessionId, { status: "waiting_on_user" });
1217
1638
 
1218
1639
  // Start the heartbeat timer for periodic progress updates.
1219
1640
  // Delay the first heartbeat by the estimated TTS playback duration so
1220
1641
  // the initial hold message finishes before any heartbeat fires.
1221
1642
  this.heartbeatSequence = 0;
1643
+ // Set the wait start time now so scheduleNextHeartbeat() always has a
1644
+ // valid reference point — even if the TTS delay timer is cancelled early
1645
+ // (e.g. by handleWaitStatePrompt when the caller speaks during playback).
1646
+ // The callback below re-stamps it to exclude the TTS delay if it fires.
1647
+ this.accessRequestWaitStartedAt = Date.now();
1222
1648
  this.accessRequestHeartbeatTimer = setTimeout(() => {
1223
1649
  this.accessRequestWaitStartedAt = Date.now();
1224
1650
  this.scheduleNextHeartbeat();
@@ -1236,9 +1662,9 @@ export class RelayConnection {
1236
1662
  return;
1237
1663
  }
1238
1664
 
1239
- if (request.status === 'approved') {
1665
+ if (request.status === "approved") {
1240
1666
  this.handleAccessRequestApproved();
1241
- } else if (request.status === 'denied') {
1667
+ } else if (request.status === "denied") {
1242
1668
  this.handleAccessRequestDenied();
1243
1669
  }
1244
1670
  // 'pending' continues polling; 'expired'/'cancelled' handled by timeout
@@ -1250,15 +1676,19 @@ export class RelayConnection {
1250
1676
 
1251
1677
  log.info(
1252
1678
  { callSessionId: this.callSessionId, requestId: this.accessRequestId },
1253
- 'Access request in-call wait timed out',
1679
+ "Access request in-call wait timed out",
1254
1680
  );
1255
1681
 
1256
1682
  this.handleAccessRequestTimeout();
1257
1683
  }, timeoutMs);
1258
1684
 
1259
1685
  log.info(
1260
- { callSessionId: this.callSessionId, requestId: this.accessRequestId, timeoutMs },
1261
- 'Access request in-call wait started',
1686
+ {
1687
+ callSessionId: this.callSessionId,
1688
+ requestId: this.accessRequestId,
1689
+ timeoutMs,
1690
+ },
1691
+ "Access request in-call wait started",
1262
1692
  );
1263
1693
  }
1264
1694
 
@@ -1287,80 +1717,35 @@ export class RelayConnection {
1287
1717
  */
1288
1718
  private handleAccessRequestApproved(): void {
1289
1719
  this.clearAccessRequestWait();
1290
- this.connectionState = 'connected';
1291
1720
 
1292
1721
  const assistantId = this.accessRequestAssistantId!;
1293
1722
  const fromNumber = this.accessRequestFromNumber!;
1294
1723
  const callerName = this.accessRequestCallerName;
1295
1724
 
1296
- recordCallEvent(this.callSessionId, 'inbound_acl_access_approved', {
1725
+ recordCallEvent(this.callSessionId, "inbound_acl_access_approved", {
1297
1726
  from: fromNumber,
1298
1727
  callerName,
1299
1728
  requestId: this.accessRequestId,
1300
1729
  });
1301
1730
 
1302
- // Activate the caller as a trusted contact via the existing upsert path
1303
- try {
1304
- upsertMember({
1305
- assistantId,
1306
- sourceChannel: 'voice',
1307
- externalUserId: fromNumber,
1308
- externalChatId: fromNumber,
1309
- displayName: callerName ?? undefined,
1310
- status: 'active',
1311
- policy: 'allow',
1312
- });
1313
- } catch (err) {
1314
- log.error({ err, callSessionId: this.callSessionId }, 'Failed to activate voice caller as trusted contact');
1315
- }
1316
-
1317
- // Re-resolve actor trust now that the member is active
1318
- const updatedTrust = resolveActorTrust({
1319
- assistantId,
1320
- sourceChannel: 'voice',
1321
- conversationExternalId: fromNumber,
1322
- actorExternalId: fromNumber,
1323
- });
1324
-
1325
- if (this.controller) {
1326
- this.controller.setGuardianContext(
1327
- toGuardianRuntimeContextFromTrust(updatedTrust, fromNumber),
1328
- );
1329
- }
1330
-
1331
- updateCallSession(this.callSessionId, { status: 'in_progress' });
1332
-
1333
1731
  log.info(
1334
1732
  { callSessionId: this.callSessionId, from: fromNumber },
1335
- 'Access request approved — caller activated and continuing call',
1733
+ "Access request approved — caller activated and continuing call",
1336
1734
  );
1337
1735
 
1338
- // Deliver deterministic transition copy directly via TTS instead of
1339
- // routing through handleUserInstruction, which would start a fresh
1340
- // model turn and risk reintroduction/disclosure reset.
1341
- const guardianLabel = this.resolveGuardianLabel();
1342
- const handoffText = `Great! ${guardianLabel} said I can speak with you. How can I help?`;
1343
- this.sendTextToken(handoffText, true);
1344
-
1345
- // Record the deterministic handoff as an assistant_spoke event and
1346
- // fire the transcript notifier so it appears in conversation history
1347
- // and real-time transcript subscribers — matching the parity of text
1348
- // spoken through the normal runTurn() pipeline.
1349
- recordCallEvent(this.callSessionId, 'assistant_spoke', { text: handoffText });
1350
- const session = getCallSession(this.callSessionId);
1351
- if (session) {
1352
- fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'assistant', handoffText);
1353
- }
1354
-
1355
- recordCallEvent(this.callSessionId, 'inbound_acl_post_approval_handoff_spoken', {
1356
- from: fromNumber,
1736
+ this.continueCallAfterTrustedContactActivation({
1737
+ assistantId,
1738
+ fromNumber,
1739
+ callerName: callerName ?? undefined,
1357
1740
  });
1358
1741
 
1359
- // Mark the next caller utterance as an opening acknowledgment so the
1360
- // LLM continues naturally without emitting a fresh introduction.
1361
- if (this.controller) {
1362
- this.controller.markNextCallerTurnAsOpeningAck();
1363
- }
1742
+ recordCallEvent(
1743
+ this.callSessionId,
1744
+ "inbound_acl_post_approval_handoff_spoken",
1745
+ {
1746
+ from: fromNumber,
1747
+ },
1748
+ );
1364
1749
  }
1365
1750
 
1366
1751
  /**
@@ -1371,7 +1756,7 @@ export class RelayConnection {
1371
1756
 
1372
1757
  const guardianLabel = this.resolveGuardianLabel();
1373
1758
 
1374
- recordCallEvent(this.callSessionId, 'inbound_acl_access_denied', {
1759
+ recordCallEvent(this.callSessionId, "inbound_acl_access_denied", {
1375
1760
  from: this.accessRequestFromNumber,
1376
1761
  requestId: this.accessRequestId,
1377
1762
  });
@@ -1381,21 +1766,21 @@ export class RelayConnection {
1381
1766
  true,
1382
1767
  );
1383
1768
 
1384
- this.connectionState = 'disconnecting';
1769
+ this.connectionState = "disconnecting";
1385
1770
 
1386
1771
  updateCallSession(this.callSessionId, {
1387
- status: 'failed',
1772
+ status: "failed",
1388
1773
  endedAt: Date.now(),
1389
- lastError: 'Inbound voice ACL: guardian denied access request',
1774
+ lastError: "Inbound voice ACL: guardian denied access request",
1390
1775
  });
1391
1776
 
1392
1777
  log.info(
1393
1778
  { callSessionId: this.callSessionId },
1394
- 'Access request denied — ending call',
1779
+ "Access request denied — ending call",
1395
1780
  );
1396
1781
 
1397
1782
  setTimeout(() => {
1398
- this.endSession('Access request denied');
1783
+ this.endSession("Access request denied");
1399
1784
  }, getTtsPlaybackDelayMs());
1400
1785
  }
1401
1786
 
@@ -1404,13 +1789,13 @@ export class RelayConnection {
1404
1789
  */
1405
1790
  private handleAccessRequestTimeout(): void {
1406
1791
  // Emit callback handoff notification before clearing wait state
1407
- this.emitAccessRequestCallbackHandoff('timeout');
1792
+ this.emitAccessRequestCallbackHandoff("timeout");
1408
1793
 
1409
1794
  this.clearAccessRequestWait();
1410
1795
 
1411
1796
  const guardianLabel = this.resolveGuardianLabel();
1412
1797
 
1413
- recordCallEvent(this.callSessionId, 'inbound_acl_access_timeout', {
1798
+ recordCallEvent(this.callSessionId, "inbound_acl_access_timeout", {
1414
1799
  from: this.accessRequestFromNumber,
1415
1800
  requestId: this.accessRequestId,
1416
1801
  callbackOptIn: this.callbackOptIn,
@@ -1418,27 +1803,27 @@ export class RelayConnection {
1418
1803
 
1419
1804
  const callbackNote = this.callbackOptIn
1420
1805
  ? ` I've noted that you'd like a callback — I'll pass that along to ${guardianLabel}.`
1421
- : '';
1806
+ : "";
1422
1807
  this.sendTextToken(
1423
1808
  `Sorry, I can't get ahold of ${guardianLabel} right now. I'll let them know you called.${callbackNote}`,
1424
1809
  true,
1425
1810
  );
1426
1811
 
1427
- this.connectionState = 'disconnecting';
1812
+ this.connectionState = "disconnecting";
1428
1813
 
1429
1814
  updateCallSession(this.callSessionId, {
1430
- status: 'failed',
1815
+ status: "failed",
1431
1816
  endedAt: Date.now(),
1432
- lastError: 'Inbound voice ACL: guardian approval wait timed out',
1817
+ lastError: "Inbound voice ACL: guardian approval wait timed out",
1433
1818
  });
1434
1819
 
1435
1820
  log.info(
1436
1821
  { callSessionId: this.callSessionId },
1437
- 'Access request timed out — ending call',
1822
+ "Access request timed out — ending call",
1438
1823
  );
1439
1824
 
1440
1825
  setTimeout(() => {
1441
- this.endSession('Access request timed out');
1826
+ this.endSession("Access request timed out");
1442
1827
  }, getTtsPlaybackDelayMs());
1443
1828
  }
1444
1829
 
@@ -1450,14 +1835,17 @@ export class RelayConnection {
1450
1835
  * Idempotent: uses callbackHandoffNotified guard + deterministic dedupeKey
1451
1836
  * to ensure at most one notification per call/request.
1452
1837
  */
1453
- private emitAccessRequestCallbackHandoff(reason: 'timeout' | 'transport_closed'): void {
1838
+ private emitAccessRequestCallbackHandoff(
1839
+ reason: "timeout" | "transport_closed",
1840
+ ): void {
1454
1841
  if (!this.callbackOptIn) return;
1455
1842
  if (!this.accessRequestId) return;
1456
1843
  if (this.callbackHandoffNotified) return;
1457
1844
 
1458
1845
  this.callbackHandoffNotified = true;
1459
1846
 
1460
- const assistantId = this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
1847
+ const assistantId =
1848
+ this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
1461
1849
  const fromNumber = this.accessRequestFromNumber ?? null;
1462
1850
 
1463
1851
  // Resolve canonical request for requestCode and conversationId
@@ -1469,32 +1857,39 @@ export class RelayConnection {
1469
1857
  let requesterMemberId: string | null = null;
1470
1858
  if (fromNumber) {
1471
1859
  try {
1472
- const member = findMember({
1473
- assistantId,
1474
- sourceChannel: 'voice',
1860
+ const contactResult = findContactChannel({
1861
+ channelType: "voice",
1475
1862
  externalUserId: fromNumber,
1476
1863
  externalChatId: fromNumber,
1477
1864
  });
1478
- if (member && member.status === 'active' && member.policy === 'allow') {
1479
- requesterMemberId = member.id;
1865
+ if (
1866
+ contactResult &&
1867
+ contactResult.channel.status === "active" &&
1868
+ contactResult.channel.policy === "allow"
1869
+ ) {
1870
+ requesterMemberId = contactResult.channel.id;
1480
1871
  }
1481
1872
  } catch (err) {
1482
- log.warn({ err, callSessionId: this.callSessionId }, 'Failed to resolve member for callback handoff');
1873
+ log.warn(
1874
+ { err, callSessionId: this.callSessionId },
1875
+ "Failed to resolve member for callback handoff",
1876
+ );
1483
1877
  }
1484
1878
  }
1485
1879
 
1486
1880
  const dedupeKey = `access-request-callback-handoff:${this.accessRequestId}`;
1487
- const sourceSessionId = canonicalRequest?.conversationId
1488
- ?? `access-req-callback-${this.accessRequestId}`;
1881
+ const sourceSessionId =
1882
+ canonicalRequest?.conversationId ??
1883
+ `access-req-callback-${this.accessRequestId}`;
1489
1884
 
1490
1885
  void emitNotificationSignal({
1491
- sourceEventName: 'ingress.access_request.callback_handoff',
1492
- sourceChannel: 'voice',
1886
+ sourceEventName: "ingress.access_request.callback_handoff",
1887
+ sourceChannel: "voice",
1493
1888
  sourceSessionId,
1494
1889
  assistantId,
1495
1890
  attentionHints: {
1496
1891
  requiresAction: false,
1497
- urgency: 'medium',
1892
+ urgency: "medium",
1498
1893
  isAsyncBackground: true,
1499
1894
  visibleInSourceNow: false,
1500
1895
  },
@@ -1502,7 +1897,7 @@ export class RelayConnection {
1502
1897
  requestId: this.accessRequestId,
1503
1898
  requestCode: canonicalRequest?.requestCode ?? null,
1504
1899
  callSessionId: this.callSessionId,
1505
- sourceChannel: 'voice',
1900
+ sourceChannel: "voice",
1506
1901
  reason,
1507
1902
  callbackOptIn: true,
1508
1903
  callerPhoneNumber: fromNumber,
@@ -1510,30 +1905,40 @@ export class RelayConnection {
1510
1905
  requesterExternalUserId: fromNumber,
1511
1906
  requesterChatId: fromNumber,
1512
1907
  requesterMemberId,
1513
- requesterMemberSourceChannel: requesterMemberId ? 'voice' : null,
1908
+ requesterMemberSourceChannel: requesterMemberId ? "voice" : null,
1514
1909
  },
1515
1910
  dedupeKey,
1516
- }).then(() => {
1517
- recordCallEvent(this.callSessionId, 'callback_handoff_notified', {
1518
- requestId: this.accessRequestId,
1519
- reason,
1520
- requesterMemberId,
1521
- });
1522
- log.info(
1523
- { callSessionId: this.callSessionId, requestId: this.accessRequestId, reason },
1524
- 'Callback handoff notification emitted',
1525
- );
1526
- }).catch((err) => {
1527
- recordCallEvent(this.callSessionId, 'callback_handoff_failed', {
1528
- requestId: this.accessRequestId,
1529
- reason,
1530
- error: err instanceof Error ? err.message : String(err),
1911
+ })
1912
+ .then(() => {
1913
+ recordCallEvent(this.callSessionId, "callback_handoff_notified", {
1914
+ requestId: this.accessRequestId,
1915
+ reason,
1916
+ requesterMemberId,
1917
+ });
1918
+ log.info(
1919
+ {
1920
+ callSessionId: this.callSessionId,
1921
+ requestId: this.accessRequestId,
1922
+ reason,
1923
+ },
1924
+ "Callback handoff notification emitted",
1925
+ );
1926
+ })
1927
+ .catch((err) => {
1928
+ recordCallEvent(this.callSessionId, "callback_handoff_failed", {
1929
+ requestId: this.accessRequestId,
1930
+ reason,
1931
+ error: err instanceof Error ? err.message : String(err),
1932
+ });
1933
+ log.error(
1934
+ {
1935
+ err,
1936
+ callSessionId: this.callSessionId,
1937
+ requestId: this.accessRequestId,
1938
+ },
1939
+ "Failed to emit callback handoff notification",
1940
+ );
1531
1941
  });
1532
- log.error(
1533
- { err, callSessionId: this.callSessionId, requestId: this.accessRequestId },
1534
- 'Failed to emit callback handoff notification',
1535
- );
1536
- });
1537
1942
  }
1538
1943
 
1539
1944
  /**
@@ -1546,7 +1951,7 @@ export class RelayConnection {
1546
1951
  this.nameCaptureTimeoutTimer = null;
1547
1952
  }
1548
1953
 
1549
- recordCallEvent(this.callSessionId, 'inbound_acl_name_capture_timeout', {
1954
+ recordCallEvent(this.callSessionId, "inbound_acl_name_capture_timeout", {
1550
1955
  from: this.accessRequestFromNumber,
1551
1956
  });
1552
1957
 
@@ -1555,27 +1960,27 @@ export class RelayConnection {
1555
1960
  true,
1556
1961
  );
1557
1962
 
1558
- this.connectionState = 'disconnecting';
1963
+ this.connectionState = "disconnecting";
1559
1964
 
1560
1965
  updateCallSession(this.callSessionId, {
1561
- status: 'failed',
1966
+ status: "failed",
1562
1967
  endedAt: Date.now(),
1563
- lastError: 'Inbound voice ACL: name capture timed out',
1968
+ lastError: "Inbound voice ACL: name capture timed out",
1564
1969
  });
1565
1970
 
1566
1971
  log.info(
1567
1972
  { callSessionId: this.callSessionId },
1568
- 'Name capture timed out — ending call',
1973
+ "Name capture timed out — ending call",
1569
1974
  );
1570
1975
 
1571
1976
  setTimeout(() => {
1572
- this.endSession('Name capture timed out');
1977
+ this.endSession("Name capture timed out");
1573
1978
  }, getTtsPlaybackDelayMs());
1574
1979
  }
1575
1980
 
1576
1981
  /**
1577
1982
  * Validate an entered invite code against active voice invites for the
1578
- * caller. On success, create/activate the ingress member and transition
1983
+ * caller. On success, create/activate the contact and transition
1579
1984
  * to the normal call flow. On failure, allow retries up to max attempts.
1580
1985
  */
1581
1986
  private attemptInviteCodeRedemption(enteredCode: string): void {
@@ -1586,74 +1991,85 @@ export class RelayConnection {
1586
1991
  const result = redeemVoiceInviteCode({
1587
1992
  assistantId: this.inviteRedemptionAssistantId,
1588
1993
  callerExternalUserId: this.inviteRedemptionFromNumber,
1589
- sourceChannel: 'voice',
1994
+ sourceChannel: "voice",
1590
1995
  code: enteredCode,
1591
1996
  });
1592
1997
 
1593
1998
  if (result.ok) {
1594
- this.connectionState = 'connected';
1595
1999
  this.inviteRedemptionActive = false;
1596
2000
  this.verificationAttempts = 0;
1597
- this.dtmfBuffer = '';
2001
+ this.dtmfBuffer = "";
1598
2002
 
1599
- recordCallEvent(this.callSessionId, 'invite_redemption_succeeded', {
2003
+ recordCallEvent(this.callSessionId, "invite_redemption_succeeded", {
1600
2004
  memberId: result.memberId,
1601
- ...(result.type === 'redeemed' ? { inviteId: result.inviteId } : {}),
2005
+ ...(result.type === "redeemed" ? { inviteId: result.inviteId } : {}),
1602
2006
  });
1603
2007
  log.info(
1604
- { callSessionId: this.callSessionId, memberId: result.memberId, type: result.type },
1605
- 'Voice invite redemption succeeded',
2008
+ {
2009
+ callSessionId: this.callSessionId,
2010
+ memberId: result.memberId,
2011
+ type: result.type,
2012
+ },
2013
+ "Voice invite redemption succeeded",
1606
2014
  );
1607
2015
 
1608
- if (this.controller) {
1609
- const redeemedActorTrust = resolveActorTrust({
1610
- assistantId: this.inviteRedemptionAssistantId,
1611
- sourceChannel: 'voice',
1612
- conversationExternalId: this.inviteRedemptionFromNumber,
1613
- actorExternalId: this.inviteRedemptionFromNumber,
1614
- });
1615
- this.controller.setGuardianContext(
1616
- toGuardianRuntimeContextFromTrust(redeemedActorTrust, this.inviteRedemptionFromNumber),
1617
- );
1618
- this.startNormalCallFlow(this.controller, true);
1619
- }
2016
+ this.continueCallAfterTrustedContactActivation({
2017
+ assistantId: this.inviteRedemptionAssistantId,
2018
+ fromNumber: this.inviteRedemptionFromNumber,
2019
+ callerName: this.inviteRedemptionFriendName ?? undefined,
2020
+ skipMemberActivation: true,
2021
+ });
1620
2022
  } else {
1621
2023
  // On any invalid/expired code, emit exact deterministic failure copy and end call immediately.
1622
2024
  this.inviteRedemptionActive = false;
1623
2025
 
1624
- recordCallEvent(this.callSessionId, 'invite_redemption_failed', {
2026
+ recordCallEvent(this.callSessionId, "invite_redemption_failed", {
1625
2027
  attempts: 1,
1626
2028
  });
1627
2029
  log.warn(
1628
2030
  { callSessionId: this.callSessionId },
1629
- 'Voice invite redemption failed — invalid or expired code',
2031
+ "Voice invite redemption failed — invalid or expired code",
1630
2032
  );
1631
2033
 
1632
- const displayGuardian = this.inviteRedemptionGuardianName ?? 'your contact';
2034
+ const displayGuardian =
2035
+ this.inviteRedemptionGuardianName ?? "your contact";
1633
2036
  this.sendTextToken(
1634
2037
  `Sorry, the code you provided is incorrect or has since expired. Please ask ${displayGuardian} for a new code. Goodbye.`,
1635
2038
  true,
1636
2039
  );
1637
2040
 
1638
- this.connectionState = 'disconnecting';
2041
+ this.connectionState = "disconnecting";
1639
2042
 
1640
2043
  updateCallSession(this.callSessionId, {
1641
- status: 'failed',
2044
+ status: "failed",
1642
2045
  endedAt: Date.now(),
1643
- lastError: 'Voice invite redemption failed — invalid or expired code',
2046
+ lastError: "Voice invite redemption failed — invalid or expired code",
1644
2047
  });
1645
2048
 
1646
2049
  const failSession = getCallSession(this.callSessionId);
1647
2050
  if (failSession) {
1648
2051
  expirePendingQuestions(this.callSessionId);
1649
- persistCallCompletionMessage(failSession.conversationId, this.callSessionId).catch((err) => {
1650
- log.error({ err, conversationId: failSession.conversationId, callSessionId: this.callSessionId }, 'Failed to persist call completion message');
2052
+ persistCallCompletionMessage(
2053
+ failSession.conversationId,
2054
+ this.callSessionId,
2055
+ ).catch((err) => {
2056
+ log.error(
2057
+ {
2058
+ err,
2059
+ conversationId: failSession.conversationId,
2060
+ callSessionId: this.callSessionId,
2061
+ },
2062
+ "Failed to persist call completion message",
2063
+ );
1651
2064
  });
1652
- fireCallCompletionNotifier(failSession.conversationId, this.callSessionId);
2065
+ fireCallCompletionNotifier(
2066
+ failSession.conversationId,
2067
+ this.callSessionId,
2068
+ );
1653
2069
  }
1654
2070
 
1655
2071
  setTimeout(() => {
1656
- this.endSession('Invite redemption failed');
2072
+ this.endSession("Invite redemption failed");
1657
2073
  }, getTtsPlaybackDelayMs());
1658
2074
  }
1659
2075
  }
@@ -1666,29 +2082,58 @@ export class RelayConnection {
1666
2082
  * to @username, then the user's preferred name from USER.md.
1667
2083
  */
1668
2084
  private resolveGuardianLabel(): string {
1669
- const assistantId = this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
2085
+ const assistantId =
2086
+ this.accessRequestAssistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
1670
2087
 
1671
2088
  // Try the voice-channel binding first, then fall back to any active
1672
2089
  // binding for the assistant (mirrors the cross-channel fallback pattern
1673
2090
  // in access-request-helper.ts).
1674
2091
  let metadataJson: string | null = null;
1675
- const voiceBinding = getGuardianBinding(assistantId, 'voice');
1676
- if (voiceBinding?.metadataJson) {
1677
- metadataJson = voiceBinding.metadataJson;
1678
- } else {
1679
- const allBindings = listActiveBindingsByAssistant(assistantId);
1680
- if (allBindings.length > 0 && allBindings[0].metadataJson) {
1681
- metadataJson = allBindings[0].metadataJson;
2092
+ // Contacts-first: prefer the voice-bound guardian, then fall back to
2093
+ // any guardian channel (mirrors the voice-first pattern in the legacy path).
2094
+ const voiceGuardian = findGuardianForChannel("voice", assistantId);
2095
+ const guardianChannels = voiceGuardian
2096
+ ? null
2097
+ : listGuardianChannels(assistantId);
2098
+ const guardianContact = voiceGuardian?.contact ?? guardianChannels?.contact;
2099
+ if (guardianContact) {
2100
+ const meta: Record<string, string> = {};
2101
+ if (guardianContact.displayName) {
2102
+ meta.displayName = guardianContact.displayName;
2103
+ }
2104
+ // Preserve the username fallback: use the voice channel's externalUserId
2105
+ // so downstream parsing can fall back to @username when displayName is a
2106
+ // raw external ID (e.g., phone number from contact-sync).
2107
+ const voiceChannel =
2108
+ voiceGuardian?.channel ??
2109
+ guardianChannels?.channels.find((ch) => ch.type === "voice");
2110
+ if (voiceChannel?.externalUserId) {
2111
+ meta.username = voiceChannel.externalUserId;
2112
+ }
2113
+ if (Object.keys(meta).length > 0) {
2114
+ metadataJson = JSON.stringify(meta);
2115
+ }
2116
+ }
2117
+ if (!metadataJson) {
2118
+ const voiceBinding = getGuardianBinding(assistantId, "voice");
2119
+ if (voiceBinding?.metadataJson) {
2120
+ metadataJson = voiceBinding.metadataJson;
1682
2121
  }
1683
2122
  }
1684
2123
 
1685
2124
  if (metadataJson) {
1686
2125
  try {
1687
2126
  const parsed = JSON.parse(metadataJson) as Record<string, unknown>;
1688
- if (typeof parsed.displayName === 'string' && parsed.displayName.trim().length > 0) {
2127
+ if (
2128
+ typeof parsed.displayName === "string" &&
2129
+ parsed.displayName.trim().length > 0
2130
+ ) {
1689
2131
  return parsed.displayName.trim();
1690
2132
  }
1691
- if (typeof parsed.username === 'string' && parsed.username.trim().length > 0) {
2133
+ if (
2134
+ typeof parsed.username === "string" &&
2135
+ parsed.username.trim().length > 0
2136
+ ) {
1692
2137
  return `@${parsed.username.trim()}`;
1693
2138
  }
1694
2139
  } catch {
@@ -1738,10 +2183,18 @@ export class RelayConnection {
1738
2183
 
1739
2184
  const elapsed = Date.now() - this.accessRequestWaitStartedAt;
1740
2185
  const initialWindow = getGuardianWaitUpdateInitialWindowMs();
1741
- const intervalMs = elapsed < initialWindow
1742
- ? getGuardianWaitUpdateInitialIntervalMs()
1743
- : getGuardianWaitUpdateSteadyMinIntervalMs() +
1744
- Math.floor(Math.random() * Math.max(0, getGuardianWaitUpdateSteadyMaxIntervalMs() - getGuardianWaitUpdateSteadyMinIntervalMs()));
2186
+ const intervalMs =
2187
+ elapsed < initialWindow
2188
+ ? getGuardianWaitUpdateInitialIntervalMs()
2189
+ : getGuardianWaitUpdateSteadyMinIntervalMs() +
2190
+ Math.floor(
2191
+ Math.random() *
2192
+ Math.max(
2193
+ 0,
2194
+ getGuardianWaitUpdateSteadyMaxIntervalMs() -
2195
+ getGuardianWaitUpdateSteadyMinIntervalMs(),
2196
+ ),
2197
+ );
1745
2198
 
1746
2199
  this.accessRequestHeartbeatTimer = setTimeout(() => {
1747
2200
  if (!this.accessRequestWaitActive) return;
@@ -1749,14 +2202,21 @@ export class RelayConnection {
1749
2202
  const message = this.getHeartbeatMessage();
1750
2203
  this.sendTextToken(message, true);
1751
2204
 
1752
- recordCallEvent(this.callSessionId, 'voice_guardian_wait_heartbeat_sent', {
1753
- sequence: this.heartbeatSequence - 1,
1754
- message,
1755
- });
2205
+ recordCallEvent(
2206
+ this.callSessionId,
2207
+ "voice_guardian_wait_heartbeat_sent",
2208
+ {
2209
+ sequence: this.heartbeatSequence - 1,
2210
+ message,
2211
+ },
2212
+ );
1756
2213
 
1757
2214
  log.debug(
1758
- { callSessionId: this.callSessionId, sequence: this.heartbeatSequence - 1 },
1759
- 'Guardian wait heartbeat sent',
2215
+ {
2216
+ callSessionId: this.callSessionId,
2217
+ sequence: this.heartbeatSequence - 1,
2218
+ },
2219
+ "Guardian wait heartbeat sent",
1760
2220
  );
1761
2221
 
1762
2222
  // Schedule the next heartbeat
@@ -1773,52 +2233,72 @@ export class RelayConnection {
1773
2233
  * - 'callback_decline': explicitly declining a callback
1774
2234
  * - 'neutral': anything else
1775
2235
  */
1776
- private classifyWaitUtterance(text: string): 'empty' | 'patience_check' | 'impatient' | 'callback_opt_in' | 'callback_decline' | 'neutral' {
2236
+ private classifyWaitUtterance(
2237
+ text: string,
2238
+ ):
2239
+ | "empty"
2240
+ | "patience_check"
2241
+ | "impatient"
2242
+ | "callback_opt_in"
2243
+ | "callback_decline"
2244
+ | "neutral" {
1777
2245
  const lower = text.toLowerCase().trim();
1778
- if (lower.length === 0) return 'empty';
2246
+ if (lower.length === 0) return "empty";
1779
2247
 
1780
2248
  // Callback opt-in patterns (check before impatience to catch "yes call me back")
1781
2249
  if (this.callbackOfferMade) {
1782
- if (/\b(yes|yeah|yep|sure|okay|ok|please)\b.*\b(call\s*(me\s*)?back|callback)\b/.test(lower)
1783
- || /\b(call\s*(me\s*)?back|callback)\b.*\b(yes|yeah|please|sure)\b/.test(lower)
1784
- || /^(yes|yeah|yep|sure|okay|ok|please)\s*[.,!]?\s*$/.test(lower)
1785
- || /\bcall\s*(me\s*)?back\b/.test(lower)
1786
- || /\bplease\s+do\b/.test(lower)) {
1787
- return 'callback_opt_in';
2250
+ if (
2251
+ /\b(yes|yeah|yep|sure|okay|ok|please)\b.*\b(call\s*(me\s*)?back|callback)\b/.test(
2252
+ lower,
2253
+ ) ||
2254
+ /\b(call\s*(me\s*)?back|callback)\b.*\b(yes|yeah|please|sure)\b/.test(
2255
+ lower,
2256
+ ) ||
2257
+ /^(yes|yeah|yep|sure|okay|ok|please)\s*[.,!]?\s*$/.test(lower) ||
2258
+ /\bcall\s*(me\s*)?back\b/.test(lower) ||
2259
+ /\bplease\s+do\b/.test(lower)
2260
+ ) {
2261
+ return "callback_opt_in";
1788
2262
  }
1789
- if (/\b(no|nah|nope)\b/.test(lower)
1790
- || /\bi('?ll| will)\s+hold\b/.test(lower)
1791
- || /\bi('?ll| will)\s+wait\b/.test(lower)) {
1792
- return 'callback_decline';
2263
+ if (
2264
+ /\b(no|nah|nope)\b/.test(lower) ||
2265
+ /\bi('?ll| will)\s+hold\b/.test(lower) ||
2266
+ /\bi('?ll| will)\s+wait\b/.test(lower)
2267
+ ) {
2268
+ return "callback_decline";
1793
2269
  }
1794
2270
  }
1795
2271
 
1796
2272
  // Impatience patterns
1797
- if (/\bhurry\s*(up)?\b/.test(lower)
1798
- || /\btaking\s+(too\s+|so\s+)?long\b/.test(lower)
1799
- || /\bforget\s+it\b/.test(lower)
1800
- || /\bnever\s*mind\b/.test(lower)
1801
- || /\bdon'?t\s+have\s+time\b/.test(lower)
1802
- || /\bhow\s+much\s+longer\b/.test(lower)
1803
- || /\bi('?m| am)\s+(getting\s+)?impatient\b/.test(lower)
1804
- || /\bthis\s+is\s+(ridiculous|absurd|crazy)\b/.test(lower)
1805
- || /\bcome\s+on\b/.test(lower)
1806
- || /\bi\s+(gotta|have\s+to|need\s+to)\s+go\b/.test(lower)) {
1807
- return 'impatient';
2273
+ if (
2274
+ /\bhurry\s*(up)?\b/.test(lower) ||
2275
+ /\btaking\s+(too\s+|so\s+)?long\b/.test(lower) ||
2276
+ /\bforget\s+it\b/.test(lower) ||
2277
+ /\bnever\s*mind\b/.test(lower) ||
2278
+ /\bdon'?t\s+have\s+time\b/.test(lower) ||
2279
+ /\bhow\s+much\s+longer\b/.test(lower) ||
2280
+ /\bi('?m| am)\s+(getting\s+)?impatient\b/.test(lower) ||
2281
+ /\bthis\s+is\s+(ridiculous|absurd|crazy)\b/.test(lower) ||
2282
+ /\bcome\s+on\b/.test(lower) ||
2283
+ /\bi\s+(gotta|have\s+to|need\s+to)\s+go\b/.test(lower)
2284
+ ) {
2285
+ return "impatient";
1808
2286
  }
1809
2287
 
1810
2288
  // Patience check / status inquiry patterns
1811
- if (/\bhello\??\s*$/.test(lower)
1812
- || /\bstill\s+there\b/.test(lower)
1813
- || /\bany\s+(update|news)\b/.test(lower)
1814
- || /\bwhat('?s| is)\s+(happening|going\s+on)\b/.test(lower)
1815
- || /\bare\s+you\s+still\b/.test(lower)
1816
- || /\bhow\s+(long|much\s+longer)\b/.test(lower)
1817
- || /\banyone\s+there\b/.test(lower)) {
1818
- return 'patience_check';
2289
+ if (
2290
+ /\bhello\??\s*$/.test(lower) ||
2291
+ /\bstill\s+there\b/.test(lower) ||
2292
+ /\bany\s+(update|news)\b/.test(lower) ||
2293
+ /\bwhat('?s| is)\s+(happening|going\s+on)\b/.test(lower) ||
2294
+ /\bare\s+you\s+still\b/.test(lower) ||
2295
+ /\bhow\s+(long|much\s+longer)\b/.test(lower) ||
2296
+ /\banyone\s+there\b/.test(lower)
2297
+ ) {
2298
+ return "patience_check";
1819
2299
  }
1820
2300
 
1821
- return 'neutral';
2301
+ return "neutral";
1822
2302
  }
1823
2303
 
1824
2304
  /**
@@ -1829,12 +2309,16 @@ export class RelayConnection {
1829
2309
  const now = Date.now();
1830
2310
  const classification = this.classifyWaitUtterance(text);
1831
2311
 
1832
- recordCallEvent(this.callSessionId, 'voice_guardian_wait_prompt_classified', {
1833
- classification,
1834
- transcript: text,
1835
- });
2312
+ recordCallEvent(
2313
+ this.callSessionId,
2314
+ "voice_guardian_wait_prompt_classified",
2315
+ {
2316
+ classification,
2317
+ transcript: text,
2318
+ },
2319
+ );
1836
2320
 
1837
- if (classification === 'empty') return;
2321
+ if (classification === "empty") return;
1838
2322
 
1839
2323
  const guardianLabel = this.resolveGuardianLabel();
1840
2324
 
@@ -1842,10 +2326,14 @@ export class RelayConnection {
1842
2326
  // the caller is answering a direct question and dropping their response
1843
2327
  // would silently discard their decision.
1844
2328
  switch (classification) {
1845
- case 'callback_opt_in': {
2329
+ case "callback_opt_in": {
1846
2330
  this.callbackOptIn = true;
1847
2331
  this.lastInWaitReplyAt = now;
1848
- recordCallEvent(this.callSessionId, 'voice_guardian_wait_callback_opt_in_set', {});
2332
+ recordCallEvent(
2333
+ this.callSessionId,
2334
+ "voice_guardian_wait_callback_opt_in_set",
2335
+ {},
2336
+ );
1849
2337
  if (this.accessRequestHeartbeatTimer) {
1850
2338
  clearTimeout(this.accessRequestHeartbeatTimer);
1851
2339
  this.accessRequestHeartbeatTimer = null;
@@ -1857,10 +2345,14 @@ export class RelayConnection {
1857
2345
  this.scheduleNextHeartbeat();
1858
2346
  return;
1859
2347
  }
1860
- case 'callback_decline': {
2348
+ case "callback_decline": {
1861
2349
  this.callbackOptIn = false;
1862
2350
  this.lastInWaitReplyAt = now;
1863
- recordCallEvent(this.callSessionId, 'voice_guardian_wait_callback_opt_in_declined', {});
2351
+ recordCallEvent(
2352
+ this.callSessionId,
2353
+ "voice_guardian_wait_callback_opt_in_declined",
2354
+ {},
2355
+ );
1864
2356
  if (this.accessRequestHeartbeatTimer) {
1865
2357
  clearTimeout(this.accessRequestHeartbeatTimer);
1866
2358
  this.accessRequestHeartbeatTimer = null;
@@ -1877,21 +2369,31 @@ export class RelayConnection {
1877
2369
  }
1878
2370
 
1879
2371
  // Enforce cooldown on non-callback utterances to prevent spam
1880
- if (now - this.lastInWaitReplyAt < RelayConnection.IN_WAIT_REPLY_COOLDOWN_MS) {
1881
- log.debug({ callSessionId: this.callSessionId }, 'In-wait reply suppressed by cooldown');
2372
+ if (
2373
+ now - this.lastInWaitReplyAt <
2374
+ RelayConnection.IN_WAIT_REPLY_COOLDOWN_MS
2375
+ ) {
2376
+ log.debug(
2377
+ { callSessionId: this.callSessionId },
2378
+ "In-wait reply suppressed by cooldown",
2379
+ );
1882
2380
  return;
1883
2381
  }
1884
2382
  this.lastInWaitReplyAt = now;
1885
2383
 
1886
2384
  switch (classification) {
1887
- case 'impatient': {
2385
+ case "impatient": {
1888
2386
  if (this.accessRequestHeartbeatTimer) {
1889
2387
  clearTimeout(this.accessRequestHeartbeatTimer);
1890
2388
  this.accessRequestHeartbeatTimer = null;
1891
2389
  }
1892
2390
  if (!this.callbackOfferMade) {
1893
2391
  this.callbackOfferMade = true;
1894
- recordCallEvent(this.callSessionId, 'voice_guardian_wait_callback_offer_sent', {});
2392
+ recordCallEvent(
2393
+ this.callSessionId,
2394
+ "voice_guardian_wait_callback_offer_sent",
2395
+ {},
2396
+ );
1895
2397
  this.sendTextToken(
1896
2398
  `I understand this is taking a while. I can have ${guardianLabel} call you back once I hear from them. Would you like that, or would you prefer to keep holding?`,
1897
2399
  true,
@@ -1906,7 +2408,7 @@ export class RelayConnection {
1906
2408
  this.scheduleNextHeartbeat();
1907
2409
  break;
1908
2410
  }
1909
- case 'patience_check': {
2411
+ case "patience_check": {
1910
2412
  // Immediate reassurance — reset the heartbeat timer so we
1911
2413
  // don't double up with a scheduled heartbeat
1912
2414
  if (this.accessRequestHeartbeatTimer) {
@@ -1920,7 +2422,7 @@ export class RelayConnection {
1920
2422
  this.scheduleNextHeartbeat();
1921
2423
  break;
1922
2424
  }
1923
- case 'neutral':
2425
+ case "neutral":
1924
2426
  default: {
1925
2427
  if (this.accessRequestHeartbeatTimer) {
1926
2428
  clearTimeout(this.accessRequestHeartbeatTimer);
@@ -1937,7 +2439,7 @@ export class RelayConnection {
1937
2439
  }
1938
2440
 
1939
2441
  private async handlePrompt(msg: RelayPromptMessage): Promise<void> {
1940
- if (this.connectionState === 'disconnecting') {
2442
+ if (this.connectionState === "disconnecting") {
1941
2443
  return;
1942
2444
  }
1943
2445
 
@@ -1947,7 +2449,7 @@ export class RelayConnection {
1947
2449
  }
1948
2450
 
1949
2451
  // During name capture, the caller's response is their name.
1950
- if (this.connectionState === 'awaiting_name') {
2452
+ if (this.connectionState === "awaiting_name") {
1951
2453
  const callerName = msg.voicePrompt.trim();
1952
2454
  if (!callerName) {
1953
2455
  // Whitespace-only or empty transcript (e.g. silence/noise) —
@@ -1957,7 +2459,7 @@ export class RelayConnection {
1957
2459
  }
1958
2460
  log.info(
1959
2461
  { callSessionId: this.callSessionId, callerName },
1960
- 'Name captured from unknown inbound caller',
2462
+ "Name captured from unknown inbound caller",
1961
2463
  );
1962
2464
  this.handleNameCaptureResponse(callerName);
1963
2465
  return;
@@ -1965,18 +2467,27 @@ export class RelayConnection {
1965
2467
 
1966
2468
  // During guardian decision wait, classify caller speech for
1967
2469
  // reassurance, impatience detection, and callback offer.
1968
- if (this.connectionState === 'awaiting_guardian_decision') {
2470
+ if (this.connectionState === "awaiting_guardian_decision") {
1969
2471
  this.handleWaitStatePrompt(msg.voicePrompt);
1970
2472
  return;
1971
2473
  }
1972
2474
 
1973
2475
  // During guardian verification (inbound or outbound), attempt to parse
1974
2476
  // spoken digits from the transcript and validate them.
1975
- if (this.connectionState === 'verification_pending' && this.guardianVerificationActive) {
1976
- const spokenDigits = RelayConnection.parseDigitsFromSpeech(msg.voicePrompt);
2477
+ if (
2478
+ this.connectionState === "verification_pending" &&
2479
+ this.guardianVerificationActive
2480
+ ) {
2481
+ const spokenDigits = RelayConnection.parseDigitsFromSpeech(
2482
+ msg.voicePrompt,
2483
+ );
1977
2484
  log.info(
1978
- { callSessionId: this.callSessionId, transcript: msg.voicePrompt, spokenDigits },
1979
- 'Speech received during guardian voice verification',
2485
+ {
2486
+ callSessionId: this.callSessionId,
2487
+ transcript: msg.voicePrompt,
2488
+ spokenDigits,
2489
+ },
2490
+ "Speech received during guardian voice verification",
1980
2491
  );
1981
2492
  if (spokenDigits.length >= this.verificationCodeLength) {
1982
2493
  const enteredCode = spokenDigits.slice(0, this.verificationCodeLength);
@@ -1992,14 +2503,26 @@ export class RelayConnection {
1992
2503
 
1993
2504
  // During invite redemption, attempt to parse spoken digits from the
1994
2505
  // transcript and validate against the caller's active voice invite.
1995
- if (this.connectionState === 'verification_pending' && this.inviteRedemptionActive) {
1996
- const spokenDigits = RelayConnection.parseDigitsFromSpeech(msg.voicePrompt);
2506
+ if (
2507
+ this.connectionState === "verification_pending" &&
2508
+ this.inviteRedemptionActive
2509
+ ) {
2510
+ const spokenDigits = RelayConnection.parseDigitsFromSpeech(
2511
+ msg.voicePrompt,
2512
+ );
1997
2513
  log.info(
1998
- { callSessionId: this.callSessionId, transcript: msg.voicePrompt, spokenDigits },
1999
- 'Speech received during invite redemption',
2514
+ {
2515
+ callSessionId: this.callSessionId,
2516
+ transcript: msg.voicePrompt,
2517
+ spokenDigits,
2518
+ },
2519
+ "Speech received during invite redemption",
2000
2520
  );
2001
2521
  if (spokenDigits.length >= this.inviteRedemptionCodeLength) {
2002
- const enteredCode = spokenDigits.slice(0, this.inviteRedemptionCodeLength);
2522
+ const enteredCode = spokenDigits.slice(
2523
+ 0,
2524
+ this.inviteRedemptionCodeLength,
2525
+ );
2003
2526
  this.attemptInviteCodeRedemption(enteredCode);
2004
2527
  } else if (spokenDigits.length > 0) {
2005
2528
  this.sendTextToken(
@@ -2012,31 +2535,39 @@ export class RelayConnection {
2012
2535
 
2013
2536
  // During outbound callee verification, ignore voice prompts — the callee
2014
2537
  // should be entering DTMF digits, not speaking.
2015
- if (this.connectionState === 'verification_pending') {
2016
- log.debug({ callSessionId: this.callSessionId }, 'Ignoring voice prompt during callee verification');
2538
+ if (this.connectionState === "verification_pending") {
2539
+ log.debug(
2540
+ { callSessionId: this.callSessionId },
2541
+ "Ignoring voice prompt during callee verification",
2542
+ );
2017
2543
  return;
2018
2544
  }
2019
2545
 
2020
2546
  log.info(
2021
- { callSessionId: this.callSessionId, transcript: msg.voicePrompt, lang: msg.lang },
2022
- 'Caller transcript received (final)',
2547
+ {
2548
+ callSessionId: this.callSessionId,
2549
+ transcript: msg.voicePrompt,
2550
+ lang: msg.lang,
2551
+ },
2552
+ "Caller transcript received (final)",
2023
2553
  );
2024
2554
 
2025
2555
  // Spread to widen the typed message into a plain record — extractPromptSpeakerMetadata
2026
2556
  // probes for snake_case and nested property variants not on RelayPromptMessage.
2027
2557
  const speakerMetadata = extractPromptSpeakerMetadata({ ...msg });
2028
- const speaker = this.speakerIdentityTracker.identifySpeaker(speakerMetadata);
2558
+ const speaker =
2559
+ this.speakerIdentityTracker.identifySpeaker(speakerMetadata);
2029
2560
 
2030
2561
  // Record in conversation history
2031
2562
  this.conversationHistory.push({
2032
- role: 'caller',
2563
+ role: "caller",
2033
2564
  text: msg.voicePrompt,
2034
2565
  timestamp: Date.now(),
2035
2566
  speaker,
2036
2567
  });
2037
2568
 
2038
2569
  // Record event
2039
- recordCallEvent(this.callSessionId, 'caller_spoke', {
2570
+ recordCallEvent(this.callSessionId, "caller_spoke", {
2040
2571
  transcript: msg.voicePrompt,
2041
2572
  lang: msg.lang,
2042
2573
  speakerId: speaker.speakerId,
@@ -2050,7 +2581,12 @@ export class RelayConnection {
2050
2581
  // User message persistence is handled by the session pipeline
2051
2582
  // (voice-session-bridge -> session.persistUserMessage) so we only
2052
2583
  // need to fire the transcript notifier for UI subscribers here.
2053
- fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'caller', msg.voicePrompt);
2584
+ fireCallTranscriptNotifier(
2585
+ session.conversationId,
2586
+ this.callSessionId,
2587
+ "caller",
2588
+ msg.voicePrompt,
2589
+ );
2054
2590
  }
2055
2591
 
2056
2592
  // Route to controller for session-backed response
@@ -2065,24 +2601,35 @@ export class RelayConnection {
2065
2601
  try {
2066
2602
  await conversationStore.addMessage(
2067
2603
  session.conversationId,
2068
- 'user',
2069
- JSON.stringify([{ type: 'text', text: msg.voicePrompt }]),
2070
- { userMessageChannel: 'voice', assistantMessageChannel: 'voice', userMessageInterface: 'voice', assistantMessageInterface: 'voice' },
2604
+ "user",
2605
+ JSON.stringify([{ type: "text", text: msg.voicePrompt }]),
2606
+ {
2607
+ userMessageChannel: "voice",
2608
+ assistantMessageChannel: "voice",
2609
+ userMessageInterface: "voice",
2610
+ assistantMessageInterface: "voice",
2611
+ },
2071
2612
  );
2072
2613
  } catch (err) {
2073
2614
  // Best-effort — don't let persistence failures prevent the hold
2074
2615
  // response from reaching the caller.
2075
- log.warn({ err, callSessionId: this.callSessionId }, 'Failed to persist early caller utterance');
2616
+ log.warn(
2617
+ { err, callSessionId: this.callSessionId },
2618
+ "Failed to persist early caller utterance",
2619
+ );
2076
2620
  }
2077
2621
  }
2078
- this.sendTextToken('I\'m still setting up. Please hold.', true);
2622
+ this.sendTextToken("I'm still setting up. Please hold.", true);
2079
2623
  }
2080
2624
  }
2081
2625
 
2082
2626
  private handleInterrupt(msg: RelayInterruptMessage): void {
2083
2627
  log.info(
2084
- { callSessionId: this.callSessionId, utteranceUntilInterrupt: msg.utteranceUntilInterrupt },
2085
- 'Caller interrupted assistant',
2628
+ {
2629
+ callSessionId: this.callSessionId,
2630
+ utteranceUntilInterrupt: msg.utteranceUntilInterrupt,
2631
+ },
2632
+ "Caller interrupted assistant",
2086
2633
  );
2087
2634
 
2088
2635
  // Abort any in-flight processing
@@ -2096,32 +2643,41 @@ export class RelayConnection {
2096
2643
  }
2097
2644
 
2098
2645
  private handleDtmf(msg: RelayDtmfMessage): void {
2099
- if (this.connectionState === 'disconnecting') {
2646
+ if (this.connectionState === "disconnecting") {
2100
2647
  return;
2101
2648
  }
2102
2649
 
2103
2650
  // Ignore DTMF during name capture and guardian decision wait
2104
- if (this.connectionState === 'awaiting_name' || this.connectionState === 'awaiting_guardian_decision') {
2651
+ if (
2652
+ this.connectionState === "awaiting_name" ||
2653
+ this.connectionState === "awaiting_guardian_decision"
2654
+ ) {
2105
2655
  return;
2106
2656
  }
2107
2657
 
2108
2658
  log.info(
2109
2659
  { callSessionId: this.callSessionId, digit: msg.digit },
2110
- 'DTMF digit received',
2660
+ "DTMF digit received",
2111
2661
  );
2112
2662
 
2113
- recordCallEvent(this.callSessionId, 'caller_spoke', {
2663
+ recordCallEvent(this.callSessionId, "caller_spoke", {
2114
2664
  dtmfDigit: msg.digit,
2115
2665
  });
2116
2666
 
2117
2667
  // If guardian verification (inbound or outbound) is pending, accumulate
2118
2668
  // digits and validate against the challenge via the guardian service.
2119
- if (this.connectionState === 'verification_pending' && this.guardianVerificationActive) {
2669
+ if (
2670
+ this.connectionState === "verification_pending" &&
2671
+ this.guardianVerificationActive
2672
+ ) {
2120
2673
  this.dtmfBuffer += msg.digit;
2121
2674
 
2122
2675
  if (this.dtmfBuffer.length >= this.verificationCodeLength) {
2123
- const enteredCode = this.dtmfBuffer.slice(0, this.verificationCodeLength);
2124
- this.dtmfBuffer = '';
2676
+ const enteredCode = this.dtmfBuffer.slice(
2677
+ 0,
2678
+ this.verificationCodeLength,
2679
+ );
2680
+ this.dtmfBuffer = "";
2125
2681
  this.attemptGuardianCodeVerification(enteredCode);
2126
2682
  }
2127
2683
  return;
@@ -2129,39 +2685,63 @@ export class RelayConnection {
2129
2685
 
2130
2686
  // If invite redemption is pending, accumulate digits and validate
2131
2687
  // the code against the caller's active voice invite.
2132
- if (this.connectionState === 'verification_pending' && this.inviteRedemptionActive) {
2688
+ if (
2689
+ this.connectionState === "verification_pending" &&
2690
+ this.inviteRedemptionActive
2691
+ ) {
2133
2692
  this.dtmfBuffer += msg.digit;
2134
2693
 
2135
2694
  if (this.dtmfBuffer.length >= this.inviteRedemptionCodeLength) {
2136
- const enteredCode = this.dtmfBuffer.slice(0, this.inviteRedemptionCodeLength);
2137
- this.dtmfBuffer = '';
2695
+ const enteredCode = this.dtmfBuffer.slice(
2696
+ 0,
2697
+ this.inviteRedemptionCodeLength,
2698
+ );
2699
+ this.dtmfBuffer = "";
2138
2700
  this.attemptInviteCodeRedemption(enteredCode);
2139
2701
  }
2140
2702
  return;
2141
2703
  }
2142
2704
 
2143
2705
  // If outbound callee verification is pending, accumulate digits and check the code
2144
- if (this.connectionState === 'verification_pending' && this.verificationCode) {
2706
+ if (
2707
+ this.connectionState === "verification_pending" &&
2708
+ this.verificationCode
2709
+ ) {
2145
2710
  this.dtmfBuffer += msg.digit;
2146
2711
 
2147
2712
  if (this.dtmfBuffer.length >= this.verificationCodeLength) {
2148
- const enteredCode = this.dtmfBuffer.slice(0, this.verificationCodeLength);
2149
- this.dtmfBuffer = '';
2713
+ const enteredCode = this.dtmfBuffer.slice(
2714
+ 0,
2715
+ this.verificationCodeLength,
2716
+ );
2717
+ this.dtmfBuffer = "";
2150
2718
 
2151
2719
  if (enteredCode === this.verificationCode) {
2152
2720
  // Verification succeeded
2153
- this.connectionState = 'connected';
2721
+ this.connectionState = "connected";
2154
2722
  this.verificationCode = null;
2155
2723
  this.verificationAttempts = 0;
2156
2724
 
2157
- recordCallEvent(this.callSessionId, 'callee_verification_succeeded', {});
2158
- log.info({ callSessionId: this.callSessionId }, 'Callee verification succeeded');
2725
+ recordCallEvent(
2726
+ this.callSessionId,
2727
+ "callee_verification_succeeded",
2728
+ {},
2729
+ );
2730
+ log.info(
2731
+ { callSessionId: this.callSessionId },
2732
+ "Callee verification succeeded",
2733
+ );
2159
2734
 
2160
2735
  // Proceed to the normal call flow
2161
2736
  if (this.controller) {
2162
- this.controller.startInitialGreeting().catch((err) =>
2163
- log.error({ err, callSessionId: this.callSessionId }, 'Failed to start initial outbound greeting after verification'),
2164
- );
2737
+ this.controller
2738
+ .startInitialGreeting()
2739
+ .catch((err) =>
2740
+ log.error(
2741
+ { err, callSessionId: this.callSessionId },
2742
+ "Failed to start initial outbound greeting after verification",
2743
+ ),
2744
+ );
2165
2745
  }
2166
2746
  } else {
2167
2747
  // Verification failed for this attempt
@@ -2169,48 +2749,85 @@ export class RelayConnection {
2169
2749
 
2170
2750
  if (this.verificationAttempts >= this.verificationMaxAttempts) {
2171
2751
  // Max attempts reached — end the call
2172
- recordCallEvent(this.callSessionId, 'callee_verification_failed', {
2752
+ recordCallEvent(this.callSessionId, "callee_verification_failed", {
2173
2753
  attempts: this.verificationAttempts,
2174
2754
  });
2175
- log.warn({ callSessionId: this.callSessionId, attempts: this.verificationAttempts }, 'Callee verification failed — max attempts reached');
2755
+ log.warn(
2756
+ {
2757
+ callSessionId: this.callSessionId,
2758
+ attempts: this.verificationAttempts,
2759
+ },
2760
+ "Callee verification failed — max attempts reached",
2761
+ );
2176
2762
 
2177
- this.sendTextToken('Verification failed. Goodbye.', true);
2763
+ this.sendTextToken("Verification failed. Goodbye.", true);
2178
2764
 
2179
2765
  // Mark failed immediately so a relay close during the goodbye TTS
2180
2766
  // window cannot race this into a terminal "completed" status.
2181
2767
  updateCallSession(this.callSessionId, {
2182
- status: 'failed',
2768
+ status: "failed",
2183
2769
  endedAt: Date.now(),
2184
- lastError: 'Callee verification failed — max attempts exceeded',
2770
+ lastError: "Callee verification failed — max attempts exceeded",
2185
2771
  });
2186
2772
 
2187
2773
  const session = getCallSession(this.callSessionId);
2188
2774
  if (session) {
2189
2775
  expirePendingQuestions(this.callSessionId);
2190
- persistCallCompletionMessage(session.conversationId, this.callSessionId).catch((err) => {
2191
- log.error({ err, conversationId: session.conversationId, callSessionId: this.callSessionId }, 'Failed to persist call completion message');
2776
+ persistCallCompletionMessage(
2777
+ session.conversationId,
2778
+ this.callSessionId,
2779
+ ).catch((err) => {
2780
+ log.error(
2781
+ {
2782
+ err,
2783
+ conversationId: session.conversationId,
2784
+ callSessionId: this.callSessionId,
2785
+ },
2786
+ "Failed to persist call completion message",
2787
+ );
2192
2788
  });
2193
- fireCallCompletionNotifier(session.conversationId, this.callSessionId);
2789
+ fireCallCompletionNotifier(
2790
+ session.conversationId,
2791
+ this.callSessionId,
2792
+ );
2194
2793
  if (session.initiatedFromConversationId) {
2195
- addPointerMessage(session.initiatedFromConversationId, 'failed', session.toNumber, {
2196
- reason: 'Callee verification failed',
2197
- }).catch((err) => {
2198
- log.warn({ conversationId: session.initiatedFromConversationId, err }, 'Skipping pointer write — origin conversation may no longer exist');
2794
+ addPointerMessage(
2795
+ session.initiatedFromConversationId,
2796
+ "failed",
2797
+ session.toNumber,
2798
+ {
2799
+ reason: "Callee verification failed",
2800
+ },
2801
+ ).catch((err) => {
2802
+ log.warn(
2803
+ {
2804
+ conversationId: session.initiatedFromConversationId,
2805
+ err,
2806
+ },
2807
+ "Skipping pointer write — origin conversation may no longer exist",
2808
+ );
2199
2809
  });
2200
2810
  }
2201
2811
  }
2202
2812
 
2203
2813
  // End the call with failed status after TTS plays
2204
2814
  setTimeout(() => {
2205
- this.endSession('Verification failed');
2815
+ this.endSession("Verification failed");
2206
2816
  }, getTtsPlaybackDelayMs());
2207
2817
  } else {
2208
2818
  // Allow another attempt
2209
2819
  log.info(
2210
- { callSessionId: this.callSessionId, attempt: this.verificationAttempts, maxAttempts: this.verificationMaxAttempts },
2211
- 'Callee verification attempt failed — retrying',
2820
+ {
2821
+ callSessionId: this.callSessionId,
2822
+ attempt: this.verificationAttempts,
2823
+ maxAttempts: this.verificationMaxAttempts,
2824
+ },
2825
+ "Callee verification attempt failed — retrying",
2826
+ );
2827
+ this.sendTextToken(
2828
+ "That code was incorrect. Please try again.",
2829
+ true,
2212
2830
  );
2213
- this.sendTextToken('That code was incorrect. Please try again.', true);
2214
2831
  }
2215
2832
  }
2216
2833
  }
@@ -2220,10 +2837,10 @@ export class RelayConnection {
2220
2837
  private handleError(msg: RelayErrorMessage): void {
2221
2838
  log.error(
2222
2839
  { callSessionId: this.callSessionId, description: msg.description },
2223
- 'ConversationRelay error',
2840
+ "ConversationRelay error",
2224
2841
  );
2225
2842
 
2226
- recordCallEvent(this.callSessionId, 'call_failed', {
2843
+ recordCallEvent(this.callSessionId, "call_failed", {
2227
2844
  error: msg.description,
2228
2845
  });
2229
2846
  }