@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
@@ -17,7 +17,6 @@ import { describe, test, expect, beforeEach, afterAll, mock, type Mock } from 'b
17
17
  import { mkdtempSync, rmSync } from 'node:fs';
18
18
  import { tmpdir } from 'node:os';
19
19
  import { join } from 'node:path';
20
- import { EventEmitter } from 'node:events';
21
20
 
22
21
  const testDir = mkdtempSync(join(tmpdir(), 'relay-server-test-'));
23
22
 
@@ -33,6 +32,7 @@ mock.module('../util/platform.js', () => ({
33
32
  getDbPath: () => join(testDir, 'test.db'),
34
33
  getLogPath: () => join(testDir, 'test.log'),
35
34
  ensureDataDir: () => {},
35
+ readHttpToken: () => null,
36
36
  }));
37
37
 
38
38
  mock.module('../util/logger.js', () => ({
@@ -44,53 +44,75 @@ mock.module('../util/logger.js', () => ({
44
44
 
45
45
  // ── Config mock ─────────────────────────────────────────────────────
46
46
 
47
- mock.module('../config/loader.js', () => ({
48
- getConfig: () => ({
49
- apiKeys: { anthropic: 'test-key' },
50
- calls: {
51
- enabled: true,
52
- provider: 'twilio',
53
- maxDurationSeconds: 3600,
54
- userConsultTimeoutSeconds: 120,
55
- disclosure: { enabled: false, text: '' },
56
- safety: { denyCategories: [] },
47
+ const mockConfig = {
48
+ provider: 'anthropic',
49
+ providerOrder: ['anthropic'],
50
+ apiKeys: { anthropic: 'test-key' },
51
+ calls: {
52
+ enabled: true,
53
+ provider: 'twilio',
54
+ maxDurationSeconds: 3600,
55
+ userConsultTimeoutSeconds: 120,
56
+ disclosure: { enabled: false, text: '' },
57
+ safety: { denyCategories: [] },
58
+ verification: {
59
+ enabled: false,
60
+ maxAttempts: 3,
61
+ codeLength: 6,
57
62
  },
58
- }),
63
+ },
64
+ memory: { enabled: false },
65
+ };
66
+
67
+ mock.module('../config/loader.js', () => ({
68
+ getConfig: () => mockConfig,
59
69
  }));
60
70
 
61
- // ── Anthropic SDK mock ──────────────────────────────────────────────
71
+ // ── Helpers for building mock provider responses ────────────────────
62
72
 
63
- function createMockStream(tokens: string[]) {
64
- const emitter = new EventEmitter();
73
+ function createMockProviderResponse(tokens: string[]) {
65
74
  const fullText = tokens.join('');
66
-
67
- const stream = {
68
- on: (event: string, handler: (...args: unknown[]) => void) => {
69
- emitter.on(event, handler);
70
- return stream;
71
- },
72
- finalMessage: () => {
73
- for (const token of tokens) {
74
- emitter.emit('text', token);
75
- }
76
- return Promise.resolve({
77
- content: [{ type: 'text', text: fullText }],
78
- });
79
- },
75
+ return async (
76
+ _messages: unknown[],
77
+ _tools: unknown[],
78
+ _systemPrompt: string,
79
+ options?: { onEvent?: (event: { type: string; text?: string }) => void; signal?: AbortSignal },
80
+ ) => {
81
+ for (const token of tokens) {
82
+ options?.onEvent?.({ type: 'text_delta', text: token });
83
+ }
84
+ return {
85
+ content: [{ type: 'text', text: fullText }],
86
+ model: 'claude-sonnet-4-20250514',
87
+ usage: { inputTokens: 100, outputTokens: 50 },
88
+ stopReason: 'end_turn',
89
+ };
80
90
  };
81
-
82
- return stream;
83
91
  }
84
92
 
85
- let mockStreamFn: Mock<(...args: unknown[]) => unknown>;
93
+ // ── Provider registry mock ──────────────────────────────────────────
86
94
 
87
- mock.module('@anthropic-ai/sdk', () => {
88
- mockStreamFn = mock((..._args: unknown[]) => createMockStream(['Hello']));
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ let mockSendMessage: Mock<any>;
97
+
98
+ mock.module('../providers/registry.js', () => {
99
+ mockSendMessage = mock(createMockProviderResponse(['Hello']));
89
100
  return {
90
- default: class MockAnthropic {
91
- messages = {
92
- stream: (...args: unknown[]) => mockStreamFn(...args),
101
+ listProviders: () => ['anthropic'],
102
+ getFailoverProvider: () => ({
103
+ name: 'anthropic',
104
+ sendMessage: (...args: unknown[]) => mockSendMessage(...args),
105
+ }),
106
+ getDefaultModel: (providerName: string) => {
107
+ const defaults: Record<string, string> = {
108
+ anthropic: 'claude-opus-4-6',
109
+ openai: 'gpt-5.2',
110
+ gemini: 'gemini-3-flash',
111
+ ollama: 'llama3.2',
112
+ fireworks: 'accounts/fireworks/models/kimi-k2p5',
113
+ openrouter: 'x-ai/grok-4',
93
114
  };
115
+ return defaults[providerName] ?? defaults.anthropic;
94
116
  },
95
117
  };
96
118
  });
@@ -104,9 +126,15 @@ import {
104
126
  getCallSession,
105
127
  getCallEvents,
106
128
  } from '../calls/call-store.js';
129
+ import { getMessages } from '../memory/conversation-store.js';
107
130
  import { registerCallCompletionNotifier, unregisterCallCompletionNotifier } from '../calls/call-state.js';
108
131
  import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
109
132
  import type { RelayWebSocketData } from '../calls/relay-server.js';
133
+ import {
134
+ createVerificationChallenge,
135
+ getGuardianBinding,
136
+ } from '../runtime/channel-guardian-service.js';
137
+ import { createBinding } from '../memory/channel-guardian-store.js';
110
138
 
111
139
  initializeDb();
112
140
 
@@ -159,18 +187,48 @@ function ensureConversation(id: string): void {
159
187
 
160
188
  function resetTables() {
161
189
  const db = getDb();
190
+ db.run('DELETE FROM guardian_action_deliveries');
191
+ db.run('DELETE FROM guardian_action_requests');
162
192
  db.run('DELETE FROM call_pending_questions');
163
193
  db.run('DELETE FROM call_events');
164
194
  db.run('DELETE FROM call_sessions');
195
+ db.run('DELETE FROM tool_invocations');
196
+ db.run('DELETE FROM messages');
165
197
  db.run('DELETE FROM conversations');
198
+ db.run('DELETE FROM channel_guardian_verification_challenges');
199
+ db.run('DELETE FROM channel_guardian_bindings');
200
+ db.run('DELETE FROM channel_guardian_rate_limits');
166
201
  ensuredConvIds = new Set();
167
202
  }
168
203
 
204
+ function getLatestAssistantText(conversationId: string): string | null {
205
+ const messages = getMessages(conversationId).filter((m) => m.role === 'assistant');
206
+ if (messages.length === 0) return null;
207
+ const latest = messages[messages.length - 1];
208
+ try {
209
+ const parsed = JSON.parse(latest.content) as unknown;
210
+ if (Array.isArray(parsed)) {
211
+ return parsed
212
+ .filter((block): block is { type: string; text?: string } => typeof block === 'object' && block != null)
213
+ .filter((block) => block.type === 'text')
214
+ .map((block) => block.text ?? '')
215
+ .join('');
216
+ }
217
+ if (typeof parsed === 'string') return parsed;
218
+ } catch {
219
+ // Ignore parse failures and fall back to raw content.
220
+ }
221
+ return latest.content;
222
+ }
223
+
169
224
  describe('relay-server', () => {
170
225
  beforeEach(() => {
171
226
  resetTables();
172
227
  activeRelayConnections.clear();
173
- mockStreamFn.mockImplementation(() => createMockStream(['Hello']));
228
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello']));
229
+ mockConfig.calls.verification.enabled = false;
230
+ mockConfig.calls.verification.maxAttempts = 3;
231
+ mockConfig.calls.verification.codeLength = 6;
174
232
  });
175
233
 
176
234
  // ── Setup message handling ──────────────────────────────────────
@@ -211,6 +269,41 @@ describe('relay-server', () => {
211
269
  relay.destroy();
212
270
  });
213
271
 
272
+ test('handleMessage: setup triggers initial assistant greeting turn', async () => {
273
+ ensureConversation('conv-relay-setup-greet');
274
+ const session = createCallSession({
275
+ conversationId: 'conv-relay-setup-greet',
276
+ provider: 'twilio',
277
+ fromNumber: '+15551111111',
278
+ toNumber: '+15552222222',
279
+ task: 'Confirm appointment time',
280
+ });
281
+
282
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, I am calling to confirm your appointment.']));
283
+
284
+ const { ws, relay } = createMockWs(session.id);
285
+
286
+ await relay.handleMessage(JSON.stringify({
287
+ type: 'setup',
288
+ callSid: 'CA_setup_greet_123',
289
+ from: '+15551111111',
290
+ to: '+15552222222',
291
+ }));
292
+
293
+ await new Promise((resolve) => setTimeout(resolve, 10));
294
+
295
+ const textMessages = ws.sentMessages
296
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string; last?: boolean })
297
+ .filter((m) => m.type === 'text');
298
+ expect(textMessages.some((m) => (m.token ?? '').includes('confirm your appointment'))).toBe(true);
299
+ expect(textMessages.some((m) => m.last === true)).toBe(true);
300
+
301
+ const events = getCallEvents(session.id).filter((e) => e.eventType === 'assistant_spoke');
302
+ expect(events.length).toBeGreaterThan(0);
303
+
304
+ relay.destroy();
305
+ });
306
+
214
307
  test('handleTransportClosed: normal close marks call completed and notifies completion', () => {
215
308
  ensureConversation('conv-relay-close-normal');
216
309
  const session = createCallSession({
@@ -235,6 +328,7 @@ describe('relay-server', () => {
235
328
  const endedEvents = getCallEvents(session.id).filter((e) => e.eventType === 'call_ended');
236
329
  expect(endedEvents.length).toBe(1);
237
330
  expect(completionCount).toBe(1);
331
+ expect(getLatestAssistantText('conv-relay-close-normal')).toContain('**Call completed**');
238
332
 
239
333
  unregisterCallCompletionNotifier('conv-relay-close-normal');
240
334
  relay.destroy();
@@ -259,6 +353,7 @@ describe('relay-server', () => {
259
353
  expect(updated!.lastError).toContain('abnormal closure');
260
354
  const failEvents = getCallEvents(session.id).filter((e) => e.eventType === 'call_failed');
261
355
  expect(failEvents.length).toBe(1);
356
+ expect(getLatestAssistantText('conv-relay-close-abnormal')).toContain('**Call failed**');
262
357
 
263
358
  relay.destroy();
264
359
  });
@@ -285,8 +380,9 @@ describe('relay-server', () => {
285
380
 
286
381
  // Verify event recorded with custom parameters
287
382
  const events = getCallEvents(session.id);
288
- expect(events.length).toBe(1);
289
- const payload = JSON.parse(events[0].payloadJson);
383
+ const connectedEvents = events.filter((e) => e.eventType === 'call_connected');
384
+ expect(connectedEvents.length).toBe(1);
385
+ const payload = JSON.parse(connectedEvents[0].payloadJson);
290
386
  expect(payload.customParameters).toEqual({ taskId: 'task-1', priority: 'high' });
291
387
 
292
388
  relay.destroy();
@@ -359,6 +455,9 @@ describe('relay-server', () => {
359
455
  to: '+15552222222',
360
456
  }));
361
457
 
458
+ // Let any async initial-greeting turn settle so we can compare only
459
+ // the effect of the partial prompt itself.
460
+ await new Promise((resolve) => setTimeout(resolve, 10));
362
461
  const messagesBeforePrompt = ws.sentMessages.length;
363
462
 
364
463
  // Send a partial prompt (last=false)
@@ -467,6 +566,58 @@ describe('relay-server', () => {
467
566
  relay.destroy();
468
567
  });
469
568
 
569
+ test('verification failure remains failed if transport closes during goodbye delay', async () => {
570
+ ensureConversation('conv-relay-verify-race');
571
+ ensureConversation('conv-relay-verify-race-initiator');
572
+ const session = createCallSession({
573
+ conversationId: 'conv-relay-verify-race',
574
+ provider: 'twilio',
575
+ fromNumber: '+15551111111',
576
+ toNumber: '+15552222222',
577
+ initiatedFromConversationId: 'conv-relay-verify-race-initiator',
578
+ });
579
+
580
+ mockConfig.calls.verification.enabled = true;
581
+ mockConfig.calls.verification.maxAttempts = 1;
582
+ mockConfig.calls.verification.codeLength = 1;
583
+
584
+ const { relay } = createMockWs(session.id);
585
+
586
+ await relay.handleMessage(JSON.stringify({
587
+ type: 'setup',
588
+ callSid: 'CA_verify_race_123',
589
+ from: '+15551111111',
590
+ to: '+15552222222',
591
+ }));
592
+
593
+ const verificationCode = relay.getVerificationCode();
594
+ expect(verificationCode).not.toBeNull();
595
+ const wrongDigit = verificationCode === '0' ? '1' : '0';
596
+
597
+ await relay.handleMessage(JSON.stringify({
598
+ type: 'dtmf',
599
+ digit: wrongDigit,
600
+ }));
601
+
602
+ // Simulate the callee hanging up before the delayed endSession executes.
603
+ relay.handleTransportClosed(1000, 'callee hung up');
604
+
605
+ const updated = getCallSession(session.id);
606
+ expect(updated).not.toBeNull();
607
+ expect(updated!.status).toBe('failed');
608
+ expect(updated!.lastError).toContain('max attempts exceeded');
609
+ expect(getLatestAssistantText('conv-relay-verify-race')).toContain('**Call failed**');
610
+
611
+ // Let the delayed endSession callback flush to avoid timer bleed across tests.
612
+ await new Promise((resolve) => setTimeout(resolve, 2100));
613
+
614
+ const finalState = getCallSession(session.id);
615
+ expect(finalState).not.toBeNull();
616
+ expect(finalState!.status).toBe('failed');
617
+
618
+ relay.destroy();
619
+ });
620
+
470
621
  // ── Error handling ──────────────────────────────────────────────
471
622
 
472
623
  test('handleMessage: error message records call_failed event', async () => {
@@ -685,4 +836,592 @@ describe('relay-server', () => {
685
836
  relay.destroy();
686
837
  expect(() => relay.destroy()).not.toThrow();
687
838
  });
839
+
840
+ // ── Inbound call setup ────────────────────────────────────────────
841
+
842
+ test('handleMessage: inbound call (no task) triggers greeting without verification', async () => {
843
+ ensureConversation('conv-relay-inbound-greet');
844
+ // Inbound sessions have no task and no initiatedFromConversationId
845
+ const session = createCallSession({
846
+ conversationId: 'conv-relay-inbound-greet',
847
+ provider: 'twilio',
848
+ fromNumber: '+15559999999',
849
+ toNumber: '+15551111111',
850
+ // no task — inbound call
851
+ });
852
+
853
+ // Enable verification to prove inbound calls skip it
854
+ mockConfig.calls.verification.enabled = true;
855
+
856
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, how can I help you today?']));
857
+
858
+ const { ws, relay } = createMockWs(session.id);
859
+
860
+ await relay.handleMessage(JSON.stringify({
861
+ type: 'setup',
862
+ callSid: 'CA_inbound_greet_123',
863
+ from: '+15559999999',
864
+ to: '+15551111111',
865
+ }));
866
+
867
+ await new Promise((resolve) => setTimeout(resolve, 10));
868
+
869
+ // Should NOT have started verification (no verification code prompt)
870
+ expect(relay.getVerificationCode()).toBeNull();
871
+
872
+ // Should have generated a greeting via the orchestrator
873
+ const textMessages = ws.sentMessages
874
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string; last?: boolean })
875
+ .filter((m) => m.type === 'text');
876
+ expect(textMessages.some((m) => (m.token ?? '').includes('how can I help'))).toBe(true);
877
+ expect(textMessages.some((m) => m.last === true)).toBe(true);
878
+
879
+ relay.destroy();
880
+ });
881
+
882
+ test('handleMessage: inbound call persists caller transcript to voice conversation', async () => {
883
+ ensureConversation('conv-relay-inbound-persist');
884
+ const session = createCallSession({
885
+ conversationId: 'conv-relay-inbound-persist',
886
+ provider: 'twilio',
887
+ fromNumber: '+15559999999',
888
+ toNumber: '+15551111111',
889
+ // no task — inbound call
890
+ });
891
+
892
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Sure, let me help with that.']));
893
+
894
+ const { relay } = createMockWs(session.id);
895
+
896
+ await relay.handleMessage(JSON.stringify({
897
+ type: 'setup',
898
+ callSid: 'CA_inbound_persist_123',
899
+ from: '+15559999999',
900
+ to: '+15551111111',
901
+ }));
902
+
903
+ // Wait for initial greeting to settle
904
+ await new Promise((resolve) => setTimeout(resolve, 10));
905
+
906
+ // Send a caller utterance
907
+ await relay.handleMessage(JSON.stringify({
908
+ type: 'prompt',
909
+ voicePrompt: 'I would like to schedule an appointment',
910
+ lang: 'en-US',
911
+ last: true,
912
+ }));
913
+
914
+ // Verify caller transcript is persisted to the voice conversation
915
+ const userMessages = getMessages('conv-relay-inbound-persist').filter((m) => m.role === 'user');
916
+ expect(userMessages.length).toBeGreaterThan(0);
917
+ const lastUserMsg = userMessages[userMessages.length - 1];
918
+ expect(lastUserMsg.content).toContain('schedule an appointment');
919
+
920
+ // Verify assistant response is also persisted
921
+ const assistantMessages = getMessages('conv-relay-inbound-persist').filter((m) => m.role === 'assistant');
922
+ expect(assistantMessages.length).toBeGreaterThan(0);
923
+
924
+ relay.destroy();
925
+ });
926
+
927
+ test('handleMessage: inbound call supports multi-turn conversation', async () => {
928
+ ensureConversation('conv-relay-inbound-multi');
929
+ const session = createCallSession({
930
+ conversationId: 'conv-relay-inbound-multi',
931
+ provider: 'twilio',
932
+ fromNumber: '+15559999999',
933
+ toNumber: '+15551111111',
934
+ // no task — inbound call
935
+ });
936
+
937
+ let turnCount = 0;
938
+ mockSendMessage.mockImplementation(async (_messages: unknown[], _tools: unknown[], _systemPrompt: unknown, options?: { onEvent?: (event: { type: string; text?: string }) => void }) => {
939
+ turnCount++;
940
+ let tokens: string[];
941
+ if (turnCount === 1) tokens = ['Hello, how can I help you?'];
942
+ else if (turnCount === 2) tokens = ['Sure, I can help with that.'];
943
+ else tokens = ['Your appointment is confirmed.'];
944
+ for (const token of tokens) {
945
+ options?.onEvent?.({ type: 'text_delta', text: token });
946
+ }
947
+ return {
948
+ content: [{ type: 'text', text: tokens.join('') }],
949
+ model: 'claude-sonnet-4-20250514',
950
+ usage: { inputTokens: 100, outputTokens: 50 },
951
+ stopReason: 'end_turn',
952
+ };
953
+ });
954
+
955
+ const { ws: _ws, relay } = createMockWs(session.id);
956
+
957
+ // Setup
958
+ await relay.handleMessage(JSON.stringify({
959
+ type: 'setup',
960
+ callSid: 'CA_inbound_multi_123',
961
+ from: '+15559999999',
962
+ to: '+15551111111',
963
+ }));
964
+
965
+ await new Promise((resolve) => setTimeout(resolve, 10));
966
+
967
+ // First caller turn
968
+ await relay.handleMessage(JSON.stringify({
969
+ type: 'prompt',
970
+ voicePrompt: 'I need to schedule something',
971
+ lang: 'en-US',
972
+ last: true,
973
+ }));
974
+
975
+ // Second caller turn
976
+ await relay.handleMessage(JSON.stringify({
977
+ type: 'prompt',
978
+ voicePrompt: 'How about next Tuesday?',
979
+ lang: 'en-US',
980
+ last: true,
981
+ }));
982
+
983
+ // Verify conversation history has multiple turns
984
+ const history = relay.getConversationHistory();
985
+ expect(history.length).toBe(2);
986
+ expect(history[0].text).toBe('I need to schedule something');
987
+ expect(history[1].text).toBe('How about next Tuesday?');
988
+
989
+ // Verify LLM was called for each turn (greeting + 2 caller turns)
990
+ expect(turnCount).toBe(3);
991
+
992
+ // Verify events were recorded for both caller utterances
993
+ const events = getCallEvents(session.id).filter((e) => e.eventType === 'caller_spoke');
994
+ expect(events.length).toBe(2);
995
+
996
+ relay.destroy();
997
+ });
998
+
999
+ // ── Inbound voice guardian verification gate ────────────────────────
1000
+
1001
+ test('inbound guardian verification: DTMF code entry succeeds and starts normal call flow', async () => {
1002
+ ensureConversation('conv-guardian-dtmf-ok');
1003
+ const session = createCallSession({
1004
+ conversationId: 'conv-guardian-dtmf-ok',
1005
+ provider: 'twilio',
1006
+ fromNumber: '+15559999999',
1007
+ toNumber: '+15551111111',
1008
+ assistantId: 'test-assistant',
1009
+ // no task — inbound call
1010
+ });
1011
+
1012
+ // Create a pending voice guardian challenge
1013
+ const challenge = createVerificationChallenge('test-assistant', 'voice');
1014
+ const secret = challenge.secret;
1015
+
1016
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, how can I help you?']));
1017
+
1018
+ const { ws, relay } = createMockWs(session.id);
1019
+
1020
+ await relay.handleMessage(JSON.stringify({
1021
+ type: 'setup',
1022
+ callSid: 'CA_guardian_dtmf_ok',
1023
+ from: '+15559999999',
1024
+ to: '+15551111111',
1025
+ }));
1026
+
1027
+ // Should be in verification-pending state
1028
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1029
+ expect(relay.getConnectionState()).toBe('verification_pending');
1030
+
1031
+ // Verify TTS prompt was sent asking for code
1032
+ const setupMessages = ws.sentMessages
1033
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1034
+ .filter((m) => m.type === 'text');
1035
+ expect(setupMessages.some((m) => (m.token ?? '').includes('verification code'))).toBe(true);
1036
+
1037
+ // Enter the correct code via DTMF
1038
+ for (const digit of secret) {
1039
+ await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
1040
+ }
1041
+
1042
+ await new Promise((resolve) => setTimeout(resolve, 10));
1043
+
1044
+ // Verification should have succeeded
1045
+ expect(relay.isGuardianVerificationActive()).toBe(false);
1046
+ expect(relay.getConnectionState()).toBe('connected');
1047
+
1048
+ // Guardian binding should have been created
1049
+ const binding = getGuardianBinding('test-assistant', 'voice');
1050
+ expect(binding).not.toBeNull();
1051
+
1052
+ // Orchestrator greeting should have fired
1053
+ const textMessages = ws.sentMessages
1054
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1055
+ .filter((m) => m.type === 'text');
1056
+ expect(textMessages.some((m) => (m.token ?? '').includes('how can I help'))).toBe(true);
1057
+
1058
+ // Verify events recorded
1059
+ const guardianEvents = getCallEvents(session.id);
1060
+ expect(guardianEvents.some((e) => e.eventType === 'guardian_voice_verification_started')).toBe(true);
1061
+ expect(guardianEvents.some((e) => e.eventType === 'guardian_voice_verification_succeeded')).toBe(true);
1062
+
1063
+ relay.destroy();
1064
+ });
1065
+
1066
+ test('inbound guardian verification: speech-based code entry succeeds', async () => {
1067
+ ensureConversation('conv-guardian-speech-ok');
1068
+ const session = createCallSession({
1069
+ conversationId: 'conv-guardian-speech-ok',
1070
+ provider: 'twilio',
1071
+ fromNumber: '+15559999999',
1072
+ toNumber: '+15551111111',
1073
+ assistantId: 'test-assistant',
1074
+ });
1075
+
1076
+ const challenge = createVerificationChallenge('test-assistant', 'voice');
1077
+ const secret = challenge.secret;
1078
+
1079
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello, verified caller!']));
1080
+
1081
+ const { ws, relay } = createMockWs(session.id);
1082
+
1083
+ await relay.handleMessage(JSON.stringify({
1084
+ type: 'setup',
1085
+ callSid: 'CA_guardian_speech_ok',
1086
+ from: '+15559999999',
1087
+ to: '+15551111111',
1088
+ }));
1089
+
1090
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1091
+
1092
+ // Speak the code as individual digit characters
1093
+ const spokenCode = secret.split('').join(' ');
1094
+ await relay.handleMessage(JSON.stringify({
1095
+ type: 'prompt',
1096
+ voicePrompt: spokenCode,
1097
+ lang: 'en-US',
1098
+ last: true,
1099
+ }));
1100
+
1101
+ await new Promise((resolve) => setTimeout(resolve, 10));
1102
+
1103
+ // Verification should have succeeded
1104
+ expect(relay.isGuardianVerificationActive()).toBe(false);
1105
+ expect(relay.getConnectionState()).toBe('connected');
1106
+
1107
+ // Binding created
1108
+ const binding = getGuardianBinding('test-assistant', 'voice');
1109
+ expect(binding).not.toBeNull();
1110
+
1111
+ // Greeting should have started
1112
+ const textMessages = ws.sentMessages
1113
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1114
+ .filter((m) => m.type === 'text');
1115
+ expect(textMessages.some((m) => (m.token ?? '').includes('verified caller'))).toBe(true);
1116
+
1117
+ relay.destroy();
1118
+ });
1119
+
1120
+ test('inbound call: caller matching voice guardian binding is classified as guardian', async () => {
1121
+ ensureConversation('conv-guardian-role-match');
1122
+ const session = createCallSession({
1123
+ conversationId: 'conv-guardian-role-match',
1124
+ provider: 'twilio',
1125
+ fromNumber: '+15550001111',
1126
+ toNumber: '+15551111111',
1127
+ assistantId: 'test-assistant',
1128
+ });
1129
+
1130
+ createBinding({
1131
+ assistantId: 'test-assistant',
1132
+ channel: 'voice',
1133
+ guardianExternalUserId: '+15550001111',
1134
+ guardianDeliveryChatId: '+15550001111',
1135
+ });
1136
+
1137
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello there.']));
1138
+
1139
+ const { relay } = createMockWs(session.id);
1140
+
1141
+ await relay.handleMessage(JSON.stringify({
1142
+ type: 'setup',
1143
+ callSid: 'CA_guardian_role_match',
1144
+ from: '+15550001111',
1145
+ to: '+15551111111',
1146
+ }));
1147
+
1148
+ const runtimeContext = (relay.getOrchestrator() as unknown as { guardianContext?: { sourceChannel?: string; actorRole?: string; guardianExternalUserId?: string } })?.guardianContext;
1149
+ expect(runtimeContext?.sourceChannel).toBe('voice');
1150
+ expect(runtimeContext?.actorRole).toBe('guardian');
1151
+ expect(runtimeContext?.guardianExternalUserId).toBe('+15550001111');
1152
+
1153
+ relay.destroy();
1154
+ });
1155
+
1156
+ test('inbound call: caller not matching voice guardian binding is classified as non-guardian', async () => {
1157
+ ensureConversation('conv-guardian-role-mismatch');
1158
+ const session = createCallSession({
1159
+ conversationId: 'conv-guardian-role-mismatch',
1160
+ provider: 'twilio',
1161
+ fromNumber: '+15550002222',
1162
+ toNumber: '+15551111111',
1163
+ assistantId: 'test-assistant',
1164
+ });
1165
+
1166
+ createBinding({
1167
+ assistantId: 'test-assistant',
1168
+ channel: 'voice',
1169
+ guardianExternalUserId: '+15550009999',
1170
+ guardianDeliveryChatId: '+15550009999',
1171
+ });
1172
+
1173
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Hello there.']));
1174
+
1175
+ const { relay } = createMockWs(session.id);
1176
+
1177
+ await relay.handleMessage(JSON.stringify({
1178
+ type: 'setup',
1179
+ callSid: 'CA_guardian_role_mismatch',
1180
+ from: '+15550002222',
1181
+ to: '+15551111111',
1182
+ }));
1183
+
1184
+ const runtimeContext = (relay.getOrchestrator() as unknown as {
1185
+ guardianContext?: {
1186
+ sourceChannel?: string;
1187
+ actorRole?: string;
1188
+ guardianExternalUserId?: string;
1189
+ requesterExternalUserId?: string;
1190
+ };
1191
+ })?.guardianContext;
1192
+ expect(runtimeContext?.sourceChannel).toBe('voice');
1193
+ expect(runtimeContext?.actorRole).toBe('non-guardian');
1194
+ expect(runtimeContext?.guardianExternalUserId).toBe('+15550009999');
1195
+ expect(runtimeContext?.requesterExternalUserId).toBe('+15550002222');
1196
+
1197
+ relay.destroy();
1198
+ });
1199
+
1200
+ test('inbound guardian verification updates orchestrator context to guardian', async () => {
1201
+ ensureConversation('conv-guardian-context-upgrade');
1202
+ const session = createCallSession({
1203
+ conversationId: 'conv-guardian-context-upgrade',
1204
+ provider: 'twilio',
1205
+ fromNumber: '+15550003333',
1206
+ toNumber: '+15551111111',
1207
+ assistantId: 'test-assistant',
1208
+ });
1209
+
1210
+ const challenge = createVerificationChallenge('test-assistant', 'voice');
1211
+ const spokenCode = challenge.secret.split('').join(' ');
1212
+
1213
+ const { relay } = createMockWs(session.id);
1214
+
1215
+ await relay.handleMessage(JSON.stringify({
1216
+ type: 'setup',
1217
+ callSid: 'CA_guardian_context_upgrade',
1218
+ from: session.fromNumber,
1219
+ to: session.toNumber,
1220
+ }));
1221
+
1222
+ const preVerify = (relay.getOrchestrator() as unknown as {
1223
+ guardianContext?: { actorRole?: string };
1224
+ })?.guardianContext;
1225
+ expect(preVerify?.actorRole).toBe('unverified_channel');
1226
+
1227
+ await relay.handleMessage(JSON.stringify({
1228
+ type: 'prompt',
1229
+ voicePrompt: spokenCode,
1230
+ lang: 'en-US',
1231
+ last: true,
1232
+ }));
1233
+
1234
+ await new Promise((resolve) => setTimeout(resolve, 10));
1235
+
1236
+ const postVerify = (relay.getOrchestrator() as unknown as {
1237
+ guardianContext?: { sourceChannel?: string; actorRole?: string; guardianExternalUserId?: string };
1238
+ })?.guardianContext;
1239
+ expect(postVerify?.sourceChannel).toBe('voice');
1240
+ expect(postVerify?.actorRole).toBe('guardian');
1241
+ expect(postVerify?.guardianExternalUserId).toBe(session.fromNumber);
1242
+
1243
+ relay.destroy();
1244
+ });
1245
+
1246
+ test('inbound guardian verification: invalid code triggers retry prompt', async () => {
1247
+ ensureConversation('conv-guardian-retry');
1248
+ const session = createCallSession({
1249
+ conversationId: 'conv-guardian-retry',
1250
+ provider: 'twilio',
1251
+ fromNumber: '+15559999999',
1252
+ toNumber: '+15551111111',
1253
+ assistantId: 'test-assistant',
1254
+ });
1255
+
1256
+ createVerificationChallenge('test-assistant', 'voice');
1257
+
1258
+ const { ws, relay } = createMockWs(session.id);
1259
+
1260
+ await relay.handleMessage(JSON.stringify({
1261
+ type: 'setup',
1262
+ callSid: 'CA_guardian_retry',
1263
+ from: '+15559999999',
1264
+ to: '+15551111111',
1265
+ }));
1266
+
1267
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1268
+
1269
+ // Enter a wrong code via DTMF
1270
+ for (const digit of '000000') {
1271
+ await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
1272
+ }
1273
+
1274
+ // Should still be in verification-pending state (retry allowed)
1275
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1276
+ expect(relay.getConnectionState()).toBe('verification_pending');
1277
+
1278
+ // Should have sent a retry prompt
1279
+ const textMessages = ws.sentMessages
1280
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1281
+ .filter((m) => m.type === 'text');
1282
+ expect(textMessages.some((m) => (m.token ?? '').includes('incorrect'))).toBe(true);
1283
+
1284
+ relay.destroy();
1285
+ });
1286
+
1287
+ test('inbound guardian verification: max attempts exhaustion terminates call', async () => {
1288
+ ensureConversation('conv-guardian-max-attempts');
1289
+ const session = createCallSession({
1290
+ conversationId: 'conv-guardian-max-attempts',
1291
+ provider: 'twilio',
1292
+ fromNumber: '+15559999999',
1293
+ toNumber: '+15551111111',
1294
+ assistantId: 'test-assistant',
1295
+ });
1296
+
1297
+ createVerificationChallenge('test-assistant', 'voice');
1298
+
1299
+ const { ws, relay } = createMockWs(session.id);
1300
+
1301
+ await relay.handleMessage(JSON.stringify({
1302
+ type: 'setup',
1303
+ callSid: 'CA_guardian_max_attempts',
1304
+ from: '+15559999999',
1305
+ to: '+15551111111',
1306
+ }));
1307
+
1308
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1309
+
1310
+ // Enter wrong codes 3 times (max attempts = 3)
1311
+ for (let attempt = 0; attempt < 3; attempt++) {
1312
+ for (const digit of '000000') {
1313
+ await relay.handleMessage(JSON.stringify({ type: 'dtmf', digit }));
1314
+ }
1315
+ }
1316
+
1317
+ // Call should be marked as failed
1318
+ const updated = getCallSession(session.id);
1319
+ expect(updated).not.toBeNull();
1320
+ expect(updated!.status).toBe('failed');
1321
+ expect(updated!.lastError).toContain('Guardian voice verification failed');
1322
+
1323
+ // Should have sent goodbye message
1324
+ const textMessages = ws.sentMessages
1325
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1326
+ .filter((m) => m.type === 'text');
1327
+ expect(textMessages.some((m) => (m.token ?? '').includes('Verification failed. Goodbye.'))).toBe(true);
1328
+
1329
+ // Verify events
1330
+ const events = getCallEvents(session.id);
1331
+ expect(events.some((e) => e.eventType === 'guardian_voice_verification_failed')).toBe(true);
1332
+
1333
+ // Let the delayed endSession callback flush
1334
+ await new Promise((resolve) => setTimeout(resolve, 2100));
1335
+
1336
+ // Verify end message was sent
1337
+ const endMessages = ws.sentMessages
1338
+ .map((raw) => JSON.parse(raw) as { type: string })
1339
+ .filter((m) => m.type === 'end');
1340
+ expect(endMessages.length).toBe(1);
1341
+
1342
+ relay.destroy();
1343
+ });
1344
+
1345
+ test('inbound guardian verification: no pending challenge proceeds with normal flow', async () => {
1346
+ ensureConversation('conv-guardian-no-challenge');
1347
+ const session = createCallSession({
1348
+ conversationId: 'conv-guardian-no-challenge',
1349
+ provider: 'twilio',
1350
+ fromNumber: '+15559999999',
1351
+ toNumber: '+15551111111',
1352
+ assistantId: 'test-assistant',
1353
+ // no task — inbound call
1354
+ });
1355
+
1356
+ // Do NOT create any pending challenge
1357
+
1358
+ mockSendMessage.mockImplementation(createMockProviderResponse(['Welcome to the line.']));
1359
+
1360
+ const { ws, relay } = createMockWs(session.id);
1361
+
1362
+ await relay.handleMessage(JSON.stringify({
1363
+ type: 'setup',
1364
+ callSid: 'CA_guardian_no_challenge',
1365
+ from: '+15559999999',
1366
+ to: '+15551111111',
1367
+ }));
1368
+
1369
+ await new Promise((resolve) => setTimeout(resolve, 10));
1370
+
1371
+ // Should NOT be in guardian verification state
1372
+ expect(relay.isGuardianVerificationActive()).toBe(false);
1373
+ expect(relay.getConnectionState()).toBe('connected');
1374
+
1375
+ // Should have started normal greeting
1376
+ const textMessages = ws.sentMessages
1377
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1378
+ .filter((m) => m.type === 'text');
1379
+ expect(textMessages.some((m) => (m.token ?? '').includes('Welcome to the line'))).toBe(true);
1380
+
1381
+ relay.destroy();
1382
+ });
1383
+
1384
+ test('inbound guardian verification: speech with partial digits prompts for more', async () => {
1385
+ ensureConversation('conv-guardian-partial-speech');
1386
+ const session = createCallSession({
1387
+ conversationId: 'conv-guardian-partial-speech',
1388
+ provider: 'twilio',
1389
+ fromNumber: '+15559999999',
1390
+ toNumber: '+15551111111',
1391
+ assistantId: 'test-assistant',
1392
+ });
1393
+
1394
+ createVerificationChallenge('test-assistant', 'voice');
1395
+
1396
+ const { ws, relay } = createMockWs(session.id);
1397
+
1398
+ await relay.handleMessage(JSON.stringify({
1399
+ type: 'setup',
1400
+ callSid: 'CA_guardian_partial_speech',
1401
+ from: '+15559999999',
1402
+ to: '+15551111111',
1403
+ }));
1404
+
1405
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1406
+
1407
+ // Speak only 3 digits
1408
+ await relay.handleMessage(JSON.stringify({
1409
+ type: 'prompt',
1410
+ voicePrompt: 'one two three',
1411
+ lang: 'en-US',
1412
+ last: true,
1413
+ }));
1414
+
1415
+ // Should still be in verification state
1416
+ expect(relay.isGuardianVerificationActive()).toBe(true);
1417
+
1418
+ // Should have prompted for more digits
1419
+ const textMessages = ws.sentMessages
1420
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
1421
+ .filter((m) => m.type === 'text');
1422
+ expect(textMessages.some((m) => (m.token ?? '').includes('3 digits'))).toBe(true);
1423
+ expect(textMessages.some((m) => (m.token ?? '').includes('all 6 digits'))).toBe(true);
1424
+
1425
+ relay.destroy();
1426
+ });
688
1427
  });