vellum 0.2.1 → 0.2.2

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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,213 @@
1
+ export interface PromptSpeakerContext {
2
+ speakerId: string;
3
+ speakerLabel: string;
4
+ speakerConfidence: number | null;
5
+ source: 'provider' | 'inferred';
6
+ }
7
+
8
+ export interface PromptSpeakerMetadata {
9
+ speakerId?: string;
10
+ speakerLabel?: string;
11
+ speakerName?: string;
12
+ speakerConfidence?: number;
13
+ participantId?: string;
14
+ }
15
+
16
+ interface SpeakerProfile {
17
+ speakerId: string;
18
+ speakerLabel: string;
19
+ speakerConfidence: number | null;
20
+ source: 'provider' | 'inferred';
21
+ utteranceCount: number;
22
+ firstSeenAt: number;
23
+ lastSeenAt: number;
24
+ }
25
+
26
+ function toCleanString(value: unknown): string | null {
27
+ if (typeof value !== 'string') return null;
28
+ const trimmed = value.trim();
29
+ return trimmed.length > 0 ? trimmed : null;
30
+ }
31
+
32
+ function toNumber(value: unknown): number | null {
33
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
34
+ if (typeof value === 'string') {
35
+ const parsed = Number(value);
36
+ if (Number.isFinite(parsed)) return parsed;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function getObject(value: unknown): Record<string, unknown> | null {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
43
+ return value as Record<string, unknown>;
44
+ }
45
+
46
+ function normalizeSpeakerLabel(metadata: PromptSpeakerMetadata, fallbackIndex: number): string {
47
+ const preferredLabel = toCleanString(metadata.speakerName) ?? toCleanString(metadata.speakerLabel);
48
+ if (preferredLabel) return preferredLabel;
49
+ return `Speaker ${fallbackIndex}`;
50
+ }
51
+
52
+ export function extractPromptSpeakerMetadata(message: Record<string, unknown>): PromptSpeakerMetadata {
53
+ const providerMetadata = getObject(message.providerMetadata);
54
+ const metadata = getObject(message.metadata);
55
+ const participant = getObject(message.participant);
56
+ const speaker = getObject(message.speaker);
57
+
58
+ const pick = (...values: unknown[]): string | undefined => {
59
+ for (const value of values) {
60
+ const cleaned = toCleanString(value);
61
+ if (cleaned) return cleaned;
62
+ }
63
+ return undefined;
64
+ };
65
+
66
+ const pickNumber = (...values: unknown[]): number | undefined => {
67
+ for (const value of values) {
68
+ const parsed = toNumber(value);
69
+ if (parsed !== null) return parsed;
70
+ }
71
+ return undefined;
72
+ };
73
+
74
+ return {
75
+ speakerId: pick(
76
+ message.speakerId,
77
+ message.speaker_id,
78
+ speaker?.id,
79
+ speaker?.speakerId,
80
+ metadata?.speakerId,
81
+ providerMetadata?.speakerId,
82
+ metadata?.speaker_id,
83
+ providerMetadata?.speaker_id,
84
+ ),
85
+ speakerLabel: pick(
86
+ message.speakerLabel,
87
+ message.speaker_label,
88
+ message.speaker,
89
+ speaker?.label,
90
+ speaker?.name,
91
+ metadata?.speakerLabel,
92
+ providerMetadata?.speakerLabel,
93
+ metadata?.speaker_label,
94
+ providerMetadata?.speaker_label,
95
+ ),
96
+ speakerName: pick(
97
+ message.speakerName,
98
+ message.speaker_name,
99
+ participant?.name,
100
+ metadata?.speakerName,
101
+ providerMetadata?.speakerName,
102
+ metadata?.speaker_name,
103
+ providerMetadata?.speaker_name,
104
+ ),
105
+ speakerConfidence: pickNumber(
106
+ message.speakerConfidence,
107
+ message.speaker_confidence,
108
+ message.confidence,
109
+ speaker?.confidence,
110
+ metadata?.speakerConfidence,
111
+ providerMetadata?.speakerConfidence,
112
+ metadata?.speaker_confidence,
113
+ providerMetadata?.speaker_confidence,
114
+ ),
115
+ participantId: pick(
116
+ message.participantId,
117
+ message.participant_id,
118
+ participant?.id,
119
+ metadata?.participantId,
120
+ providerMetadata?.participantId,
121
+ metadata?.participant_id,
122
+ providerMetadata?.participant_id,
123
+ ),
124
+ };
125
+ }
126
+
127
+ export class SpeakerIdentityTracker {
128
+ private profiles = new Map<string, SpeakerProfile>();
129
+ private nextInferredIndex = 1;
130
+
131
+ identifySpeaker(metadata: PromptSpeakerMetadata): PromptSpeakerContext {
132
+ const providerSpeakerId =
133
+ toCleanString(metadata.speakerId)
134
+ ?? toCleanString(metadata.participantId)
135
+ ?? null;
136
+
137
+ if (providerSpeakerId) {
138
+ const existing = this.profiles.get(providerSpeakerId);
139
+ if (existing) {
140
+ existing.lastSeenAt = Date.now();
141
+ existing.utteranceCount += 1;
142
+ if (metadata.speakerConfidence !== undefined) {
143
+ existing.speakerConfidence = metadata.speakerConfidence;
144
+ }
145
+ return {
146
+ speakerId: existing.speakerId,
147
+ speakerLabel: existing.speakerLabel,
148
+ speakerConfidence: existing.speakerConfidence,
149
+ source: existing.source,
150
+ };
151
+ }
152
+
153
+ const profile: SpeakerProfile = {
154
+ speakerId: providerSpeakerId,
155
+ speakerLabel: normalizeSpeakerLabel(metadata, this.nextInferredIndex),
156
+ speakerConfidence: metadata.speakerConfidence ?? null,
157
+ source: 'provider',
158
+ utteranceCount: 1,
159
+ firstSeenAt: Date.now(),
160
+ lastSeenAt: Date.now(),
161
+ };
162
+ this.profiles.set(providerSpeakerId, profile);
163
+ this.nextInferredIndex += 1;
164
+ return {
165
+ speakerId: profile.speakerId,
166
+ speakerLabel: profile.speakerLabel,
167
+ speakerConfidence: profile.speakerConfidence,
168
+ source: profile.source,
169
+ };
170
+ }
171
+
172
+ const inferredSpeakerId = 'primary-speaker';
173
+ const existingPrimary = this.profiles.get(inferredSpeakerId);
174
+ if (existingPrimary) {
175
+ existingPrimary.lastSeenAt = Date.now();
176
+ existingPrimary.utteranceCount += 1;
177
+ return {
178
+ speakerId: existingPrimary.speakerId,
179
+ speakerLabel: existingPrimary.speakerLabel,
180
+ speakerConfidence: existingPrimary.speakerConfidence,
181
+ source: existingPrimary.source,
182
+ };
183
+ }
184
+
185
+ const inferredProfile: SpeakerProfile = {
186
+ speakerId: inferredSpeakerId,
187
+ speakerLabel: normalizeSpeakerLabel(metadata, this.nextInferredIndex),
188
+ speakerConfidence: metadata.speakerConfidence ?? null,
189
+ source: 'inferred',
190
+ utteranceCount: 1,
191
+ firstSeenAt: Date.now(),
192
+ lastSeenAt: Date.now(),
193
+ };
194
+ this.profiles.set(inferredSpeakerId, inferredProfile);
195
+ this.nextInferredIndex += 1;
196
+
197
+ return {
198
+ speakerId: inferredProfile.speakerId,
199
+ speakerLabel: inferredProfile.speakerLabel,
200
+ speakerConfidence: inferredProfile.speakerConfidence,
201
+ source: inferredProfile.source,
202
+ };
203
+ }
204
+
205
+ listProfiles(): PromptSpeakerContext[] {
206
+ return [...this.profiles.values()].map((profile) => ({
207
+ speakerId: profile.speakerId,
208
+ speakerLabel: profile.speakerLabel,
209
+ speakerConfidence: profile.speakerConfidence,
210
+ source: profile.source,
211
+ }));
212
+ }
213
+ }
@@ -128,6 +128,15 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
128
128
 
129
129
  // ── Webhook signature verification ──────────────────────────────────
130
130
 
131
+ /**
132
+ * Returns the Twilio auth token from the secure key store, or null if
133
+ * not configured. Exposed as a static method so callers (e.g. the
134
+ * HTTP server webhook middleware) can check availability independently.
135
+ */
136
+ static getAuthToken(): string | null {
137
+ return getSecureKey('twilio_auth_token') ?? null;
138
+ }
139
+
131
140
  /**
132
141
  * Validates an X-Twilio-Signature header using HMAC-SHA1.
133
142
  *
@@ -143,13 +152,8 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
143
152
  url: string,
144
153
  params: Record<string, string>,
145
154
  signature: string,
155
+ authToken: string,
146
156
  ): boolean {
147
- const authToken = getSecureKey('twilio_auth_token');
148
- if (!authToken) {
149
- log.error('Cannot verify Twilio webhook signature: auth token not configured');
150
- return false;
151
- }
152
-
153
157
  const sortedKeys = Object.keys(params).sort();
154
158
  let data = url;
155
159
  for (const key of sortedKeys) {
@@ -13,11 +13,15 @@ import {
13
13
  updateCallSession,
14
14
  recordCallEvent,
15
15
  expirePendingQuestions,
16
- getPendingQuestion,
17
- answerPendingQuestion,
16
+ buildCallbackDedupeKey,
17
+ claimCallback,
18
+ releaseCallbackClaim,
19
+ finalizeCallbackClaim,
18
20
  } from './call-store.js';
19
21
  import type { CallStatus } from './types.js';
20
- import { getCallOrchestrator } from './call-state.js';
22
+ import { logDeadLetterEvent } from './call-recovery.js';
23
+ import { isTerminalState } from './call-state-machine.js';
24
+ import { getTwilioConfig } from './twilio-config.js';
21
25
 
22
26
  const log = getLogger('twilio-routes');
23
27
 
@@ -32,12 +36,12 @@ function escapeXml(str: string): string {
32
36
  .replace(/'/g, '&apos;');
33
37
  }
34
38
 
35
- function generateTwiML(callSessionId: string, wssBaseUrl: string, welcomeGreeting: string): string {
39
+ function generateTwiML(callSessionId: string, relayUrl: string, welcomeGreeting: string): string {
36
40
  return `<?xml version="1.0" encoding="UTF-8"?>
37
41
  <Response>
38
42
  <Connect>
39
43
  <ConversationRelay
40
- url="${escapeXml(wssBaseUrl)}/v1/calls/relay?callSessionId=${escapeXml(callSessionId)}"
44
+ url="${escapeXml(relayUrl)}?callSessionId=${escapeXml(callSessionId)}"
41
45
  welcomeGreeting="${escapeXml(welcomeGreeting)}"
42
46
  voice="Google.en-US-Journey-O"
43
47
  language="en-US"
@@ -50,6 +54,19 @@ function generateTwiML(callSessionId: string, wssBaseUrl: string, welcomeGreetin
50
54
  </Response>`;
51
55
  }
52
56
 
57
+ /**
58
+ * Resolve the WebSocket relay URL from Twilio config.
59
+ *
60
+ * Treats wssBaseUrl as present only when it is non-empty after trimming.
61
+ * Falls back to webhookBaseUrl, normalizing the scheme from http(s) to ws(s)
62
+ * and stripping any trailing slash.
63
+ */
64
+ export function resolveRelayUrl(wssBaseUrl: string, webhookBaseUrl: string): string {
65
+ const base = wssBaseUrl.trim() || webhookBaseUrl;
66
+ const normalized = base.replace(/\/$/, '').replace(/^http(s?)/, 'ws$1');
67
+ return `${normalized}/v1/calls/relay`;
68
+ }
69
+
53
70
  /**
54
71
  * Map Twilio call status strings to our internal CallStatus.
55
72
  */
@@ -93,6 +110,11 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
93
110
  return new Response('Call session not found', { status: 404 });
94
111
  }
95
112
 
113
+ if (isTerminalState(session.status)) {
114
+ log.warn({ callSessionId, status: session.status }, 'Voice webhook: call session is in terminal state');
115
+ return new Response('Call session is no longer active', { status: 410 });
116
+ }
117
+
96
118
  // Parse the Twilio POST body to capture CallSid immediately, so status
97
119
  // callbacks (keyed by CallSid) can locate this session even if the
98
120
  // WebSocket relay hasn't been set up yet.
@@ -103,10 +125,11 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
103
125
  log.info({ callSessionId, callSid }, 'Stored CallSid from voice webhook');
104
126
  }
