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,550 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { execFileSync } from 'node:child_process';
6
+ import type { CommitContext } from '../workspace/commit-message-provider.js';
7
+
8
+ import {
9
+ CommitEnrichmentService,
10
+ _resetEnrichmentService,
11
+ } from '../workspace/commit-message-enrichment-service.js';
12
+ import { WorkspaceGitService, _resetGitServiceRegistry } from '../workspace/git-service.js';
13
+
14
+ describe('CommitEnrichmentService', () => {
15
+ let testDir: string;
16
+ let gitService: WorkspaceGitService;
17
+
18
+ beforeEach(async () => {
19
+ testDir = join(tmpdir(), `vellum-enrichment-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
20
+ mkdirSync(testDir, { recursive: true });
21
+ _resetGitServiceRegistry();
22
+ _resetEnrichmentService();
23
+
24
+ gitService = new WorkspaceGitService(testDir);
25
+ await gitService.ensureInitialized();
26
+ });
27
+
28
+ afterEach(async () => {
29
+ if (existsSync(testDir)) {
30
+ rmSync(testDir, { recursive: true, force: true });
31
+ }
32
+ });
33
+
34
+ function makeContext(overrides?: Partial<CommitContext>): CommitContext {
35
+ return {
36
+ workspaceDir: testDir,
37
+ trigger: 'turn',
38
+ sessionId: 'sess_test',
39
+ turnNumber: 1,
40
+ changedFiles: ['file.txt'],
41
+ timestampMs: Date.now(),
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ async function createCommit(): Promise<string> {
47
+ writeFileSync(join(testDir, `file-${Date.now()}.txt`), 'content');
48
+ await gitService.commitChanges('test commit');
49
+ return await gitService.getHeadHash();
50
+ }
51
+
52
+ async function waitForDrain(service: CommitEnrichmentService, timeoutMs = 5000): Promise<void> {
53
+ const started = Date.now();
54
+ while (service._getQueueSize() > 0 || service._getActiveWorkers() > 0) {
55
+ if (Date.now() - started > timeoutMs) {
56
+ throw new Error(`Timed out waiting for enrichment queue to drain after ${timeoutMs}ms`);
57
+ }
58
+ await new Promise(resolve => setTimeout(resolve, 50));
59
+ }
60
+ }
61
+
62
+ test('enqueue and execute writes git note on success', async () => {
63
+ const commitHash = await createCommit();
64
+ const service = new CommitEnrichmentService({
65
+ maxQueueSize: 10,
66
+ maxConcurrency: 1,
67
+ jobTimeoutMs: 5000,
68
+ maxRetries: 0,
69
+ });
70
+
71
+ service.enqueue({
72
+ workspaceDir: testDir,
73
+ commitHash,
74
+ context: makeContext(),
75
+ gitService,
76
+ });
77
+
78
+ // Wait for async processing
79
+ await service.shutdown();
80
+
81
+ // Verify git note was written
82
+ const noteContent = execFileSync('git', ['notes', '--ref=vellum', 'show', commitHash], {
83
+ cwd: testDir,
84
+ encoding: 'utf-8',
85
+ });
86
+
87
+ const note = JSON.parse(noteContent);
88
+ expect(note.enriched).toBe(true);
89
+ expect(note.trigger).toBe('turn');
90
+ expect(note.sessionId).toBe('sess_test');
91
+ expect(note.turnNumber).toBe(1);
92
+ expect(note.filesChanged).toBe(1);
93
+ expect(service._getSucceededCount()).toBe(1);
94
+ });
95
+
96
+ test('queue overflow drops oldest job', async () => {
97
+ const service = new CommitEnrichmentService({
98
+ maxQueueSize: 2,
99
+ maxConcurrency: 1,
100
+ jobTimeoutMs: 30000,
101
+ maxRetries: 0,
102
+ });
103
+
104
+ const hash1 = await createCommit();
105
+ const hash2 = await createCommit();
106
+ const hash3 = await createCommit();
107
+
108
+ // Enqueue 3 jobs — hash1 starts immediately (active worker),
109
+ // hash2 goes to queue (size=1), hash3 goes to queue (size=2), no overflow drop.
110
+ service.enqueue({ workspaceDir: testDir, commitHash: hash1, context: makeContext(), gitService });
111
+ service.enqueue({ workspaceDir: testDir, commitHash: hash2, context: makeContext(), gitService });
112
+ service.enqueue({ workspaceDir: testDir, commitHash: hash3, context: makeContext(), gitService });
113
+
114
+ // No overflow drops — queue size 2 can hold 2 pending while 1 is active
115
+ expect(service._getDroppedCount()).toBe(0);
116
+ expect(service._getQueueSize()).toBe(2);
117
+
118
+ // Shutdown discards the 2 pending jobs
119
+ await service.shutdown();
120
+ expect(service._getDroppedCount()).toBe(2);
121
+ expect(service._getSucceededCount()).toBe(1);
122
+ });
123
+
124
+ test('queue overflow actually drops when truly full', async () => {
125
+ // Create a service where the worker is slow
126
+ const service = new CommitEnrichmentService({
127
+ maxQueueSize: 1,
128
+ maxConcurrency: 1,
129
+ jobTimeoutMs: 30000,
130
+ maxRetries: 0,
131
+ });
132
+
133
+ const hash1 = await createCommit();
134
+ const hash2 = await createCommit();
135
+ const hash3 = await createCommit();
136
+
137
+ // hash1 starts processing immediately (active worker = 1, queue empty)
138
+ // hash2 goes to queue (queue size = 1)
139
+ // hash3 tries to go to queue but it's full → drops hash2, adds hash3
140
+ service.enqueue({ workspaceDir: testDir, commitHash: hash1, context: makeContext(), gitService });
141
+ service.enqueue({ workspaceDir: testDir, commitHash: hash2, context: makeContext(), gitService });
142
+ service.enqueue({ workspaceDir: testDir, commitHash: hash3, context: makeContext(), gitService });
143
+
144
+ expect(service._getDroppedCount()).toBe(1);
145
+
146
+ await service.shutdown();
147
+ });
148
+
149
+ test('fire-and-forget enqueue does not block caller', async () => {
150
+ const commitHash = await createCommit();
151
+ const service = new CommitEnrichmentService({
152
+ maxQueueSize: 10,
153
+ maxConcurrency: 1,
154
+ jobTimeoutMs: 5000,
155
+ maxRetries: 0,
156
+ });
157
+
158
+ const start = Date.now();
159
+ service.enqueue({
160
+ workspaceDir: testDir,
161
+ commitHash,
162
+ context: makeContext(),
163
+ gitService,
164
+ });
165
+ const elapsed = Date.now() - start;
166
+
167
+ // enqueue should return immediately (< 50ms)
168
+ expect(elapsed).toBeLessThan(50);
169
+
170
+ await service.shutdown();
171
+ });
172
+
173
+ test('graceful shutdown drains in-flight and discards pending', async () => {
174
+ const hash1 = await createCommit();
175
+ const hash2 = await createCommit();
176
+
177
+ const service = new CommitEnrichmentService({
178
+ maxQueueSize: 10,
179
+ maxConcurrency: 1,
180
+ jobTimeoutMs: 5000,
181
+ maxRetries: 0,
182
+ });
183
+
184
+ service.enqueue({ workspaceDir: testDir, commitHash: hash1, context: makeContext(), gitService });
185
+ service.enqueue({ workspaceDir: testDir, commitHash: hash2, context: makeContext(), gitService });
186
+
187
+ // Shutdown should complete without hanging
188
+ await service.shutdown();
189
+
190
+ // The first job was in-flight and should complete. The second was pending
191
+ // and should be discarded, counted as dropped.
192
+ expect(service._getSucceededCount()).toBe(1);
193
+ expect(service._getDroppedCount()).toBe(1);
194
+ expect(service._getQueueSize()).toBe(0);
195
+ });
196
+
197
+ test('shutdown discards all pending jobs and counts them as dropped', async () => {
198
+ // Use maxConcurrency 1 so only one job starts processing; the rest stay pending.
199
+ const hashes: string[] = [];
200
+ for (let i = 0; i < 5; i++) {
201
+ hashes.push(await createCommit());
202
+ }
203
+
204
+ const service = new CommitEnrichmentService({
205
+ maxQueueSize: 10,
206
+ maxConcurrency: 1,
207
+ jobTimeoutMs: 5000,
208
+ maxRetries: 0,
209
+ });
210
+
211
+ for (const hash of hashes) {
212
+ service.enqueue({ workspaceDir: testDir, commitHash: hash, context: makeContext(), gitService });
213
+ }
214
+
215
+ // First job is in-flight, remaining 4 are pending
216
+ await service.shutdown();
217
+
218
+ // In-flight job completes, pending jobs are discarded
219
+ expect(service._getSucceededCount()).toBe(1);
220
+ expect(service._getDroppedCount()).toBe(4);
221
+ });
222
+
223
+ test('shutdown does not cause concurrency spike', async () => {
224
+ const hashes: string[] = [];
225
+ for (let i = 0; i < 3; i++) {
226
+ hashes.push(await createCommit());
227
+ }
228
+
229
+ const service = new CommitEnrichmentService({
230
+ maxQueueSize: 10,
231
+ maxConcurrency: 1,
232
+ jobTimeoutMs: 5000,
233
+ maxRetries: 0,
234
+ });
235
+
236
+ for (const hash of hashes) {
237
+ service.enqueue({ workspaceDir: testDir, commitHash: hash, context: makeContext(), gitService });
238
+ }
239
+
240
+ await service.shutdown();
241
+
242
+ // Active workers should be 0 after shutdown
243
+ expect(service._getActiveWorkers()).toBe(0);
244
+ });
245
+
246
+ test('discards jobs enqueued after shutdown', async () => {
247
+ const commitHash = await createCommit();
248
+ const service = new CommitEnrichmentService({
249
+ maxQueueSize: 10,
250
+ maxConcurrency: 1,
251
+ jobTimeoutMs: 5000,
252
+ maxRetries: 0,
253
+ });
254
+
255
+ await service.shutdown();
256
+
257
+ // Enqueue after shutdown should be silently discarded
258
+ service.enqueue({
259
+ workspaceDir: testDir,
260
+ commitHash,
261
+ context: makeContext(),
262
+ gitService,
263
+ });
264
+
265
+ expect(service._getQueueSize()).toBe(0);
266
+ expect(service._getSucceededCount()).toBe(0);
267
+ });
268
+
269
+ test('multiple successful enrichments write separate git notes', async () => {
270
+ const hash1 = await createCommit();
271
+ const hash2 = await createCommit();
272
+
273
+ const service = new CommitEnrichmentService({
274
+ maxQueueSize: 10,
275
+ maxConcurrency: 1,
276
+ jobTimeoutMs: 5000,
277
+ maxRetries: 0,
278
+ });
279
+
280
+ service.enqueue({
281
+ workspaceDir: testDir,
282
+ commitHash: hash1,
283
+ context: makeContext({ turnNumber: 1 }),
284
+ gitService,
285
+ });
286
+ service.enqueue({
287
+ workspaceDir: testDir,
288
+ commitHash: hash2,
289
+ context: makeContext({ turnNumber: 2 }),
290
+ gitService,
291
+ });
292
+
293
+ // Wait for queue to drain before shutdown (avoids discarding pending jobs)
294
+ await waitForDrain(service, 5000);
295
+ await service.shutdown();
296
+
297
+ // Both notes should exist
298
+ const note1 = JSON.parse(execFileSync('git', ['notes', '--ref=vellum', 'show', hash1], {
299
+ cwd: testDir, encoding: 'utf-8',
300
+ }));
301
+ const note2 = JSON.parse(execFileSync('git', ['notes', '--ref=vellum', 'show', hash2], {
302
+ cwd: testDir, encoding: 'utf-8',
303
+ }));
304
+
305
+ expect(note1.turnNumber).toBe(1);
306
+ expect(note2.turnNumber).toBe(2);
307
+ expect(service._getSucceededCount()).toBe(2);
308
+ });
309
+
310
+ test('job timeout triggers retry with backoff then fails after max retries', async () => {
311
+ // Use a very short timeout so the real git notes write times out
312
+ const service = new CommitEnrichmentService({
313
+ maxQueueSize: 10,
314
+ maxConcurrency: 1,
315
+ jobTimeoutMs: 1, // 1ms timeout — will always time out
316
+ maxRetries: 2,
317
+ });
318
+
319
+ const commitHash = await createCommit();
320
+ service.enqueue({
321
+ workspaceDir: testDir,
322
+ commitHash,
323
+ context: makeContext(),
324
+ gitService,
325
+ });
326
+
327
+ // Wait for all retries to complete (initial + 2 retries, with backoff)
328
+ // Backoff: 1s after attempt 1, 2s after attempt 2 = ~3s total
329
+ // But since the job itself is very fast to time out, total time is dominated by backoff
330
+ await waitForDrain(service, 10000);
331
+ await service.shutdown();
332
+
333
+ // After 1 initial attempt + 2 retries (3 total), the job should be counted as failed
334
+ expect(service._getFailedCount()).toBe(1);
335
+ expect(service._getSucceededCount()).toBe(0);
336
+ }, 15000); // Allow up to 15s for backoff delays
337
+
338
+ test('queue overflow drop behavior is deterministic', async () => {
339
+ // With maxQueueSize=2 and maxConcurrency=1:
340
+ // - Job A starts processing immediately (in-flight)
341
+ // - Job B enters queue (size=1)
342
+ // - Job C enters queue (size=2)
343
+ // - Job D overflows: drops oldest (B), adds D → queue has [C, D]
344
+ // - Job E overflows: drops oldest (C), adds E → queue has [D, E]
345
+ const service = new CommitEnrichmentService({
346
+ maxQueueSize: 2,
347
+ maxConcurrency: 1,
348
+ jobTimeoutMs: 30000,
349
+ maxRetries: 0,
350
+ });
351
+
352
+ const hashA = await createCommit();
353
+ const hashB = await createCommit();
354
+ const hashC = await createCommit();
355
+ const hashD = await createCommit();
356
+ const hashE = await createCommit();
357
+
358
+ service.enqueue({ workspaceDir: testDir, commitHash: hashA, context: makeContext({ turnNumber: 1 }), gitService });
359
+ service.enqueue({ workspaceDir: testDir, commitHash: hashB, context: makeContext({ turnNumber: 2 }), gitService });
360
+ service.enqueue({ workspaceDir: testDir, commitHash: hashC, context: makeContext({ turnNumber: 3 }), gitService });
361
+ // No drops yet: A is in-flight, B and C in queue (size=2)
362
+ expect(service._getDroppedCount()).toBe(0);
363
+
364
+ service.enqueue({ workspaceDir: testDir, commitHash: hashD, context: makeContext({ turnNumber: 4 }), gitService });
365
+ // Queue was full (2), so oldest (B) was dropped
366
+ expect(service._getDroppedCount()).toBe(1);
367
+
368
+ service.enqueue({ workspaceDir: testDir, commitHash: hashE, context: makeContext({ turnNumber: 5 }), gitService });
369
+ // Queue was full again (2), so oldest (C) was dropped
370
+ expect(service._getDroppedCount()).toBe(2);
371
+
372
+ // Queue should have exactly 2 items: D and E
373
+ expect(service._getQueueSize()).toBe(2);
374
+
375
+ await service.shutdown();
376
+
377
+ // A was in-flight and completed; D and E were pending and discarded at shutdown
378
+ expect(service._getSucceededCount()).toBe(1);
379
+ // 2 overflow drops + 2 shutdown discards = 4 total
380
+ expect(service._getDroppedCount()).toBe(4);
381
+ });
382
+
383
+ test('timed-out enrichment work is cancelled via AbortSignal', async () => {
384
+ // Track whether the slow enrichment work actually ran to completion
385
+ let enrichmentCompleted = false;
386
+ const commitHash = await createCommit();
387
+
388
+ const service = new CommitEnrichmentService({
389
+ maxQueueSize: 10,
390
+ maxConcurrency: 1,
391
+ jobTimeoutMs: 50, // Very short timeout
392
+ maxRetries: 0,
393
+ });
394
+
395
+ // Monkey-patch writeNote to simulate slow work that respects the abort signal.
396
+ // The real writeNote now passes the signal to execFileAsync which kills the
397
+ // child process on abort. This mock replicates that behavior by rejecting
398
+ // when the signal fires.
399
+ const originalWriteNote = gitService.writeNote.bind(gitService);
400
+ gitService.writeNote = async (_hash: string, _note: string, signal?: AbortSignal) => {
401
+ // Simulate slow work that is cancellable via AbortSignal
402
+ await new Promise<void>((resolve, reject) => {
403
+ const timer = setTimeout(() => {
404
+ enrichmentCompleted = true;
405
+ resolve();
406
+ }, 2000);
407
+ signal?.addEventListener('abort', () => {
408
+ clearTimeout(timer);
409
+ reject(new Error('aborted'));
410
+ }, { once: true });
411
+ });
412
+ };
413
+
414
+ service.enqueue({
415
+ workspaceDir: testDir,
416
+ commitHash,
417
+ context: makeContext(),
418
+ gitService,
419
+ });
420
+
421
+ await waitForDrain(service, 5000);
422
+ await service.shutdown();
423
+
424
+ // Allow any zombie work to settle — if abort didn't work, the 2s timer
425
+ // would still be running and would set enrichmentCompleted=true. Wait
426
+ // longer than the 2000ms mock delay to reliably catch the regression.
427
+ await new Promise(resolve => setTimeout(resolve, 2500));
428
+
429
+ // The job should have timed out and been counted as failed
430
+ expect(service._getFailedCount()).toBe(1);
431
+ expect(service._getSucceededCount()).toBe(0);
432
+ // The slow enrichment work should NOT have completed since the signal was aborted
433
+ expect(enrichmentCompleted).toBe(false);
434
+
435
+ // Restore original
436
+ gitService.writeNote = originalWriteNote;
437
+ });
438
+
439
+ test('shutdown does not hang on timed-out jobs', async () => {
440
+ const commitHash = await createCommit();
441
+
442
+ const service = new CommitEnrichmentService({
443
+ maxQueueSize: 10,
444
+ maxConcurrency: 1,
445
+ jobTimeoutMs: 50, // Short timeout
446
+ maxRetries: 0,
447
+ });
448
+
449
+ // Make writeNote artificially slow so the job will always time out.
450
+ // The mock respects the abort signal so the subprocess is killed on timeout.
451
+ const originalWriteNote = gitService.writeNote.bind(gitService);
452
+ gitService.writeNote = async (_hash: string, _note: string, signal?: AbortSignal) => {
453
+ await new Promise<void>((resolve, reject) => {
454
+ const timer = setTimeout(resolve, 5000);
455
+ signal?.addEventListener('abort', () => {
456
+ clearTimeout(timer);
457
+ reject(new Error('aborted'));
458
+ }, { once: true });
459
+ });
460
+ };
461
+
462
+ service.enqueue({
463
+ workspaceDir: testDir,
464
+ commitHash,
465
+ context: makeContext(),
466
+ gitService,
467
+ });
468
+
469
+ // Shutdown should complete promptly, not hang for 5s waiting on the slow writeNote
470
+ const shutdownStart = Date.now();
471
+ await service.shutdown();
472
+ const shutdownElapsed = Date.now() - shutdownStart;
473
+
474
+ // Shutdown should complete well under the 5s slow-work duration
475
+ expect(shutdownElapsed).toBeLessThan(3000);
476
+ expect(service._getFailedCount()).toBe(1);
477
+
478
+ gitService.writeNote = originalWriteNote;
479
+ }, 10000);
480
+
481
+ test('abort signal is triggered on non-timeout errors before retry', async () => {
482
+ const commitHash = await createCommit();
483
+
484
+ const service = new CommitEnrichmentService({
485
+ maxQueueSize: 10,
486
+ maxConcurrency: 1,
487
+ jobTimeoutMs: 5000,
488
+ maxRetries: 0,
489
+ });
490
+
491
+ // Make writeNote throw an error and observe whether the signal gets aborted
492
+ const originalWriteNote = gitService.writeNote.bind(gitService);
493
+ gitService.writeNote = async (_hash: string, _note: string) => {
494
+ // Set up a listener on the abort controller's signal to track abortion.
495
+ // We access the signal indirectly by throwing, which triggers the catch
496
+ // block in executeJob where controller.abort() is called.
497
+ throw new Error('Simulated writeNote failure');
498
+ };
499
+
500
+ service.enqueue({
501
+ workspaceDir: testDir,
502
+ commitHash,
503
+ context: makeContext(),
504
+ gitService,
505
+ });
506
+
507
+ await waitForDrain(service, 5000);
508
+ await service.shutdown();
509
+
510
+ // The job should have failed (no retries configured)
511
+ expect(service._getFailedCount()).toBe(1);
512
+ expect(service._getSucceededCount()).toBe(0);
513
+
514
+ gitService.writeNote = originalWriteNote;
515
+ });
516
+
517
+ test('enqueue is fire-and-forget and never throws even when called rapidly', async () => {
518
+ const service = new CommitEnrichmentService({
519
+ maxQueueSize: 3,
520
+ maxConcurrency: 1,
521
+ jobTimeoutMs: 5000,
522
+ maxRetries: 0,
523
+ });
524
+
525
+ const hashes: string[] = [];
526
+ for (let i = 0; i < 5; i++) {
527
+ hashes.push(await createCommit());
528
+ }
529
+
530
+ // Rapidly enqueue more jobs than the queue can hold — must never throw
531
+ const fn = () => {
532
+ for (const hash of hashes) {
533
+ service.enqueue({
534
+ workspaceDir: testDir,
535
+ commitHash: hash,
536
+ context: makeContext(),
537
+ gitService,
538
+ });
539
+ }
540
+ };
541
+
542
+ expect(fn).not.toThrow();
543
+
544
+ // Some jobs should have been dropped due to overflow (queue size 3, 1 in-flight)
545
+ // 5 jobs: 1 in-flight + 3 queue + 1 overflow = at least 1 drop
546
+ expect(service._getDroppedCount()).toBeGreaterThanOrEqual(1);
547
+
548
+ await service.shutdown();
549
+ });
550
+ });