vellum 0.2.0 → 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 (361) 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 +161 -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__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +17 -10
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,416 @@
1
+ import { eq, and, notInArray, desc } from 'drizzle-orm';
2
+ import { v4 as uuid } from 'uuid';
3
+ import { getDb } from '../memory/db.js';
4
+ import { callSessions, callEvents, callPendingQuestions } from '../memory/schema.js';
5
+ import type { CallSession, CallEvent, CallPendingQuestion, CallEventType, CallStatus } from './types.js';
6
+ import { validateTransition } from './call-state-machine.js';
7
+ import { getLogger } from '../util/logger.js';
8
+
9
+ const log = getLogger('call-store');
10
+
11
+ // ── Helpers ──────────────────────────────────────────────────────────
12
+
13
+ function parseCallSession(row: typeof callSessions.$inferSelect): CallSession {
14
+ return {
15
+ id: row.id,
16
+ conversationId: row.conversationId,
17
+ provider: row.provider,
18
+ providerCallSid: row.providerCallSid,
19
+ fromNumber: row.fromNumber,
20
+ toNumber: row.toNumber,
21
+ task: row.task,
22
+ status: row.status as CallSession['status'],
23
+ startedAt: row.startedAt,
24
+ endedAt: row.endedAt,
25
+ lastError: row.lastError,
26
+ createdAt: row.createdAt,
27
+ updatedAt: row.updatedAt,
28
+ };
29
+ }
30
+
31
+ function parseCallEvent(row: typeof callEvents.$inferSelect): CallEvent {
32
+ return {
33
+ id: row.id,
34
+ callSessionId: row.callSessionId,
35
+ eventType: row.eventType as CallEvent['eventType'],
36
+ payloadJson: row.payloadJson,
37
+ createdAt: row.createdAt,
38
+ };
39
+ }
40
+
41
+ function parsePendingQuestion(row: typeof callPendingQuestions.$inferSelect): CallPendingQuestion {
42
+ return {
43
+ id: row.id,
44
+ callSessionId: row.callSessionId,
45
+ questionText: row.questionText,
46
+ status: row.status as CallPendingQuestion['status'],
47
+ askedAt: row.askedAt,
48
+ answeredAt: row.answeredAt,
49
+ answerText: row.answerText,
50
+ };
51
+ }
52
+
53
+ // ── Call Sessions ────────────────────────────────────────────────────
54
+
55
+ export function createCallSession(opts: {
56
+ conversationId: string;
57
+ provider: string;
58
+ fromNumber: string;
59
+ toNumber: string;
60
+ task?: string;
61
+ }): CallSession {
62
+ const db = getDb();
63
+ const now = Date.now();
64
+ const session = {
65
+ id: uuid(),
66
+ conversationId: opts.conversationId,
67
+ provider: opts.provider,
68
+ providerCallSid: null,
69
+ fromNumber: opts.fromNumber,
70
+ toNumber: opts.toNumber,
71
+ task: opts.task ?? null,
72
+ status: 'initiated' as const,
73
+ startedAt: null,
74
+ endedAt: null,
75
+ lastError: null,
76
+ createdAt: now,
77
+ updatedAt: now,
78
+ };
79
+ db.insert(callSessions).values(session).run();
80
+ return session;
81
+ }
82
+
83
+ export function getCallSession(id: string): CallSession | null {
84
+ const db = getDb();
85
+ const row = db.select().from(callSessions).where(eq(callSessions.id, id)).get();
86
+ if (!row) return null;
87
+ return parseCallSession(row);
88
+ }
89
+
90
+ export function getCallSessionByCallSid(callSid: string): CallSession | null {
91
+ const db = getDb();
92
+ const row = db
93
+ .select()
94
+ .from(callSessions)
95
+ .where(eq(callSessions.providerCallSid, callSid))
96
+ .get();
97
+ if (!row) return null;
98
+ return parseCallSession(row);
99
+ }
100
+
101
+ export function getActiveCallSessionForConversation(conversationId: string): CallSession | null {
102
+ const db = getDb();
103
+ const row = db
104
+ .select()
105
+ .from(callSessions)
106
+ .where(
107
+ and(
108
+ eq(callSessions.conversationId, conversationId),
109
+ notInArray(callSessions.status, ['completed', 'failed', 'cancelled']),
110
+ ),
111
+ )
112
+ .orderBy(desc(callSessions.createdAt))
113
+ .get();
114
+ if (!row) return null;
115
+ return parseCallSession(row);
116
+ }
117
+
118
+ export function updateCallSession(
119
+ id: string,
120
+ updates: Partial<Pick<CallSession, 'status' | 'providerCallSid' | 'startedAt' | 'endedAt' | 'lastError'>>,
121
+ ): void {
122
+ const db = getDb();
123
+
124
+ // Validate status transition when a new status is provided
125
+ if (updates.status) {
126
+ const current = getCallSession(id);
127
+ if (current) {
128
+ const result = validateTransition(current.status, updates.status as CallStatus);
129
+ if (!result.valid) {
130
+ log.warn({ callSessionId: id, from: current.status, to: updates.status, reason: result.reason }, 'Invalid call status transition — skipping update');
131
+ return;
132
+ }
133
+ }
134
+ }
135
+
136
+ db.update(callSessions)
137
+ .set({ ...updates, updatedAt: Date.now() })
138
+ .where(eq(callSessions.id, id))
139
+ .run();
140
+ }
141
+
142
+ // ── Recovery queries ─────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Returns all call sessions that are in a non-terminal state
146
+ * (i.e. not completed, failed, or cancelled). Used during daemon startup
147
+ * to reconcile in-flight calls.
148
+ */
149
+ export function listRecoverableCalls(): CallSession[] {
150
+ const db = getDb();
151
+ const rows = db
152
+ .select()
153
+ .from(callSessions)
154
+ .where(
155
+ notInArray(callSessions.status, ['completed', 'failed', 'cancelled']),
156
+ )
157
+ .all();
158
+ return rows.map(parseCallSession);
159
+ }
160
+
161
+ // ── Call Events ──────────────────────────────────────────────────────
162
+
163
+ export function recordCallEvent(
164
+ callSessionId: string,
165
+ eventType: CallEventType,
166
+ payload?: Record<string, unknown>,
167
+ ): CallEvent {
168
+ const db = getDb();
169
+ const now = Date.now();
170
+ const event = {
171
+ id: uuid(),
172
+ callSessionId,
173
+ eventType,
174
+ payloadJson: JSON.stringify(payload ?? {}),
175
+ createdAt: now,
176
+ };
177
+ db.insert(callEvents).values(event).run();
178
+ return event;
179
+ }
180
+
181
+ export function getCallEvents(callSessionId: string): CallEvent[] {
182
+ const db = getDb();
183
+ const rows = db
184
+ .select()
185
+ .from(callEvents)
186
+ .where(eq(callEvents.callSessionId, callSessionId))
187
+ .orderBy(callEvents.createdAt)
188
+ .all();
189
+ return rows.map(parseCallEvent);
190
+ }
191
+
192
+ // ── Pending Questions ────────────────────────────────────────────────
193
+
194
+ export function createPendingQuestion(callSessionId: string, questionText: string): CallPendingQuestion {
195
+ const db = getDb();
196
+ const now = Date.now();
197
+ const question = {
198
+ id: uuid(),
199
+ callSessionId,
200
+ questionText,
201
+ status: 'pending' as const,
202
+ askedAt: now,
203
+ answeredAt: null,
204
+ answerText: null,
205
+ };
206
+ db.insert(callPendingQuestions).values(question).run();
207
+ return question;
208
+ }
209
+
210
+ export function getPendingQuestion(callSessionId: string): CallPendingQuestion | null {
211
+ const db = getDb();
212
+ const row = db
213
+ .select()
214
+ .from(callPendingQuestions)
215
+ .where(
216
+ and(
217
+ eq(callPendingQuestions.callSessionId, callSessionId),
218
+ eq(callPendingQuestions.status, 'pending'),
219
+ ),
220
+ )
221
+ .orderBy(desc(callPendingQuestions.askedAt))
222
+ .limit(1)
223
+ .get();
224
+ if (!row) return null;
225
+ return parsePendingQuestion(row);
226
+ }
227
+
228
+ export function answerPendingQuestion(id: string, answerText: string): void {
229
+ const db = getDb();
230
+ db.update(callPendingQuestions)
231
+ .set({
232
+ status: 'answered',
233
+ answerText,
234
+ answeredAt: Date.now(),
235
+ })
236
+ .where(
237
+ and(
238
+ eq(callPendingQuestions.id, id),
239
+ eq(callPendingQuestions.status, 'pending'),
240
+ ),
241
+ )
242
+ .run();
243
+ // Drizzle's .run() returns void for bun:sqlite, so check affected rows via raw client.
244
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
245
+ const changes = raw.query('SELECT changes() as c').get() as { c: number };
246
+ if (changes.c === 0) {
247
+ log.warn({ questionId: id }, 'answerPendingQuestion: no rows updated — question may have already been answered or expired');
248
+ }
249
+ }
250
+
251
+ export function expirePendingQuestions(callSessionId: string): void {
252
+ const db = getDb();
253
+ db.update(callPendingQuestions)
254
+ .set({ status: 'expired' })
255
+ .where(
256
+ and(
257
+ eq(callPendingQuestions.callSessionId, callSessionId),
258
+ eq(callPendingQuestions.status, 'pending'),
259
+ ),
260
+ )
261
+ .run();
262
+ }
263
+
264
+ // ── Callback Idempotency ─────────────────────────────────────────────
265
+
266
+ /** Claims older than this are considered orphaned (crashed mid-processing) and can be reclaimed. */
267
+ const CLAIM_EXPIRY_MS = 60_000; // 60 seconds
268
+
269
+ /**
270
+ * Build a dedupe key for a Twilio status callback.
271
+ * Combines CallSid + CallStatus + Timestamp (or SequenceNumber if present)
272
+ * to uniquely identify each callback.
273
+ */
274
+ export function buildCallbackDedupeKey(
275
+ callSid: string,
276
+ callStatus: string,
277
+ timestamp?: string | null,
278
+ sequenceNumber?: string | null,
279
+ ): string {
280
+ const discriminator = sequenceNumber ?? timestamp ?? '';
281
+ return `${callSid}:${callStatus}:${discriminator}`;
282
+ }
283
+
284
+ /**
285
+ * Check whether a callback dedupe key has already been processed (read-only).
286
+ * Returns true if the key already exists, false otherwise.
287
+ */
288
+ export function isCallbackProcessed(dedupeKey: string): boolean {
289
+ const db = getDb();
290
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
291
+
292
+ const row = raw.query(
293
+ `SELECT 1 FROM processed_callbacks WHERE dedupe_key = ?`,
294
+ ).get(dedupeKey);
295
+ return row != null;
296
+ }
297
+
298
+ /**
299
+ * Record a callback as processed. Should be called AFTER downstream writes
300
+ * (session updates, event recording) have succeeded so that Twilio retries
301
+ * are not silently dropped if those writes fail.
302
+ *
303
+ * Uses INSERT OR IGNORE so concurrent calls for the same key are safe.
304
+ */
305
+ export function recordProcessedCallback(
306
+ dedupeKey: string,
307
+ callSessionId: string,
308
+ ): void {
309
+ const db = getDb();
310
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
311
+
312
+ raw.query(
313
+ `INSERT OR IGNORE INTO processed_callbacks (id, dedupe_key, call_session_id, created_at) VALUES (?, ?, ?, ?)`,
314
+ ).run(uuid(), dedupeKey, callSessionId, Date.now());
315
+ }
316
+
317
+ /**
318
+ * Try to record a processed callback. Returns true if this is a new callback
319
+ * (inserted successfully). Returns false if the callback was already processed
320
+ * (dedupe key already exists), indicating a replay.
321
+ *
322
+ * Uses INSERT ... ON CONFLICT DO NOTHING pattern for atomicity.
323
+ *
324
+ * @deprecated Use claimCallback + releaseCallbackClaim instead to
325
+ * atomically claim a callback and release on failure.
326
+ */
327
+ export function tryRecordProcessedCallback(
328
+ dedupeKey: string,
329
+ callSessionId: string,
330
+ ): boolean {
331
+ const db = getDb();
332
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
333
+
334
+ raw.query(
335
+ `INSERT OR IGNORE INTO processed_callbacks (id, dedupe_key, call_session_id, created_at) VALUES (?, ?, ?, ?)`,
336
+ ).run(uuid(), dedupeKey, callSessionId, Date.now());
337
+
338
+ const changes = raw.query('SELECT changes() as c').get() as { c: number };
339
+ return changes.c > 0;
340
+ }
341
+
342
+ /**
343
+ * Atomically claim a callback for processing. Returns a unique claim ID
344
+ * (string) if this caller won the claim, or null if another caller already
345
+ * claimed it (dedupe_key conflict).
346
+ *
347
+ * Expired orphaned claims (older than CLAIM_EXPIRY_MS) are automatically
348
+ * cleared before attempting the insert, so crashes mid-processing don't
349
+ * permanently block retries.
350
+ *
351
+ * If processing fails, call `releaseCallbackClaim(dedupeKey, claimId)` to allow retries.
352
+ * On success, call `finalizeCallbackClaim(dedupeKey, claimId)` to make the claim permanent.
353
+ *
354
+ * The returned claim ID acts as an ownership token: release and finalize
355
+ * operations require it so that handler A cannot accidentally release or
356
+ * finalize a claim that was reclaimed by handler B after expiry.
357
+ */
358
+ export function claimCallback(dedupeKey: string, callSessionId: string): string | null {
359
+ const db = getDb();
360
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
361
+
362
+ // Clear any expired orphaned claims so they can be reprocessed
363
+ raw.query(
364
+ `DELETE FROM processed_callbacks WHERE dedupe_key = ? AND created_at < ?`,
365
+ ).run(dedupeKey, Date.now() - CLAIM_EXPIRY_MS);
366
+
367
+ const claimId = uuid();
368
+ raw.query(
369
+ `INSERT OR IGNORE INTO processed_callbacks (id, dedupe_key, call_session_id, claim_id, created_at) VALUES (?, ?, ?, ?, ?)`,
370
+ ).run(uuid(), dedupeKey, callSessionId, claimId, Date.now());
371
+ const changes = raw.query('SELECT changes() as c').get() as { c: number };
372
+ return changes.c > 0 ? claimId : null;
373
+ }
374
+
375
+ /**
376
+ * Release a callback claim so that retries can reprocess it.
377
+ * Called when processing fails after a successful claim.
378
+ *
379
+ * Only deletes the row if both dedupe_key AND claim_id match, preventing
380
+ * handler A from releasing a claim that was reclaimed by handler B.
381
+ */
382
+ export function releaseCallbackClaim(dedupeKey: string, claimId: string): void {
383
+ const db = getDb();
384
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
385
+ raw.query(`DELETE FROM processed_callbacks WHERE dedupe_key = ? AND claim_id = ?`).run(dedupeKey, claimId);
386
+ }
387
+
388
+ /**
389
+ * Finalize a callback claim after successful processing.
390
+ * Sets the created_at to a far-future value so the claim never expires,
391
+ * distinguishing it from in-flight claims that may need to be reclaimed.
392
+ *
393
+ * Only updates the row if both dedupe_key AND claim_id match, preventing
394
+ * handler A from finalizing a claim that was reclaimed by handler B.
395
+ *
396
+ * Returns true if the claim was successfully finalized, or false if 0 rows
397
+ * were updated — meaning the claim was reclaimed by another handler after
398
+ * expiry. Callers should treat a false return as a lost-claim signal: the
399
+ * business writes already happened but the dedupe row belongs to someone
400
+ * else, so duplicate processing may occur on later retries.
401
+ */
402
+ export function finalizeCallbackClaim(dedupeKey: string, claimId: string): boolean {
403
+ const db = getDb();
404
+ const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
405
+ // Set created_at far in the future so expiry check never matches
406
+ const NEVER_EXPIRE = Date.now() + 100 * 365 * 24 * 60 * 60 * 1000; // ~100 years
407
+ raw.query(
408
+ `UPDATE processed_callbacks SET created_at = ? WHERE dedupe_key = ? AND claim_id = ?`,
409
+ ).run(NEVER_EXPIRE, dedupeKey, claimId);
410
+ const changes = raw.query('SELECT changes() as c').get() as { c: number };
411
+ if (changes.c === 0) {
412
+ log.warn({ dedupeKey, claimId }, 'finalizeCallbackClaim: claim was lost — another handler reclaimed this key after expiry');
413
+ return false;
414
+ }
415
+ return true;
416
+ }