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,518 @@
1
+ import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ const testDir = mkdtempSync(join(tmpdir(), 'call-recovery-test-'));
7
+
8
+ mock.module('../util/platform.js', () => ({
9
+ getDataDir: () => testDir,
10
+ isMacOS: () => process.platform === 'darwin',
11
+ isLinux: () => process.platform === 'linux',
12
+ isWindows: () => process.platform === 'win32',
13
+ getSocketPath: () => join(testDir, 'test.sock'),
14
+ getPidPath: () => join(testDir, 'test.pid'),
15
+ getDbPath: () => join(testDir, 'test.db'),
16
+ getLogPath: () => join(testDir, 'test.log'),
17
+ ensureDataDir: () => {},
18
+ }));
19
+
20
+ mock.module('../util/logger.js', () => ({
21
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
22
+ get: () => () => {},
23
+ }),
24
+ }));
25
+
26
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
27
+ import { conversations } from '../memory/schema.js';
28
+ import {
29
+ createCallSession,
30
+ updateCallSession,
31
+ getCallSession,
32
+ listRecoverableCalls,
33
+ createPendingQuestion,
34
+ getPendingQuestion,
35
+ } from '../calls/call-store.js';
36
+ import { reconcileCallsOnStartup, logDeadLetterEvent, NO_SID_GRACE_PERIOD_MS } from '../calls/call-recovery.js';
37
+ import type { VoiceProvider } from '../calls/voice-provider.js';
38
+
39
+ initializeDb();
40
+
41
+ afterAll(() => {
42
+ resetDb();
43
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
44
+ });
45
+
46
+ /** Ensure a conversation row exists for the given ID so FK constraints pass. */
47
+ let ensuredConvIds = new Set<string>();
48
+ function ensureConversation(id: string): void {
49
+ if (ensuredConvIds.has(id)) return;
50
+ const db = getDb();
51
+ const now = Date.now();
52
+ db.insert(conversations).values({
53
+ id,
54
+ title: `Test conversation ${id}`,
55
+ createdAt: now,
56
+ updatedAt: now,
57
+ }).run();
58
+ ensuredConvIds.add(id);
59
+ }
60
+
61
+ function resetTables() {
62
+ const db = getDb();
63
+ db.run('DELETE FROM call_pending_questions');
64
+ db.run('DELETE FROM call_events');
65
+ db.run('DELETE FROM call_sessions');
66
+ db.run('DELETE FROM conversations');
67
+ ensuredConvIds = new Set();
68
+ }
69
+
70
+ function createTestCallSession(opts: Parameters<typeof createCallSession>[0]) {
71
+ ensureConversation(opts.conversationId);
72
+ return createCallSession(opts);
73
+ }
74
+
75
+ /** Backdate a session's createdAt so it appears older than the grace period. */
76
+ function backdateSession(sessionId: string, ageMs: number): void {
77
+ const db = getDb();
78
+ const past = Date.now() - ageMs;
79
+ db.run(`UPDATE call_sessions SET created_at = ${past} WHERE id = '${sessionId}'`);
80
+ }
81
+
82
+ /** Create a mock VoiceProvider that returns configurable statuses. */
83
+ function createMockProvider(statusMap: Record<string, string> = {}): VoiceProvider {
84
+ return {
85
+ name: 'mock-twilio',
86
+ initiateCall: async () => ({ callSid: 'mock-sid' }),
87
+ endCall: async () => {},
88
+ getCallStatus: async (callSid: string) => {
89
+ const status = statusMap[callSid];
90
+ if (status === undefined) {
91
+ throw new Error(`Unknown call SID: ${callSid}`);
92
+ }
93
+ return status;
94
+ },
95
+ };
96
+ }
97
+
98
+ /** Silent logger for tests */
99
+ const silentLog = new Proxy({} as Record<string, unknown>, {
100
+ get: () => () => {},
101
+ }) as unknown as ReturnType<typeof import('../util/logger.js').getLogger>;
102
+
103
+ describe('listRecoverableCalls', () => {
104
+ beforeEach(() => {
105
+ resetTables();
106
+ });
107
+
108
+ test('returns sessions in non-terminal states', () => {
109
+ const s1 = createTestCallSession({
110
+ conversationId: 'conv-r1',
111
+ provider: 'twilio',
112
+ fromNumber: '+15551111111',
113
+ toNumber: '+15552222222',
114
+ });
115
+ // s1 is 'initiated' — should be recoverable
116
+
117
+ const s2 = createTestCallSession({
118
+ conversationId: 'conv-r2',
119
+ provider: 'twilio',
120
+ fromNumber: '+15551111111',
121
+ toNumber: '+15553333333',
122
+ });
123
+ updateCallSession(s2.id, { status: 'in_progress' });
124
+ // s2 is 'in_progress' — should be recoverable
125
+
126
+ const s3 = createTestCallSession({
127
+ conversationId: 'conv-r3',
128
+ provider: 'twilio',
129
+ fromNumber: '+15551111111',
130
+ toNumber: '+15554444444',
131
+ });
132
+ updateCallSession(s3.id, { status: 'ringing' });
133
+ // s3 is 'ringing' — should be recoverable
134
+
135
+ const results = listRecoverableCalls();
136
+ const ids = results.map(r => r.id);
137
+
138
+ expect(ids).toContain(s1.id);
139
+ expect(ids).toContain(s2.id);
140
+ expect(ids).toContain(s3.id);
141
+ expect(results).toHaveLength(3);
142
+ });
143
+
144
+ test('does not include terminal calls (completed, failed, cancelled)', () => {
145
+ const completed = createTestCallSession({
146
+ conversationId: 'conv-t1',
147
+ provider: 'twilio',
148
+ fromNumber: '+15551111111',
149
+ toNumber: '+15552222222',
150
+ });
151
+ updateCallSession(completed.id, { status: 'completed' });
152
+
153
+ const failed = createTestCallSession({
154
+ conversationId: 'conv-t2',
155
+ provider: 'twilio',
156
+ fromNumber: '+15551111111',
157
+ toNumber: '+15553333333',
158
+ });
159
+ updateCallSession(failed.id, { status: 'failed' });
160
+
161
+ const cancelled = createTestCallSession({
162
+ conversationId: 'conv-t3',
163
+ provider: 'twilio',
164
+ fromNumber: '+15551111111',
165
+ toNumber: '+15554444444',
166
+ });
167
+ updateCallSession(cancelled.id, { status: 'cancelled' });
168
+
169
+ const results = listRecoverableCalls();
170
+ expect(results).toHaveLength(0);
171
+ });
172
+
173
+ test('returns empty array when no calls exist', () => {
174
+ const results = listRecoverableCalls();
175
+ expect(results).toHaveLength(0);
176
+ });
177
+
178
+ test('includes waiting_on_user status', () => {
179
+ const session = createTestCallSession({
180
+ conversationId: 'conv-w1',
181
+ provider: 'twilio',
182
+ fromNumber: '+15551111111',
183
+ toNumber: '+15552222222',
184
+ });
185
+ updateCallSession(session.id, { status: 'in_progress' });
186
+ updateCallSession(session.id, { status: 'waiting_on_user' });
187
+
188
+ const results = listRecoverableCalls();
189
+ expect(results).toHaveLength(1);
190
+ expect(results[0].id).toBe(session.id);
191
+ expect(results[0].status).toBe('waiting_on_user');
192
+ });
193
+ });
194
+
195
+ describe('reconcileCallsOnStartup', () => {
196
+ beforeEach(() => {
197
+ resetTables();
198
+ });
199
+
200
+ test('does nothing when no recoverable calls exist', async () => {
201
+ const provider = createMockProvider();
202
+ await reconcileCallsOnStartup(provider, silentLog);
203
+ // Should complete without error
204
+ });
205
+
206
+ test('fails stale no-SID sessions past grace period', async () => {
207
+ const session = createTestCallSession({
208
+ conversationId: 'conv-nosid',
209
+ provider: 'twilio',
210
+ fromNumber: '+15551111111',
211
+ toNumber: '+15552222222',
212
+ });
213
+ // Backdate session so it exceeds the grace period
214
+ backdateSession(session.id, NO_SID_GRACE_PERIOD_MS + 10_000);
215
+
216
+ const provider = createMockProvider();
217
+ await reconcileCallsOnStartup(provider, silentLog);
218
+
219
+ const updated = getCallSession(session.id);
220
+ expect(updated).not.toBeNull();
221
+ // Should be failed — orphan session past grace period
222
+ expect(updated!.status).toBe('failed');
223
+ expect(updated!.endedAt).not.toBeNull();
224
+ expect(updated!.lastError).toContain('grace period expired');
225
+ });
226
+
227
+ test('expires pending questions when stale no-SID session is failed', async () => {
228
+ const session = createTestCallSession({
229
+ conversationId: 'conv-nosid-pq',
230
+ provider: 'twilio',
231
+ fromNumber: '+15551111111',
232
+ toNumber: '+15552222222',
233
+ });
234
+ // Backdate session so it exceeds the grace period
235
+ backdateSession(session.id, NO_SID_GRACE_PERIOD_MS + 10_000);
236
+
237
+ // Create a pending question
238
+ createPendingQuestion(session.id, 'Are you still there?');
239
+ const pendingBefore = getPendingQuestion(session.id);
240
+ expect(pendingBefore).not.toBeNull();
241
+ expect(pendingBefore!.status).toBe('pending');
242
+
243
+ const provider = createMockProvider();
244
+ await reconcileCallsOnStartup(provider, silentLog);
245
+
246
+ // Pending question should be expired along with the session
247
+ const pendingAfter = getPendingQuestion(session.id);
248
+ expect(pendingAfter).toBeNull();
249
+ });
250
+
251
+ test('skips recent no-SID sessions within grace period', async () => {
252
+ const session = createTestCallSession({
253
+ conversationId: 'conv-nosid-recent',
254
+ provider: 'twilio',
255
+ fromNumber: '+15551111111',
256
+ toNumber: '+15552222222',
257
+ });
258
+ // Session was just created (createdAt ~ Date.now()), well within grace period
259
+
260
+ const provider = createMockProvider();
261
+ await reconcileCallsOnStartup(provider, silentLog);
262
+
263
+ const updated = getCallSession(session.id);
264
+ expect(updated).not.toBeNull();
265
+ // Should NOT be failed — still in its original non-terminal state
266
+ expect(updated!.status).toBe('initiated');
267
+ expect(updated!.endedAt).toBeNull();
268
+ expect(updated!.lastError).toContain('awaiting webhook');
269
+ });
270
+
271
+ test('transitions to completed when provider says call completed', async () => {
272
+ const session = createTestCallSession({
273
+ conversationId: 'conv-comp',
274
+ provider: 'twilio',
275
+ fromNumber: '+15551111111',
276
+ toNumber: '+15552222222',
277
+ });
278
+ updateCallSession(session.id, {
279
+ providerCallSid: 'CA_completed_123',
280
+ status: 'in_progress',
281
+ });
282
+
283
+ const provider = createMockProvider({ 'CA_completed_123': 'completed' });
284
+ await reconcileCallsOnStartup(provider, silentLog);
285
+
286
+ const updated = getCallSession(session.id);
287
+ expect(updated).not.toBeNull();
288
+ expect(updated!.status).toBe('completed');
289
+ expect(updated!.endedAt).not.toBeNull();
290
+ });
291
+
292
+ test('transitions to failed when provider says call failed', async () => {
293
+ const session = createTestCallSession({
294
+ conversationId: 'conv-fail',
295
+ provider: 'twilio',
296
+ fromNumber: '+15551111111',
297
+ toNumber: '+15552222222',
298
+ });
299
+ updateCallSession(session.id, {
300
+ providerCallSid: 'CA_failed_123',
301
+ status: 'ringing',
302
+ });
303
+
304
+ const provider = createMockProvider({ 'CA_failed_123': 'failed' });
305
+ await reconcileCallsOnStartup(provider, silentLog);
306
+
307
+ const updated = getCallSession(session.id);
308
+ expect(updated).not.toBeNull();
309
+ expect(updated!.status).toBe('failed');
310
+ expect(updated!.endedAt).not.toBeNull();
311
+ });
312
+
313
+ test('leaves call active when provider says call is still in-progress', async () => {
314
+ const session = createTestCallSession({
315
+ conversationId: 'conv-active',
316
+ provider: 'twilio',
317
+ fromNumber: '+15551111111',
318
+ toNumber: '+15552222222',
319
+ });
320
+ updateCallSession(session.id, {
321
+ providerCallSid: 'CA_active_123',
322
+ status: 'in_progress',
323
+ });
324
+
325
+ const provider = createMockProvider({ 'CA_active_123': 'in-progress' });
326
+ await reconcileCallsOnStartup(provider, silentLog);
327
+
328
+ const updated = getCallSession(session.id);
329
+ expect(updated).not.toBeNull();
330
+ expect(updated!.status).toBe('in_progress');
331
+ expect(updated!.endedAt).toBeNull();
332
+ });
333
+
334
+ test('leaves call active when provider says call is ringing', async () => {
335
+ const session = createTestCallSession({
336
+ conversationId: 'conv-ring',
337
+ provider: 'twilio',
338
+ fromNumber: '+15551111111',
339
+ toNumber: '+15552222222',
340
+ });
341
+ updateCallSession(session.id, {
342
+ providerCallSid: 'CA_ringing_123',
343
+ status: 'ringing',
344
+ });
345
+
346
+ const provider = createMockProvider({ 'CA_ringing_123': 'ringing' });
347
+ await reconcileCallsOnStartup(provider, silentLog);
348
+
349
+ const updated = getCallSession(session.id);
350
+ expect(updated).not.toBeNull();
351
+ expect(updated!.status).toBe('ringing');
352
+ expect(updated!.endedAt).toBeNull();
353
+ });
354
+
355
+ test('expires pending questions when call transitions to terminal state', async () => {
356
+ const session = createTestCallSession({
357
+ conversationId: 'conv-expire',
358
+ provider: 'twilio',
359
+ fromNumber: '+15551111111',
360
+ toNumber: '+15552222222',
361
+ });
362
+ updateCallSession(session.id, {
363
+ providerCallSid: 'CA_expire_123',
364
+ status: 'in_progress',
365
+ });
366
+ updateCallSession(session.id, { status: 'waiting_on_user' });
367
+
368
+ // Create a pending question
369
+ createPendingQuestion(session.id, 'What is your name?');
370
+
371
+ // Verify the question is pending
372
+ const pendingBefore = getPendingQuestion(session.id);
373
+ expect(pendingBefore).not.toBeNull();
374
+ expect(pendingBefore!.status).toBe('pending');
375
+
376
+ const provider = createMockProvider({ 'CA_expire_123': 'completed' });
377
+ await reconcileCallsOnStartup(provider, silentLog);
378
+
379
+ // Pending question should be expired
380
+ const pendingAfter = getPendingQuestion(session.id);
381
+ expect(pendingAfter).toBeNull();
382
+ });
383
+
384
+ test('does not expire pending questions when call stays active', async () => {
385
+ const session = createTestCallSession({
386
+ conversationId: 'conv-keep',
387
+ provider: 'twilio',
388
+ fromNumber: '+15551111111',
389
+ toNumber: '+15552222222',
390
+ });
391
+ updateCallSession(session.id, {
392
+ providerCallSid: 'CA_keep_123',
393
+ status: 'in_progress',
394
+ });
395
+ updateCallSession(session.id, { status: 'waiting_on_user' });
396
+
397
+ createPendingQuestion(session.id, 'Still waiting?');
398
+
399
+ const provider = createMockProvider({ 'CA_keep_123': 'in-progress' });
400
+ await reconcileCallsOnStartup(provider, silentLog);
401
+
402
+ // The pending question should still be there
403
+ const pendingAfter = getPendingQuestion(session.id);
404
+ expect(pendingAfter).not.toBeNull();
405
+ expect(pendingAfter!.status).toBe('pending');
406
+ });
407
+
408
+ test('fails call when provider status fetch throws', async () => {
409
+ const session = createTestCallSession({
410
+ conversationId: 'conv-err',
411
+ provider: 'twilio',
412
+ fromNumber: '+15551111111',
413
+ toNumber: '+15552222222',
414
+ });
415
+ updateCallSession(session.id, {
416
+ providerCallSid: 'CA_error_123',
417
+ status: 'in_progress',
418
+ });
419
+
420
+ // Provider will throw for unknown SIDs
421
+ const provider = createMockProvider({});
422
+ await reconcileCallsOnStartup(provider, silentLog);
423
+
424
+ const updated = getCallSession(session.id);
425
+ expect(updated).not.toBeNull();
426
+ expect(updated!.status).toBe('failed');
427
+ expect(updated!.lastError).toContain('Recovery: failed to fetch provider status');
428
+ });
429
+
430
+ test('fails call when provider returns unrecognised status', async () => {
431
+ const session = createTestCallSession({
432
+ conversationId: 'conv-unk',
433
+ provider: 'twilio',
434
+ fromNumber: '+15551111111',
435
+ toNumber: '+15552222222',
436
+ });
437
+ updateCallSession(session.id, {
438
+ providerCallSid: 'CA_unknown_123',
439
+ status: 'in_progress',
440
+ });
441
+
442
+ const provider = createMockProvider({ 'CA_unknown_123': 'some-unknown-status' });
443
+ await reconcileCallsOnStartup(provider, silentLog);
444
+
445
+ const updated = getCallSession(session.id);
446
+ expect(updated).not.toBeNull();
447
+ expect(updated!.status).toBe('failed');
448
+ expect(updated!.lastError).toContain("unrecognised provider status 'some-unknown-status'");
449
+ });
450
+
451
+ test('handles mixed recoverable calls correctly', async () => {
452
+ // Call 1: no SID, stale — should be failed (orphan past grace period)
453
+ const noSid = createTestCallSession({
454
+ conversationId: 'conv-mix1',
455
+ provider: 'twilio',
456
+ fromNumber: '+15551111111',
457
+ toNumber: '+15552222222',
458
+ });
459
+ backdateSession(noSid.id, NO_SID_GRACE_PERIOD_MS + 10_000);
460
+
461
+ // Call 2: provider says completed — should complete
462
+ const completed = createTestCallSession({
463
+ conversationId: 'conv-mix2',
464
+ provider: 'twilio',
465
+ fromNumber: '+15551111111',
466
+ toNumber: '+15553333333',
467
+ });
468
+ updateCallSession(completed.id, {
469
+ providerCallSid: 'CA_mix_completed',
470
+ status: 'in_progress',
471
+ });
472
+
473
+ // Call 3: provider says still active — should stay
474
+ const active = createTestCallSession({
475
+ conversationId: 'conv-mix3',
476
+ provider: 'twilio',
477
+ fromNumber: '+15551111111',
478
+ toNumber: '+15554444444',
479
+ });
480
+ updateCallSession(active.id, {
481
+ providerCallSid: 'CA_mix_active',
482
+ status: 'ringing',
483
+ });
484
+
485
+ const provider = createMockProvider({
486
+ 'CA_mix_completed': 'completed',
487
+ 'CA_mix_active': 'ringing',
488
+ });
489
+
490
+ await reconcileCallsOnStartup(provider, silentLog);
491
+
492
+ // No-SID session failed — orphan past grace period
493
+ const updatedNoSid = getCallSession(noSid.id);
494
+ expect(updatedNoSid!.status).toBe('failed');
495
+ expect(updatedNoSid!.endedAt).not.toBeNull();
496
+ expect(updatedNoSid!.lastError).toContain('grace period expired');
497
+
498
+ const updatedCompleted = getCallSession(completed.id);
499
+ expect(updatedCompleted!.status).toBe('completed');
500
+
501
+ const updatedActive = getCallSession(active.id);
502
+ expect(updatedActive!.status).toBe('ringing');
503
+ });
504
+ });
505
+
506
+ describe('logDeadLetterEvent', () => {
507
+ test('does not throw when called with a payload', () => {
508
+ expect(() => {
509
+ logDeadLetterEvent('test reason', { foo: 'bar' }, silentLog);
510
+ }).not.toThrow();
511
+ });
512
+
513
+ test('does not throw when called with null payload', () => {
514
+ expect(() => {
515
+ logDeadLetterEvent('null payload', null, silentLog);
516
+ }).not.toThrow();
517
+ });
518
+ });