105
127
 
106
- const wssBaseUrl = process.env.WSS_BASE_URL ?? process.env.BASE_URL ?? 'wss://localhost:7821';
128
+ const config = getTwilioConfig();
129
+ const relayUrl = resolveRelayUrl(config.wssBaseUrl, config.webhookBaseUrl);
107
130
  const welcomeGreeting = process.env.CALL_WELCOME_GREETING ?? 'Hello, how can I help you today?';
108
131
 
109
- const twiml = generateTwiML(callSessionId, wssBaseUrl, welcomeGreeting);
132
+ const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting);
110
133
 
111
134
  log.info({ callSessionId }, 'Returning ConversationRelay TwiML');
112
135
 
@@ -126,7 +149,8 @@ export async function handleStatusCallback(req: Request): Promise<Response> {
126
149
  const callStatus = formBody.get('CallStatus');
127
150
 
128
151
  if (!callSid || !callStatus) {
129
- log.warn({ callSid, callStatus }, 'Status callback missing CallSid or CallStatus');
152
+ const rawPayload = Object.fromEntries(formBody.entries());
153
+ logDeadLetterEvent('Status callback missing CallSid or CallStatus', rawPayload, log);
130
154
  return new Response(null, { status: 200 });
131
155
  }
132
156
 
@@ -140,39 +164,70 @@ export async function handleStatusCallback(req: Request): Promise<Response> {
140
164
 
141
165
  const mappedStatus = mapTwilioStatus(callStatus);
142
166
  if (!mappedStatus) {
143
- log.warn({ callSid, callStatus }, 'Status callback: unknown Twilio status');
167
+ const rawPayload = Object.fromEntries(formBody.entries());
168
+ logDeadLetterEvent(`Unknown Twilio status: ${callStatus}`, rawPayload, log);
144
169
  return new Response(null, { status: 200 });
145
170
  }
146
171
 
147
- // Build updates
148
- const updates: Parameters<typeof updateCallSession>[1] = {
149
- status: mappedStatus,
150
- };
172
+ // ── Atomic idempotency claim ────────────────────────────────────
173
+ const timestamp = formBody.get('Timestamp');
174
+ const sequenceNumber = formBody.get('SequenceNumber');
175
+ const dedupeKey = buildCallbackDedupeKey(callSid, callStatus, timestamp, sequenceNumber);
151
176
 
152
- if (mappedStatus === 'in_progress' && !session.startedAt) {
153
- updates.startedAt = Date.now();
154
- }
155
-
156
- const isTerminal = mappedStatus === 'completed' || mappedStatus === 'failed';
157
- if (isTerminal) {
158
- updates.endedAt = Date.now();
177
+ const claimId = claimCallback(dedupeKey, session.id);
178
+ if (!claimId) {
179
+ log.info({ callSid, callStatus, dedupeKey }, 'Duplicate status callback — skipping');
180
+ return new Response(null, { status: 200 });
159
181
  }
160
182
 
161
- updateCallSession(session.id, updates);
162
-
163
- // Record event
164
- const eventType = isTerminal
165
- ? (mappedStatus === 'completed' ? 'call_ended' : 'call_failed')
166
- : (mappedStatus === 'in_progress' ? 'call_connected' : 'call_started');
167
-
168
- recordCallEvent(session.id, eventType, {
169
- twilioStatus: callStatus,
170
- callSid,
171
- });
172
-
173
- // Expire pending questions on terminal status
174
- if (isTerminal) {
175
- expirePendingQuestions(session.id);
183
+ try {
184
+ // Build updates
185
+ const updates: Parameters<typeof updateCallSession>[1] = {
186
+ status: mappedStatus,
187
+ };
188
+
189
+ if (mappedStatus === 'in_progress' && !session.startedAt) {
190
+ updates.startedAt = Date.now();
191
+ }
192
+
193
+ const isTerminal = mappedStatus === 'completed' || mappedStatus === 'failed';
194
+ if (isTerminal) {
195
+ updates.endedAt = Date.now();
196
+ }
197
+
198
+ updateCallSession(session.id, updates);
199
+
200
+ // Record event
201
+ const eventType = isTerminal
202
+ ? (mappedStatus === 'completed' ? 'call_ended' : 'call_failed')
203
+ : (mappedStatus === 'in_progress' ? 'call_connected' : 'call_started');
204
+
205
+ recordCallEvent(session.id, eventType, {
206
+ twilioStatus: callStatus,
207
+ callSid,
208
+ });
209
+
210
+ // Expire pending questions on terminal status
211
+ if (isTerminal) {
212
+ expirePendingQuestions(session.id);
213
+ }
214
+
215
+ // Mark the claim as permanently processed so it never expires.
216
+ // If finalization returns false, another handler reclaimed this key
217
+ // after our claim expired — our business writes already landed but
218
+ // the dedupe row now belongs to the other handler, risking duplicate
219
+ // processing on later retries.
220
+ const finalized = finalizeCallbackClaim(dedupeKey, claimId);
221
+ if (!finalized) {
222
+ log.warn(
223
+ { dedupeKey, claimId, callSid, callStatus },
224
+ 'Lost claim during finalization — business writes committed but dedupe ownership was taken by another handler',
225
+ );
226
+ }
227
+ } catch (err) {
228
+ // Release claim so Twilio retries can reprocess
229
+ releaseCallbackClaim(dedupeKey, claimId);
230
+ throw err;
176
231
  }
177
232
 
178
233
  return new Response(null, { status: 200 });
@@ -193,44 +248,3 @@ export async function handleConnectAction(_req: Request): Promise<Response> {
193
248
  );
194
249
  }
195
250
 
196
- /**
197
- * Answer a pending question for an active call.
198
- * POST /v1/calls/:callSessionId/answer
199
- * Body: { answer: string }
200
- */
201
- export async function handleCallAnswer(req: Request, callSessionId: string): Promise<Response> {
202
- const body = await req.json() as { answer?: string };
203
- if (!body.answer) {
204
- return Response.json({ error: 'Missing answer' }, { status: 400 });
205
- }
206
-
207
- const question = getPendingQuestion(callSessionId);
208
- if (!question) {
209
- return Response.json({ error: 'No pending question found' }, { status: 404 });
210
- }
211
-
212
- // Verify the orchestrator exists before attempting to route the answer.
213
- const orchestrator = getCallOrchestrator(callSessionId);
214
- if (!orchestrator) {
215
- log.warn({ callSessionId }, 'handleCallAnswer: no active orchestrator for call session');
216
- return Response.json({ error: 'No active orchestrator for this call' }, { status: 409 });
217
- }
218
-
219
- // Route answer to the orchestrator FIRST — it atomically checks whether it is
220
- // in the `waiting_on_user` state and transitions to `processing`. Only persist
221
- // the answer to the DB if the orchestrator actually accepted it, preventing a
222
- // race where the consultation timer expires between our check and the persist.
223
- const accepted = await orchestrator.handleUserAnswer(body.answer);
224
- if (!accepted) {
225
- log.warn(
226
- { callSessionId },
227
- 'handleCallAnswer: orchestrator rejected the answer (not in waiting_on_user state)',
228
- );
229
- return Response.json({ error: 'Orchestrator is not waiting for an answer' }, { status: 409 });
230
- }
231
-
232
- // Mark question as answered — only after the orchestrator has accepted
233
- answerPendingQuestion(question.id, body.answer);
234
-
235
- return Response.json({ ok: true, questionId: question.id });
236
- }
@@ -1,4 +1,4 @@
1
- export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed';
1
+ export type CallStatus = 'initiated' | 'ringing' | 'in_progress' | 'waiting_on_user' | 'completed' | 'failed' | 'cancelled';
2
2
  export type CallEventType = 'call_started' | 'call_connected' | 'caller_spoke' | 'assistant_spoke' | 'user_question_asked' | 'user_answered' | 'call_ended' | 'call_failed';
3
3
  export type PendingQuestionStatus = 'pending' | 'answered' | 'expired' | 'cancelled';
4
4