@vellumai/assistant 0.4.26 → 0.4.29

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