@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -7,7 +7,10 @@
7
7
  */
8
8
 
9
9
  import type { ServerWebSocket } from 'bun';
10
+ import { randomInt } from 'node:crypto';
10
11
  import { getLogger } from '../util/logger.js';
12
+ import { parseJsonSafe } from '../util/json.js';
13
+ import { getConfig } from '../config/loader.js';
11
14
  import {
12
15
  getCallSession,
13
16
  updateCallSession,
@@ -16,12 +19,24 @@ import {
16
19
  } from './call-store.js';
17
20
  import { CallOrchestrator } from './call-orchestrator.js';
18
21
  import { fireCallTranscriptNotifier, fireCallCompletionNotifier } from './call-state.js';
22
+ import { addPointerMessage, formatDuration } from './call-pointer-messages.js';
23
+ import { persistCallCompletionMessage } from './call-conversation-messages.js';
24
+ import * as conversationStore from '../memory/conversation-store.js';
19
25
  import {
20
26
  extractPromptSpeakerMetadata,
21
27
  SpeakerIdentityTracker,
22
28
  type PromptSpeakerContext,
23
29
  } from './speaker-identification.js';
24
30
  import { isTerminalState } from './call-state-machine.js';
31
+ import {
32
+ getPendingChallenge,
33
+ validateAndConsumeChallenge,
34
+ } from '../runtime/channel-guardian-service.js';
35
+ import {
36
+ resolveGuardianContext,
37
+ toGuardianRuntimeContext,
38
+ } from '../runtime/guardian-context-resolver.js';
39
+ import { normalizeAssistantId } from '../util/platform.js';
25
40
 
26
41
  const log = getLogger('relay-server');
27
42
 
@@ -105,11 +120,21 @@ export interface RelayWebSocketData {
105
120
  /** Active relay connections keyed by callSessionId. */
106
121
  export const activeRelayConnections = new Map<string, RelayConnection>();
107
122
 
123
+ /** Module-level broadcast function, set by the HTTP server during startup. */
124
+ let globalBroadcast: ((msg: import('../daemon/ipc-contract.js').ServerMessage) => void) | undefined;
125
+
126
+ /** Register a broadcast function so RelayConnection can forward IPC events. */
127
+ export function setRelayBroadcast(fn: (msg: import('../daemon/ipc-contract.js').ServerMessage) => void): void {
128
+ globalBroadcast = fn;
129
+ }
130
+
108
131
  // ── RelayConnection ──────────────────────────────────────────────────
109
132
 
110
133
  /**
111
134
  * Manages a single WebSocket connection for one call.
112
135
  */
136
+ export type RelayConnectionState = 'connected' | 'verification_pending';
137
+
113
138
  export class RelayConnection {
114
139
  private ws: ServerWebSocket<RelayWebSocketData>;
115
140
  private callSessionId: string;
@@ -123,6 +148,19 @@ export class RelayConnection {
123
148
  private orchestrator: CallOrchestrator | null = null;
124
149
  private speakerIdentityTracker: SpeakerIdentityTracker;
125
150
 
151
+ // Verification state (outbound callee verification)
152
+ private connectionState: RelayConnectionState = 'connected';
153
+ private verificationCode: string | null = null;
154
+ private verificationAttempts = 0;
155
+ private verificationMaxAttempts = 3;
156
+ private verificationCodeLength = 6;
157
+ private dtmfBuffer = '';
158
+
159
+ // Inbound voice guardian verification state
160
+ private guardianVerificationActive = false;
161
+ private guardianChallengeAssistantId: string | null = null;
162
+ private guardianVerificationFromNumber: string | null = null;
163
+
126
164
  constructor(ws: ServerWebSocket<RelayWebSocketData>, callSessionId: string) {
127
165
  this.ws = ws;
128
166
  this.callSessionId = callSessionId;
@@ -131,14 +169,33 @@ export class RelayConnection {
131
169
  this.speakerIdentityTracker = new SpeakerIdentityTracker();
132
170
  }
133
171
 
172
+ /**
173
+ * Get the verification code for this connection (if verification is active).
174
+ */
175
+ getVerificationCode(): string | null {
176
+ return this.verificationCode;
177
+ }
178
+
179
+ /**
180
+ * Whether inbound guardian voice verification is currently active.
181
+ */
182
+ isGuardianVerificationActive(): boolean {
183
+ return this.guardianVerificationActive;
184
+ }
185
+
186
+ /**
187
+ * Get the current connection state.
188
+ */
189
+ getConnectionState(): RelayConnectionState {
190
+ return this.connectionState;
191
+ }
192
+
134
193
  /**
135
194
  * Handle an inbound message from Twilio via the ConversationRelay WebSocket.
136
195
  */
137
196
  async handleMessage(data: string): Promise<void> {
138
- let parsed: RelayInboundMessage;
139
- try {
140
- parsed = JSON.parse(data) as RelayInboundMessage;
141
- } catch {
197
+ const parsed = parseJsonSafe<RelayInboundMessage>(data);
198
+ if (!parsed) {
142
199
  log.warn({ callSessionId: this.callSessionId, data }, 'Failed to parse relay message');
143
200
  return;
144
201
  }
@@ -160,7 +217,7 @@ export class RelayConnection {
160
217
  this.handleError(parsed);
161
218
  break;
162
219
  default:
163
- log.warn({ callSessionId: this.callSessionId, type: (parsed as Record<string, unknown>).type }, 'Unknown relay message type');
220
+ log.warn({ callSessionId: this.callSessionId, type: (parsed as { type: unknown }).type }, 'Unknown relay message type');
164
221
  }
165
222
  }
166
223
 
@@ -252,6 +309,14 @@ export class RelayConnection {
252
309
  reason: reason || 'relay_closed',
253
310
  closeCode: code,
254
311
  });
312
+
313
+ // Post a pointer message in the initiating conversation
314
+ if (session.initiatedFromConversationId) {
315
+ const durationMs = session.startedAt ? Date.now() - session.startedAt : 0;
316
+ addPointerMessage(session.initiatedFromConversationId, 'completed', session.toNumber, {
317
+ duration: durationMs > 0 ? formatDuration(durationMs) : undefined,
318
+ });
319
+ }
255
320
  } else {
256
321
  const detail = reason || (code ? `relay_closed_${code}` : 'relay_closed_abnormal');
257
322
  updateCallSession(this.callSessionId, {
@@ -263,9 +328,17 @@ export class RelayConnection {
263
328
  reason: detail,
264
329
  closeCode: code,
265
330
  });
331
+
332
+ // Post a failure pointer message in the initiating conversation
333
+ if (session.initiatedFromConversationId) {
334
+ addPointerMessage(session.initiatedFromConversationId, 'failed', session.toNumber, {
335
+ reason: detail,
336
+ });
337
+ }
266
338
  }
267
339
 
268
340
  expirePendingQuestions(this.callSessionId);
341
+ persistCallCompletionMessage(session.conversationId, this.callSessionId);
269
342
  fireCallCompletionNotifier(session.conversationId, this.callSessionId);
270
343
  }
271
344
 
@@ -299,9 +372,272 @@ export class RelayConnection {
299
372
  customParameters: msg.customParameters,
300
373
  });
301
374
 
302
- // Create and attach the LLM-driven orchestrator
303
- const orchestrator = new CallOrchestrator(this.callSessionId, this, session?.task ?? null);
375
+ // Inbound calls skip callee verification — verification is an
376
+ // outbound-call concern where we need to confirm the callee's identity.
377
+ // We use initiatedFromConversationId rather than task == null because
378
+ // outbound calls always have an initiating conversation, while inbound
379
+ // calls (created via createInboundVoiceSession) never do. Relying on
380
+ // task == null is unreliable: task-less outbound sessions would
381
+ // incorrectly bypass outbound verification.
382
+ const assistantId = normalizeAssistantId(session?.assistantId ?? 'self');
383
+ const isInbound = session?.initiatedFromConversationId == null;
384
+
385
+ // Create and attach the LLM-driven orchestrator. For inbound voice,
386
+ // seed guardian actor context from caller identity + active binding so
387
+ // first-turn behavior matches channel ingress semantics.
388
+ const initialGuardianContext = isInbound
389
+ ? toGuardianRuntimeContext(
390
+ 'voice',
391
+ resolveGuardianContext({
392
+ assistantId,
393
+ sourceChannel: 'voice',
394
+ externalChatId: msg.from,
395
+ senderExternalUserId: msg.from || undefined,
396
+ }),
397
+ )
398
+ : undefined;
399
+
400
+ const orchestrator = new CallOrchestrator(this.callSessionId, this, session?.task ?? null, {
401
+ broadcast: globalBroadcast,
402
+ assistantId,
403
+ guardianContext: initialGuardianContext,
404
+ });
304
405
  this.setOrchestrator(orchestrator);
406
+
407
+ const config = getConfig();
408
+ const verificationConfig = config.calls.verification;
409
+ if (!isInbound && verificationConfig.enabled) {
410
+ this.startVerification(session, verificationConfig);
411
+ } else if (isInbound) {
412
+ // For inbound calls, check if there's a pending voice guardian
413
+ // challenge that the caller needs to complete before proceeding.
414
+ const pendingChallenge = getPendingChallenge(assistantId, 'voice');
415
+
416
+ if (pendingChallenge) {
417
+ this.startInboundGuardianVerification(assistantId, msg.from);
418
+ } else {
419
+ this.startNormalCallFlow(orchestrator, true);
420
+ }
421
+ } else {
422
+ this.startNormalCallFlow(orchestrator, false);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Generate a verification code and prompt the callee to enter it via DTMF.
428
+ */
429
+ private startVerification(
430
+ session: ReturnType<typeof getCallSession>,
431
+ verificationConfig: { maxAttempts: number; codeLength: number },
432
+ ): void {
433
+ this.verificationMaxAttempts = verificationConfig.maxAttempts;
434
+ this.verificationCodeLength = verificationConfig.codeLength;
435
+ this.verificationAttempts = 0;
436
+ this.dtmfBuffer = '';
437
+
438
+ // Generate a random numeric code
439
+ const maxValue = Math.pow(10, this.verificationCodeLength);
440
+ const code = randomInt(0, maxValue).toString().padStart(this.verificationCodeLength, '0');
441
+ this.verificationCode = code;
442
+ this.connectionState = 'verification_pending';
443
+
444
+ recordCallEvent(this.callSessionId, 'callee_verification_started', {
445
+ codeLength: this.verificationCodeLength,
446
+ maxAttempts: this.verificationMaxAttempts,
447
+ });
448
+
449
+ // Send a TTS prompt with the code spoken digit by digit
450
+ const spokenCode = code.split('').join('. ');
451
+ this.sendTextToken(`Please enter the verification code: ${spokenCode}.`, true);
452
+
453
+ // Post the verification code to the initiating conversation so the
454
+ // guardian (user) can share it with the callee.
455
+ if (session?.initiatedFromConversationId) {
456
+ const codeMsg = `\u{1F510} Verification code for call to ${session.toNumber}: ${code}`;
457
+ conversationStore.addMessage(
458
+ session.initiatedFromConversationId,
459
+ 'assistant',
460
+ JSON.stringify([{ type: 'text', text: codeMsg }]),
461
+ { userMessageChannel: 'voice', assistantMessageChannel: 'voice' },
462
+ );
463
+ }
464
+
465
+ log.info(
466
+ { callSessionId: this.callSessionId, codeLength: this.verificationCodeLength },
467
+ 'Callee verification started',
468
+ );
469
+ }
470
+
471
+ /**
472
+ * Start normal call flow — fire the orchestrator greeting unless a
473
+ * static welcome greeting is configured.
474
+ */
475
+ private startNormalCallFlow(orchestrator: CallOrchestrator, isInbound: boolean): void {
476
+ const hasStaticGreeting = !!process.env.CALL_WELCOME_GREETING?.trim();
477
+ if (!hasStaticGreeting) {
478
+ orchestrator.startInitialGreeting().catch((err) =>
479
+ log.error({ err, callSessionId: this.callSessionId }, `Failed to start initial ${isInbound ? 'inbound' : 'outbound'} greeting`),
480
+ );
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Enter verification-pending state for an inbound call with a pending
486
+ * voice guardian challenge. Prompts the caller to enter their six-digit
487
+ * verification code via DTMF or by speaking it.
488
+ */
489
+ private startInboundGuardianVerification(assistantId: string, fromNumber: string): void {
490
+ this.guardianVerificationActive = true;
491
+ this.guardianChallengeAssistantId = assistantId;
492
+ this.guardianVerificationFromNumber = fromNumber;
493
+ this.connectionState = 'verification_pending';
494
+ this.verificationAttempts = 0;
495
+ this.verificationMaxAttempts = 3;
496
+ this.verificationCodeLength = 6;
497
+ this.dtmfBuffer = '';
498
+
499
+ recordCallEvent(this.callSessionId, 'guardian_voice_verification_started', {
500
+ assistantId,
501
+ maxAttempts: this.verificationMaxAttempts,
502
+ });
503
+
504
+ this.sendTextToken(
505
+ 'Welcome. Please enter your six-digit verification code using your keypad, or speak the digits now.',
506
+ true,
507
+ );
508
+
509
+ log.info(
510
+ { callSessionId: this.callSessionId, assistantId },
511
+ 'Inbound guardian voice verification started',
512
+ );
513
+ }
514
+
515
+ /**
516
+ * Extract digit characters from a speech transcript. Recognizes both
517
+ * raw digit characters ("1 2 3") and spoken number words ("one two three").
518
+ */
519
+ private static parseDigitsFromSpeech(transcript: string): string {
520
+ const wordToDigit: Record<string, string> = {
521
+ zero: '0', oh: '0', o: '0',
522
+ one: '1', won: '1',
523
+ two: '2', too: '2', to: '2',
524
+ three: '3',
525
+ four: '4', for: '4', fore: '4',
526
+ five: '5',
527
+ six: '6',
528
+ seven: '7',
529
+ eight: '8', ate: '8',
530
+ nine: '9',
531
+ };
532
+
533
+ const digits: string[] = [];
534
+ const lower = transcript.toLowerCase();
535
+
536
+ // Split on whitespace and non-alphanumeric boundaries
537
+ const tokens = lower.split(/[\s,.\-;:!?]+/);
538
+ for (const token of tokens) {
539
+ if (/^\d$/.test(token)) {
540
+ digits.push(token);
541
+ } else if (wordToDigit[token]) {
542
+ digits.push(wordToDigit[token]);
543
+ } else if (/^\d+$/.test(token)) {
544
+ // Multi-digit number like "123456" — split into individual digits
545
+ digits.push(...token.split(''));
546
+ }
547
+ }
548
+
549
+ return digits.join('');
550
+ }
551
+
552
+ /**
553
+ * Attempt to validate an entered code against the pending voice guardian
554
+ * challenge via validateAndConsumeChallenge. On success, binds the
555
+ * guardian and transitions to normal call flow. On failure, enforces
556
+ * max attempts and terminates the call if exhausted.
557
+ */
558
+ private attemptGuardianCodeVerification(enteredCode: string): void {
559
+ if (!this.guardianChallengeAssistantId || !this.guardianVerificationFromNumber) {
560
+ return;
561
+ }
562
+
563
+ const result = validateAndConsumeChallenge(
564
+ this.guardianChallengeAssistantId,
565
+ 'voice',
566
+ enteredCode,
567
+ this.guardianVerificationFromNumber,
568
+ this.guardianVerificationFromNumber,
569
+ );
570
+
571
+ if (result.success) {
572
+ // Guardian binding was created by validateAndConsumeChallenge
573
+ this.connectionState = 'connected';
574
+ this.guardianVerificationActive = false;
575
+ this.verificationAttempts = 0;
576
+ this.dtmfBuffer = '';
577
+
578
+ recordCallEvent(this.callSessionId, 'guardian_voice_verification_succeeded', {
579
+ bindingId: result.bindingId,
580
+ });
581
+ log.info({ callSessionId: this.callSessionId }, 'Inbound guardian voice verification succeeded');
582
+
583
+ // Proceed to normal call flow (use startNormalCallFlow to respect
584
+ // the CALL_WELCOME_GREETING static greeting guard)
585
+ if (this.orchestrator) {
586
+ this.orchestrator.setGuardianContext(
587
+ toGuardianRuntimeContext(
588
+ 'voice',
589
+ resolveGuardianContext({
590
+ assistantId: this.guardianChallengeAssistantId,
591
+ sourceChannel: 'voice',
592
+ externalChatId: this.guardianVerificationFromNumber,
593
+ senderExternalUserId: this.guardianVerificationFromNumber,
594
+ }),
595
+ ),
596
+ );
597
+ this.startNormalCallFlow(this.orchestrator, true);
598
+ }
599
+ } else {
600
+ this.verificationAttempts++;
601
+
602
+ if (this.verificationAttempts >= this.verificationMaxAttempts) {
603
+ // Immediately deactivate verification so DTMF/speech input during
604
+ // the goodbye window doesn't trigger more verification attempts.
605
+ this.guardianVerificationActive = false;
606
+
607
+ recordCallEvent(this.callSessionId, 'guardian_voice_verification_failed', {
608
+ attempts: this.verificationAttempts,
609
+ });
610
+ log.warn(
611
+ { callSessionId: this.callSessionId, attempts: this.verificationAttempts },
612
+ 'Inbound guardian voice verification failed — max attempts reached',
613
+ );
614
+
615
+ this.sendTextToken('Verification failed. Goodbye.', true);
616
+
617
+ updateCallSession(this.callSessionId, {
618
+ status: 'failed',
619
+ endedAt: Date.now(),
620
+ lastError: 'Guardian voice verification failed — max attempts exceeded',
621
+ });
622
+
623
+ const session = getCallSession(this.callSessionId);
624
+ if (session) {
625
+ expirePendingQuestions(this.callSessionId);
626
+ persistCallCompletionMessage(session.conversationId, this.callSessionId);
627
+ fireCallCompletionNotifier(session.conversationId, this.callSessionId);
628
+ }
629
+
630
+ setTimeout(() => {
631
+ this.endSession('Guardian verification failed');
632
+ }, 2000);
633
+ } else {
634
+ log.info(
635
+ { callSessionId: this.callSessionId, attempt: this.verificationAttempts, maxAttempts: this.verificationMaxAttempts },
636
+ 'Inbound guardian voice verification attempt failed — retrying',
637
+ );
638
+ this.sendTextToken('That code was incorrect. Please try again.', true);
639
+ }
640
+ }
305
641
  }
306
642
 
307
643
  private async handlePrompt(msg: RelayPromptMessage): Promise<void> {
@@ -310,12 +646,41 @@ export class RelayConnection {
310
646
  return;
311
647
  }
312
648
 
649
+ // During inbound guardian verification, attempt to parse spoken digits
650
+ // from the transcript and validate them.
651
+ if (this.connectionState === 'verification_pending' && this.guardianVerificationActive) {
652
+ const spokenDigits = RelayConnection.parseDigitsFromSpeech(msg.voicePrompt);
653
+ log.info(
654
+ { callSessionId: this.callSessionId, transcript: msg.voicePrompt, spokenDigits },
655
+ 'Speech received during guardian voice verification',
656
+ );
657
+ if (spokenDigits.length >= this.verificationCodeLength) {
658
+ const enteredCode = spokenDigits.slice(0, this.verificationCodeLength);
659
+ this.attemptGuardianCodeVerification(enteredCode);
660
+ } else if (spokenDigits.length > 0) {
661
+ this.sendTextToken(
662
+ `I heard ${spokenDigits.length} digits. Please enter all ${this.verificationCodeLength} digits of your code.`,
663
+ true,
664
+ );
665
+ }
666
+ return;
667
+ }
668
+
669
+ // During outbound callee verification, ignore voice prompts — the callee
670
+ // should be entering DTMF digits, not speaking.
671
+ if (this.connectionState === 'verification_pending') {
672
+ log.debug({ callSessionId: this.callSessionId }, 'Ignoring voice prompt during callee verification');
673
+ return;
674
+ }
675
+
313
676
  log.info(
314
677
  { callSessionId: this.callSessionId, transcript: msg.voicePrompt, lang: msg.lang },
315
678
  'Caller transcript received (final)',
316
679
  );
317
680
 
318
- const speakerMetadata = extractPromptSpeakerMetadata(msg as unknown as Record<string, unknown>);
681
+ // Spread to widen the typed message into a plain record — extractPromptSpeakerMetadata
682
+ // probes for snake_case and nested property variants not on RelayPromptMessage.
683
+ const speakerMetadata = extractPromptSpeakerMetadata({ ...msg });
319
684
  const speaker = this.speakerIdentityTracker.identifySpeaker(speakerMetadata);
320
685
 
321
686
  // Record in conversation history
@@ -338,6 +703,14 @@ export class RelayConnection {
338
703
 
339
704
  const session = getCallSession(this.callSessionId);
340
705
  if (session) {
706
+ // Persist caller transcript to the voice conversation so it survives
707
+ // even when no live daemon Session is listening.
708
+ conversationStore.addMessage(
709
+ session.conversationId,
710
+ 'user',
711
+ JSON.stringify([{ type: 'text', text: msg.voicePrompt }]),
712
+ { userMessageChannel: 'voice', assistantMessageChannel: 'voice' },
713
+ );
341
714
  fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'caller', msg.voicePrompt);
342
715
  }
343
716
 
@@ -375,6 +748,91 @@ export class RelayConnection {
375
748
  recordCallEvent(this.callSessionId, 'caller_spoke', {
376
749
  dtmfDigit: msg.digit,
377
750
  });
751
+
752
+ // If inbound guardian verification is pending, accumulate digits and
753
+ // validate against the challenge via the guardian service.
754
+ if (this.connectionState === 'verification_pending' && this.guardianVerificationActive) {
755
+ this.dtmfBuffer += msg.digit;
756
+
757
+ if (this.dtmfBuffer.length >= this.verificationCodeLength) {
758
+ const enteredCode = this.dtmfBuffer.slice(0, this.verificationCodeLength);
759
+ this.dtmfBuffer = '';
760
+ this.attemptGuardianCodeVerification(enteredCode);
761
+ }
762
+ return;
763
+ }
764
+
765
+ // If outbound callee verification is pending, accumulate digits and check the code
766
+ if (this.connectionState === 'verification_pending' && this.verificationCode) {
767
+ this.dtmfBuffer += msg.digit;
768
+
769
+ if (this.dtmfBuffer.length >= this.verificationCodeLength) {
770
+ const enteredCode = this.dtmfBuffer.slice(0, this.verificationCodeLength);
771
+ this.dtmfBuffer = '';
772
+
773
+ if (enteredCode === this.verificationCode) {
774
+ // Verification succeeded
775
+ this.connectionState = 'connected';
776
+ this.verificationCode = null;
777
+ this.verificationAttempts = 0;
778
+
779
+ recordCallEvent(this.callSessionId, 'callee_verification_succeeded', {});
780
+ log.info({ callSessionId: this.callSessionId }, 'Callee verification succeeded');
781
+
782
+ // Proceed to the normal call flow
783
+ if (this.orchestrator) {
784
+ this.orchestrator.startInitialGreeting().catch((err) =>
785
+ log.error({ err, callSessionId: this.callSessionId }, 'Failed to start initial outbound greeting after verification'),
786
+ );
787
+ }
788
+ } else {
789
+ // Verification failed for this attempt
790
+ this.verificationAttempts++;
791
+
792
+ if (this.verificationAttempts >= this.verificationMaxAttempts) {
793
+ // Max attempts reached — end the call
794
+ recordCallEvent(this.callSessionId, 'callee_verification_failed', {
795
+ attempts: this.verificationAttempts,
796
+ });
797
+ log.warn({ callSessionId: this.callSessionId, attempts: this.verificationAttempts }, 'Callee verification failed — max attempts reached');
798
+
799
+ this.sendTextToken('Verification failed. Goodbye.', true);
800
+
801
+ // Mark failed immediately so a relay close during the goodbye TTS
802
+ // window cannot race this into a terminal "completed" status.
803
+ updateCallSession(this.callSessionId, {
804
+ status: 'failed',
805
+ endedAt: Date.now(),
806
+ lastError: 'Callee verification failed — max attempts exceeded',
807
+ });
808
+
809
+ const session = getCallSession(this.callSessionId);
810
+ if (session) {
811
+ expirePendingQuestions(this.callSessionId);
812
+ persistCallCompletionMessage(session.conversationId, this.callSessionId);
813
+ fireCallCompletionNotifier(session.conversationId, this.callSessionId);
814
+ if (session.initiatedFromConversationId) {
815
+ addPointerMessage(session.initiatedFromConversationId, 'failed', session.toNumber, {
816
+ reason: 'Callee verification failed',
817
+ });
818
+ }
819
+ }
820
+
821
+ // End the call with failed status after TTS plays
822
+ setTimeout(() => {
823
+ this.endSession('Verification failed');
824
+ }, 2000);
825
+ } else {
826
+ // Allow another attempt
827
+ log.info(
828
+ { callSessionId: this.callSessionId, attempt: this.verificationAttempts, maxAttempts: this.verificationMaxAttempts },
829
+ 'Callee verification attempt failed — retrying',
830
+ );
831
+ this.sendTextToken('That code was incorrect. Please try again.', true);
832
+ }
833
+ }
834
+ }
835
+ }
378
836
  }
379
837
 
380
838
  private handleError(msg: RelayErrorMessage): void {
@@ -66,7 +66,7 @@ export function extractPromptSpeakerMetadata(message: Record<string, unknown>):
66
66
  const pickNumber = (...values: unknown[]): number | undefined => {
67
67
  for (const value of values) {
68
68
  const parsed = toNumber(value);
69
- if (parsed !== null) return parsed;
69
+ if (parsed != null) return parsed;
70
70
  }
71
71
  return undefined;
72
72
  };
@@ -2,6 +2,8 @@ import { getSecureKey } from '../security/secure-keys.js';
2
2
  import { getLogger } from '../util/logger.js';
3
3
  import { loadConfig } from '../config/loader.js';
4
4
  import { getPublicBaseUrl, getTwilioRelayUrl } from '../inbound/public-ingress-urls.js';
5
+ import { ConfigError } from '../util/errors.js';
6
+ import { getTwilioPhoneNumberEnv, getTwilioWssBaseUrl } from '../config/env.js';
5
7
 
6
8
  const log = getLogger('twilio-config');
7
9
 
@@ -13,27 +15,33 @@ export interface TwilioConfig {
13
15
  wssBaseUrl: string;
14
16
  }
15
17
 
16
- export function getTwilioConfig(): TwilioConfig {
17
- const accountSid = getSecureKey('credential:twilio:account_sid');
18
- const authToken = getSecureKey('credential:twilio:auth_token');
19
- const config = loadConfig();
18
+ function resolveTwilioPhoneNumber(assistantId: string | undefined, config: ReturnType<typeof loadConfig>): string {
19
+ if (assistantId) {
20
+ const assistantPhone = config.sms?.assistantPhoneNumbers?.[assistantId];
21
+ if (assistantPhone) {
22
+ return assistantPhone;
23
+ }
24
+ }
20
25
 
21
- // Phone number resolution priority:
26
+ // Global fallback order:
22
27
  // 1. TWILIO_PHONE_NUMBER env var (explicit override)
23
28
  // 2. config file sms.phoneNumber (primary storage)
24
29
  // 3. credential:twilio:phone_number secure key (backward-compat fallback)
25
- const phoneNumber =
26
- process.env.TWILIO_PHONE_NUMBER ||
27
- config.sms?.phoneNumber ||
28
- getSecureKey('credential:twilio:phone_number') ||
29
- '';
30
+ return getTwilioPhoneNumberEnv() || config.sms?.phoneNumber || getSecureKey('credential:twilio:phone_number') || '';
31
+ }
32
+
33
+ export function getTwilioConfig(assistantId?: string): TwilioConfig {
34
+ const accountSid = getSecureKey('credential:twilio:account_sid');
35
+ const authToken = getSecureKey('credential:twilio:auth_token');
36
+ const config = loadConfig();
37
+ const phoneNumber = resolveTwilioPhoneNumber(assistantId, config);
30
38
  const webhookBaseUrl = getPublicBaseUrl(config);
31
39
 
32
40
  // Always use the centralized relay URL derived from the public ingress base URL.
33
41
  // TWILIO_WSS_BASE_URL is ignored.
34
42
  let wssBaseUrl: string;
35
- if (process.env.TWILIO_WSS_BASE_URL) {
36
- log.warn('TWILIO_WSS_BASE_URL env var is ignored. Relay URL is derived from ingress.publicBaseUrl.');
43
+ if (getTwilioWssBaseUrl()) {
44
+ log.warn('TWILIO_WSS_BASE_URL env var is deprecated. Relay URL is derived from ingress.publicBaseUrl.');
37
45
  }
38
46
  try {
39
47
  wssBaseUrl = getTwilioRelayUrl(config);
@@ -42,10 +50,10 @@ export function getTwilioConfig(): TwilioConfig {
42
50
  }
43
51
 
44
52
  if (!accountSid || !authToken) {
45
- throw new Error('Twilio credentials not configured. Set credential:twilio:account_sid and credential:twilio:auth_token via the credential_store tool.');
53
+ throw new ConfigError('Twilio credentials not configured. Set credential:twilio:account_sid and credential:twilio:auth_token via the credential_store tool.');
46
54
  }
47
55
  if (!phoneNumber) {
48
- throw new Error('TWILIO_PHONE_NUMBER not configured.');
56
+ throw new ConfigError('Twilio phone number not configured.');
49
57
  }
50
58
 
51
59
  log.debug('Twilio config loaded successfully');