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,329 @@
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(), 'scheduler-recurrence-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
+ migrateToDataLayout: () => {},
19
+ migrateToWorkspaceLayout: () => {},
20
+ }));
21
+
22
+ mock.module('../util/logger.js', () => ({
23
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
24
+ get: () => () => {},
25
+ }),
26
+ }));
27
+
28
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
29
+ import { createTask } from '../tasks/task-store.js';
30
+ import {
31
+ createSchedule,
32
+ getSchedule,
33
+ getScheduleRuns,
34
+ } from '../schedule/schedule-store.js';
35
+ import { startScheduler } from '../schedule/scheduler.js';
36
+
37
+ initializeDb();
38
+
39
+ /** Access the underlying bun:sqlite Database for raw parameterized queries. */
40
+ function getRawDb(): import('bun:sqlite').Database {
41
+ return (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
42
+ }
43
+
44
+ /** Force a schedule to be due by setting next_run_at in the past. */
45
+ function forceScheduleDue(scheduleId: string): void {
46
+ getRawDb().run('UPDATE cron_jobs SET next_run_at = ? WHERE id = ?', [Date.now() - 1000, scheduleId]);
47
+ }
48
+
49
+ afterAll(() => {
50
+ resetDb();
51
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
52
+ });
53
+
54
+ // Build an RRULE expression anchored at the given start date, recurring every minute.
55
+ // This ensures the rule always has future occurrences relative to the test clock.
56
+ function buildEveryMinuteRrule(dtstart: Date = new Date()): string {
57
+ const pad = (n: number) => String(n).padStart(2, '0');
58
+ const ds = `${dtstart.getUTCFullYear()}${pad(dtstart.getUTCMonth() + 1)}${pad(dtstart.getUTCDate())}T${pad(dtstart.getUTCHours())}${pad(dtstart.getUTCMinutes())}${pad(dtstart.getUTCSeconds())}Z`;
59
+ return `DTSTART:${ds}\nRRULE:FREQ=MINUTELY;INTERVAL=1`;
60
+ }
61
+
62
+ // Build an RRULE expression that ended in the past (UNTIL already passed).
63
+ function buildEndedRrule(): string {
64
+ const past = new Date(Date.now() - 86_400_000 * 30); // 30 days ago
65
+ const until = new Date(Date.now() - 86_400_000); // 1 day ago
66
+ const pad = (n: number) => String(n).padStart(2, '0');
67
+ const ds = `${past.getUTCFullYear()}${pad(past.getUTCMonth() + 1)}${pad(past.getUTCDate())}T000000Z`;
68
+ const us = `${until.getUTCFullYear()}${pad(until.getUTCMonth() + 1)}${pad(until.getUTCDate())}T235959Z`;
69
+ return `DTSTART:${ds}\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL=${us}`;
70
+ }
71
+
72
+ // ── RRULE schedule fires through the scheduler ──────────────────────
73
+
74
+ describe('scheduler RRULE execution', () => {
75
+ beforeEach(() => {
76
+ const db = getDb();
77
+ db.run('DELETE FROM cron_runs');
78
+ db.run('DELETE FROM cron_jobs');
79
+ db.run('DELETE FROM task_runs');
80
+ db.run('DELETE FROM tasks');
81
+ db.run('DELETE FROM messages');
82
+ db.run('DELETE FROM conversations');
83
+ });
84
+
85
+ test('RRULE schedule fires and creates cron_runs entry', async () => {
86
+ const rruleExpr = buildEveryMinuteRrule();
87
+ const schedule = createSchedule({
88
+ name: 'RRULE Test',
89
+ cronExpression: rruleExpr,
90
+ message: 'Hello from RRULE',
91
+ syntax: 'rrule',
92
+ expression: rruleExpr,
93
+ });
94
+
95
+ // Verify it was stored with rrule syntax
96
+ const stored = getSchedule(schedule.id);
97
+ expect(stored).not.toBeNull();
98
+ expect(stored!.syntax).toBe('rrule');
99
+
100
+ // Force it to be due
101
+ forceScheduleDue(schedule.id);
102
+
103
+ const processedMessages: { conversationId: string; message: string }[] = [];
104
+ const processMessage = async (conversationId: string, message: string) => {
105
+ processedMessages.push({ conversationId, message });
106
+ };
107
+
108
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
109
+ await new Promise(resolve => setTimeout(resolve, 500));
110
+ scheduler.stop();
111
+
112
+ // processMessage should have been called with the RRULE message
113
+ expect(processedMessages.some(m => m.message === 'Hello from RRULE')).toBe(true);
114
+
115
+ // A cron_runs entry should have been created
116
+ const runs = getScheduleRuns(schedule.id);
117
+ expect(runs.length).toBeGreaterThanOrEqual(1);
118
+ expect(runs[0].status).toBe('ok');
119
+ });
120
+
121
+ test('RRULE run_task:<id> triggers task runner', async () => {
122
+ const task = createTask({
123
+ title: 'RRULE Task',
124
+ template: 'Execute RRULE task',
125
+ });
126
+
127
+ const rruleExpr = buildEveryMinuteRrule();
128
+ const schedule = createSchedule({
129
+ name: 'RRULE Task Schedule',
130
+ cronExpression: rruleExpr,
131
+ message: `run_task:${task.id}`,
132
+ syntax: 'rrule',
133
+ expression: rruleExpr,
134
+ });
135
+
136
+ forceScheduleDue(schedule.id);
137
+
138
+ const directCalls: { conversationId: string; message: string }[] = [];
139
+ const processMessage = async (conversationId: string, message: string) => {
140
+ directCalls.push({ conversationId, message });
141
+ };
142
+
143
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
144
+ await new Promise(resolve => setTimeout(resolve, 500));
145
+ scheduler.stop();
146
+
147
+ // runTask renders the template, so processMessage gets the template text
148
+ const runTaskCalls = directCalls.filter(c => c.message === 'Execute RRULE task');
149
+ const rawCalls = directCalls.filter(c => c.message.startsWith('run_task:'));
150
+
151
+ expect(runTaskCalls.length).toBe(1);
152
+ expect(rawCalls.length).toBe(0);
153
+
154
+ // A cron_runs entry should exist
155
+ const runs = getScheduleRuns(schedule.id);
156
+ expect(runs.length).toBeGreaterThanOrEqual(1);
157
+ });
158
+
159
+ test('ended RRULE (UNTIL in past) is not repeatedly claimed', async () => {
160
+ const endedExpr = buildEndedRrule();
161
+
162
+ // Insert directly via raw SQL because createSchedule would throw when
163
+ // computing nextRunAt for an already-ended RRULE. This simulates a
164
+ // schedule that was valid when created but has since expired.
165
+ const id = crypto.randomUUID();
166
+ const now = Date.now();
167
+ getRawDb().run(
168
+ `INSERT INTO cron_jobs (id, name, enabled, cron_expression, schedule_syntax, timezone, message, next_run_at, last_run_at, last_status, retry_count, created_by, created_at, updated_at)
169
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
170
+ [id, 'Ended RRULE', 1, endedExpr, 'rrule', null, 'Should not fire', now - 1000, null, null, 0, 'agent', now, now],
171
+ );
172
+
173
+ const processedMessages: string[] = [];
174
+ const processMessage = async (_conversationId: string, message: string) => {
175
+ processedMessages.push(message);
176
+ };
177
+
178
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
179
+ await new Promise(resolve => setTimeout(resolve, 500));
180
+ scheduler.stop();
181
+
182
+ // The ended RRULE should NOT have fired
183
+ expect(processedMessages).not.toContain('Should not fire');
184
+
185
+ // No runs should have been created
186
+ const runs = getScheduleRuns(id);
187
+ expect(runs.length).toBe(0);
188
+ });
189
+
190
+ test('existing cron schedule behavior is unchanged', async () => {
191
+ const schedule = createSchedule({
192
+ name: 'Cron Schedule',
193
+ cronExpression: '* * * * *',
194
+ message: 'Cron message',
195
+ });
196
+
197
+ // Verify it defaults to cron syntax
198
+ const stored = getSchedule(schedule.id);
199
+ expect(stored).not.toBeNull();
200
+ expect(stored!.syntax).toBe('cron');
201
+
202
+ forceScheduleDue(schedule.id);
203
+
204
+ const processedMessages: { conversationId: string; message: string }[] = [];
205
+ const processMessage = async (conversationId: string, message: string) => {
206
+ processedMessages.push({ conversationId, message });
207
+ };
208
+
209
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
210
+ await new Promise(resolve => setTimeout(resolve, 500));
211
+ scheduler.stop();
212
+
213
+ // processMessage should have been called with the cron message
214
+ expect(processedMessages.some(m => m.message === 'Cron message')).toBe(true);
215
+
216
+ // A cron_runs entry should have been created
217
+ const runs = getScheduleRuns(schedule.id);
218
+ expect(runs.length).toBeGreaterThanOrEqual(1);
219
+ expect(runs[0].status).toBe('ok');
220
+ });
221
+
222
+ test('RRULE set with EXDATE skips excluded occurrence and advances to next valid date', async () => {
223
+ // Build an RRULE set that fires every minute but excludes the next immediate occurrence.
224
+ // The scheduler should skip the excluded date and advance to the one after.
225
+ const now = new Date();
226
+ const pad = (n: number) => String(n).padStart(2, '0');
227
+
228
+ // DTSTART one hour ago so there are plenty of past occurrences
229
+ const pastDate = new Date(now.getTime() - 3_600_000);
230
+ const ds = `${pastDate.getUTCFullYear()}${pad(pastDate.getUTCMonth() + 1)}${pad(pastDate.getUTCDate())}T${pad(pastDate.getUTCHours())}${pad(pastDate.getUTCMinutes())}${pad(pastDate.getUTCSeconds())}Z`;
231
+
232
+ // Exclude the current minute's occurrence
233
+ const currentMinuteDate = new Date(now);
234
+ currentMinuteDate.setUTCSeconds(0);
235
+ currentMinuteDate.setUTCMilliseconds(0);
236
+ // Round to the previous minute boundary relative to dtstart
237
+ const exDate = `${currentMinuteDate.getUTCFullYear()}${pad(currentMinuteDate.getUTCMonth() + 1)}${pad(currentMinuteDate.getUTCDate())}T${pad(currentMinuteDate.getUTCHours())}${pad(currentMinuteDate.getUTCMinutes())}00Z`;
238
+
239
+ const expression = `DTSTART:${ds}\nRRULE:FREQ=MINUTELY;INTERVAL=1\nEXDATE:${exDate}`;
240
+
241
+ const schedule = createSchedule({
242
+ name: 'RRULE set EXDATE test',
243
+ cronExpression: expression,
244
+ message: 'Set exclusion test',
245
+ syntax: 'rrule',
246
+ expression,
247
+ });
248
+
249
+ // Force the schedule to be due
250
+ forceScheduleDue(schedule.id);
251
+
252
+ const processedMessages: string[] = [];
253
+ const processMessage = async (_conversationId: string, message: string) => {
254
+ processedMessages.push(message);
255
+ };
256
+
257
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
258
+ await new Promise(resolve => setTimeout(resolve, 500));
259
+ scheduler.stop();
260
+
261
+ // The schedule should have been claimed and nextRunAt advanced
262
+ const after = getSchedule(schedule.id);
263
+ expect(after).not.toBeNull();
264
+ expect(after!.lastRunAt).not.toBeNull();
265
+ // nextRunAt should be in the future (not the excluded date)
266
+ expect(after!.nextRunAt).toBeGreaterThan(Date.now() - 5000);
267
+ });
268
+
269
+ test('RRULE set schedule fires and creates cron_runs entry', async () => {
270
+ const expression = [
271
+ 'DTSTART:20250101T000000Z',
272
+ 'RRULE:FREQ=MINUTELY;INTERVAL=1',
273
+ 'EXDATE:20250101T000100Z',
274
+ ].join('\n');
275
+
276
+ const schedule = createSchedule({
277
+ name: 'Set schedule fire test',
278
+ cronExpression: expression,
279
+ message: 'Set fire test',
280
+ syntax: 'rrule',
281
+ expression,
282
+ });
283
+
284
+ forceScheduleDue(schedule.id);
285
+
286
+ const processedMessages: string[] = [];
287
+ const processMessage = async (_conversationId: string, message: string) => {
288
+ processedMessages.push(message);
289
+ };
290
+
291
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
292
+ await new Promise(resolve => setTimeout(resolve, 500));
293
+ scheduler.stop();
294
+
295
+ expect(processedMessages).toContain('Set fire test');
296
+
297
+ const runs = getScheduleRuns(schedule.id);
298
+ expect(runs.length).toBeGreaterThanOrEqual(1);
299
+ expect(runs[0].status).toBe('ok');
300
+ });
301
+
302
+ test('RRULE schedule advances nextRunAt after firing', async () => {
303
+ const rruleExpr = buildEveryMinuteRrule();
304
+ const schedule = createSchedule({
305
+ name: 'Advancing RRULE',
306
+ cronExpression: rruleExpr,
307
+ message: 'Advance test',
308
+ syntax: 'rrule',
309
+ expression: rruleExpr,
310
+ });
311
+
312
+ const originalNextRunAt = getSchedule(schedule.id)!.nextRunAt;
313
+ forceScheduleDue(schedule.id);
314
+ const forcedDueAt = getSchedule(schedule.id)!.nextRunAt;
315
+
316
+ const processMessage = async () => {};
317
+ const scheduler = startScheduler(processMessage, () => {}, () => {});
318
+ await new Promise(resolve => setTimeout(resolve, 500));
319
+ scheduler.stop();
320
+
321
+ const after = getSchedule(schedule.id);
322
+ expect(after).not.toBeNull();
323
+ // nextRunAt must have moved forward from the forced-due value
324
+ expect(after!.nextRunAt).toBeGreaterThan(forcedDueAt);
325
+ // It should be at or near the original computed value (within a few seconds tolerance)
326
+ expect(Math.abs(after!.nextRunAt - originalNextRunAt)).toBeLessThan(5000);
327
+ expect(after!.lastRunAt).not.toBeNull();
328
+ });
329
+ });
@@ -24,12 +24,12 @@ mock.module('../util/logger.js', () => ({
24
24
  }),
25
25
  }));
26
26
 
27
- import { initializeDb, getDb } from '../memory/db.js';
27
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
28
28
  import { renderHistoryContent, mergeToolResults } from '../daemon/handlers.js';
29
29
  import {
30
30
  uploadAttachment,
31
31
  linkAttachmentToMessage,
32
- getAttachmentsForMessageUnscoped,
32
+ getAttachmentsForMessage,
33
33
  } from '../memory/attachments-store.js';
34
34
  import {
35
35
  createConversation,
@@ -39,6 +39,7 @@ import {
39
39
  initializeDb();
40
40
 
41
41
  afterAll(() => {
42
+ resetDb();
42
43
  try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
43
44
  });
44
45
 
@@ -370,7 +371,7 @@ describe('mergeToolResults', () => {
370
371
  });
371
372
  });
372
373
 
373
- describe('getAttachmentsForMessageUnscoped', () => {
374
+ describe('getAttachmentsForMessage', () => {
374
375
  beforeEach(() => {
375
376
  const db = getDb();
376
377
  db.run('DELETE FROM message_attachments');
@@ -387,10 +388,10 @@ describe('getAttachmentsForMessageUnscoped', () => {
387
388
 
388
389
  test('returns attachments linked to a message', () => {
389
390
  const msgId = createMessage('assistant', 'Here is a chart');
390
- const stored = uploadAttachment('ast-1', 'chart.png', 'image/png', 'iVBOR');
391
+ const stored = uploadAttachment('chart.png', 'image/png', 'iVBOR');
391
392
  linkAttachmentToMessage(msgId, stored.id, 0);
392
393
 
393
- const result = getAttachmentsForMessageUnscoped(msgId);
394
+ const result = getAttachmentsForMessage(msgId);
394
395
  expect(result).toHaveLength(1);
395
396
  expect(result[0].id).toBe(stored.id);
396
397
  expect(result[0].originalFilename).toBe('chart.png');
@@ -399,32 +400,32 @@ describe('getAttachmentsForMessageUnscoped', () => {
399
400
  });
400
401
 
401
402
  test('returns empty array when no attachments are linked', () => {
402
- expect(getAttachmentsForMessageUnscoped('msg-nonexistent')).toEqual([]);
403
+ expect(getAttachmentsForMessage('msg-nonexistent')).toEqual([]);
403
404
  });
404
405
 
405
406
  test('returns multiple attachments in position order', () => {
406
407
  const msgId = createMessage('assistant', 'Two files');
407
- const a1 = uploadAttachment('ast-1', 'first.txt', 'text/plain', 'AAAA');
408
- const a2 = uploadAttachment('ast-1', 'second.txt', 'text/plain', 'BBBB');
408
+ const a1 = uploadAttachment('first.txt', 'text/plain', 'AAAA');
409
+ const a2 = uploadAttachment('second.txt', 'text/plain', 'BBBB');
409
410
 
410
411
  linkAttachmentToMessage(msgId, a2.id, 1);
411
412
  linkAttachmentToMessage(msgId, a1.id, 0);
412
413
 
413
- const result = getAttachmentsForMessageUnscoped(msgId);
414
+ const result = getAttachmentsForMessage(msgId);
414
415
  expect(result).toHaveLength(2);
415
416
  expect(result[0].originalFilename).toBe('first.txt');
416
417
  expect(result[1].originalFilename).toBe('second.txt');
417
418
  });
418
419
 
419
- test('works across different assistantIds', () => {
420
+ test('returns all attachments linked to a message', () => {
420
421
  const msgId = createMessage('assistant', 'Mixed');
421
- const a1 = uploadAttachment('ast-A', 'a.png', 'image/png', 'AAAA');
422
- const a2 = uploadAttachment('ast-B', 'b.png', 'image/png', 'BBBB');
422
+ const a1 = uploadAttachment('a.png', 'image/png', 'AAAA');
423
+ const a2 = uploadAttachment('b.png', 'image/png', 'BBBB');
423
424
 
424
425
  linkAttachmentToMessage(msgId, a1.id, 0);
425
426
  linkAttachmentToMessage(msgId, a2.id, 1);
426
427
 
427
- const result = getAttachmentsForMessageUnscoped(msgId);
428
+ const result = getAttachmentsForMessage(msgId);
428
429
  expect(result).toHaveLength(2);
429
430
  });
430
431
  });
@@ -117,6 +117,34 @@ describe('classifySessionError', () => {
117
117
  }
118
118
  });
119
119
 
120
+ describe('timeout errors (generic, not network/gateway)', () => {
121
+ const cases = [
122
+ 'timeout',
123
+ 'deadline exceeded',
124
+ 'request timed out',
125
+ ];
126
+
127
+ for (const msg of cases) {
128
+ it(`classifies "${msg}" as PROVIDER_API with timeout message`, () => {
129
+ const result = classifySessionError(new Error(msg), baseCtx);
130
+ expect(result.code).toBe('PROVIDER_API');
131
+ expect(result.userMessage).toContain('took too long');
132
+ expect(result.retryable).toBe(true);
133
+ });
134
+ }
135
+
136
+ it('does not steal "connection timeout" from PROVIDER_NETWORK', () => {
137
+ const result = classifySessionError(new Error('connection timeout'), baseCtx);
138
+ expect(result.code).toBe('PROVIDER_NETWORK');
139
+ });
140
+
141
+ it('does not steal "Gateway timeout" from PROVIDER_API', () => {
142
+ const result = classifySessionError(new Error('Gateway timeout'), baseCtx);
143
+ expect(result.code).toBe('PROVIDER_API');
144
+ expect(result.userMessage).toContain('returned an error');
145
+ });
146
+ });
147
+
120
148
  describe('context-too-large errors', () => {
121
149
  const cases = [
122
150
  'context_length_exceeded',