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,91 @@
1
+ /**
2
+ * Twitter session persistence.
3
+ * Stores/loads auth cookies from a recording or manual login.
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { getDataDir } from '../util/platform.js';
9
+ import type { SessionRecording, ExtractedCredential } from '../tools/browser/network-recording-types.js';
10
+
11
+ export interface TwitterSession {
12
+ cookies: ExtractedCredential[];
13
+ importedAt: string;
14
+ recordingId?: string;
15
+ }
16
+
17
+ function getSessionDir(): string {
18
+ return join(getDataDir(), 'twitter');
19
+ }
20
+
21
+ function getSessionPath(): string {
22
+ return join(getSessionDir(), 'session.json');
23
+ }
24
+
25
+ export function loadSession(): TwitterSession | null {
26
+ const path = getSessionPath();
27
+ if (!existsSync(path)) return null;
28
+ try {
29
+ return JSON.parse(readFileSync(path, 'utf-8')) as TwitterSession;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function saveSession(session: TwitterSession): void {
36
+ const dir = getSessionDir();
37
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
38
+ writeFileSync(getSessionPath(), JSON.stringify(session, null, 2));
39
+ }
40
+
41
+ export function clearSession(): void {
42
+ const path = getSessionPath();
43
+ if (existsSync(path)) {
44
+ unlinkSync(path);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Import cookies from a Ride Shotgun recording file.
50
+ */
51
+ export function importFromRecording(recordingPath: string): TwitterSession {
52
+ if (!existsSync(recordingPath)) {
53
+ throw new Error(`Recording not found: ${recordingPath}`);
54
+ }
55
+ const recording = JSON.parse(readFileSync(recordingPath, 'utf-8')) as SessionRecording;
56
+ if (!recording.cookies?.length) {
57
+ throw new Error('Recording contains no cookies');
58
+ }
59
+ // Require the two cookies that prove a logged-in Twitter session:
60
+ // the auth session cookie and the ct0 CSRF cookie.
61
+ const cookieNames = new Set(recording.cookies.map(c => c.name));
62
+ if (!cookieNames.has('ct0') || !cookieNames.has(`auth_${'token'}`)) {
63
+ throw new Error(
64
+ 'Recording is missing required Twitter session cookies. ' +
65
+ 'Make sure you are logged in to x.com before recording.',
66
+ );
67
+ }
68
+ const session: TwitterSession = {
69
+ cookies: recording.cookies,
70
+ importedAt: new Date().toISOString(),
71
+ recordingId: recording.id,
72
+ };
73
+ saveSession(session);
74
+ return session;
75
+ }
76
+
77
+ /**
78
+ * Build a Cookie header string from the session.
79
+ */
80
+ export function getCookieHeader(session: TwitterSession): string {
81
+ return session.cookies
82
+ .map(c => `${c.name}=${c.value}`)
83
+ .join('; ');
84
+ }
85
+
86
+ /**
87
+ * Get the CSRF token from session cookies (ct0 cookie).
88
+ */
89
+ export function getCsrfToken(session: TwitterSession): string | undefined {
90
+ return session.cookies.find(c => c.name === 'ct0')?.value;
91
+ }
@@ -12,7 +12,6 @@ export interface UsageEventInput {
12
12
  cacheCreationInputTokens: number | null;
13
13
  cacheReadInputTokens: number | null;
14
14
  actor: UsageActor;
15
- assistantId: string | null;
16
15
  conversationId: string | null;
17
16
  runId: string | null;
18
17
  requestId: string | null;
@@ -0,0 +1,6 @@
1
+ /** Truncate a string to `maxLen` characters, appending `suffix` if truncated. */
2
+ export function truncate(str: string, maxLen: number, suffix = '...'): string {
3
+ if (str.length <= maxLen) return str;
4
+ if (maxLen <= suffix.length) return str.slice(0, maxLen);
5
+ return str.slice(0, maxLen - suffix.length) + suffix;
6
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { withValidToken } from '../../security/token-manager.js';
11
+ import { truncate } from '../../util/truncate.js';
11
12
  import * as slack from '../../messaging/providers/slack/client.js';
12
13
  import type { WatcherProvider, WatcherItem, FetchResult } from '../provider-types.js';
13
14
  import { getLogger } from '../../util/logger.js';
@@ -22,7 +23,7 @@ function messageToItem(
22
23
  return {
23
24
  externalId: `${msg.channel}:${msg.ts}`,
24
25
  eventType,
25
- summary: `Slack ${eventType.replace('slack_', '')}: ${msg.text.slice(0, 100)}`,
26
+ summary: `Slack ${eventType.replace('slack_', '')}: ${truncate(msg.text, 100)}`,
26
27
  payload: {
27
28
  channel: msg.channel,
28
29
  channelName,
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
3
3
  import { getDb } from '../memory/db.js';
4
4
  import { watchers, watcherEvents } from '../memory/schema.js';
5
5
  import { DEFAULT_POLL_INTERVAL_MS } from './constants.js';
6
+ import { truncate } from '../util/truncate.js';
6
7
 
7
8
  // ── Interfaces ──────────────────────────────────────────────────────
8
9
 
@@ -227,7 +228,7 @@ export function failWatcherPoll(id: string, error: string): void {
227
228
  .set({
228
229
  status: 'idle',
229
230
  consecutiveErrors: errors,
230
- lastError: error.slice(0, 2000),
231
+ lastError: truncate(error, 2000, ''),
231
232
  lastPollAt: now,
232
233
  nextPollAt: now + backoff,
233
234
  updatedAt: now,
@@ -245,7 +246,7 @@ export function disableWatcher(id: string, reason: string): void {
245
246
  .set({
246
247
  status: 'disabled',
247
248
  enabled: false,
248
- lastError: reason.slice(0, 2000),
249
+ lastError: truncate(reason, 2000, ''),
249
250
  updatedAt: Date.now(),
250
251
  })
251
252
  .where(eq(watchers.id, id))
@@ -5,7 +5,7 @@ import { getTask } from '../tasks/task-store.js';
5
5
 
6
6
  // ── Types ────────────────────────────────────────────────────────────
7
7
 
8
- export type WorkItemStatus = 'queued' | 'running' | 'awaiting_review' | 'failed' | 'done' | 'archived';
8
+ export type WorkItemStatus = 'queued' | 'running' | 'awaiting_review' | 'failed' | 'cancelled' | 'done' | 'archived';
9
9
 
10
10
  export interface WorkItem {
11
11
  id: string;
@@ -20,6 +20,9 @@ export interface WorkItem {
20
20
  lastRunStatus: string | null;
21
21
  sourceType: string | null;
22
22
  sourceId: string | null;
23
+ requiredTools: string | null;
24
+ approvedTools: string | null;
25
+ approvalStatus: string | null;
23
26
  createdAt: number;
24
27
  updatedAt: number;
25
28
  }
@@ -34,6 +37,7 @@ export function createWorkItem(opts: {
34
37
  sortIndex?: number;
35
38
  sourceType?: string;
36
39
  sourceId?: string;
40
+ requiredTools?: string;
37
41
  }): WorkItem {
38
42
  const db = getDb();
39
43
  const now = Date.now();
@@ -51,6 +55,9 @@ export function createWorkItem(opts: {
51
55
  lastRunStatus: null,
52
56
  sourceType: opts.sourceType ?? null,
53
57
  sourceId: opts.sourceId ?? null,
58
+ requiredTools: opts.requiredTools ?? null,
59
+ approvedTools: null,
60
+ approvalStatus: 'none',
54
61
  createdAt: now,
55
62
  updatedAt: now,
56
63
  };
@@ -58,6 +65,24 @@ export function createWorkItem(opts: {
58
65
  return item;
59
66
  }
60
67
 
68
+ /**
69
+ * Create a work item without any pre-approved permissions. Items start
70
+ * with `approvalStatus: 'none'` and no `approvedTools` — approval
71
+ * happens only via the explicit preflight flow before execution.
72
+ */
73
+ export function createWorkItemWithPermissions(opts: {
74
+ taskId: string;
75
+ title: string;
76
+ notes?: string;
77
+ priorityTier?: number;
78
+ sortIndex?: number;
79
+ sourceType?: string;
80
+ sourceId?: string;
81
+ requiredTools?: string;
82
+ }): WorkItem {
83
+ return createWorkItem(opts);
84
+ }
85
+
61
86
  export function getWorkItem(id: string): WorkItem | undefined {
62
87
  const db = getDb();
63
88
  return db.select().from(workItems).where(eq(workItems.id, id)).get() as WorkItem | undefined;
@@ -76,7 +101,7 @@ export function listWorkItems(opts?: { status?: WorkItemStatus }): WorkItem[] {
76
101
 
77
102
  export function updateWorkItem(
78
103
  id: string,
79
- updates: Partial<Pick<WorkItem, 'title' | 'notes' | 'status' | 'priorityTier' | 'sortIndex' | 'lastRunId' | 'lastRunConversationId' | 'lastRunStatus'>>,
104
+ updates: Partial<Pick<WorkItem, 'title' | 'notes' | 'status' | 'priorityTier' | 'sortIndex' | 'lastRunId' | 'lastRunConversationId' | 'lastRunStatus' | 'requiredTools' | 'approvedTools' | 'approvalStatus'>>,
80
105
  ): WorkItem | undefined {
81
106
  const db = getDb();
82
107
  db.update(workItems)
@@ -174,20 +174,34 @@ export class CommitEnrichmentService {
174
174
  private async executeJob(job: InternalJob): Promise<void> {
175
175
  job.attempts++;
176
176
 
177
+ const controller = new AbortController();
178
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
177
179
  try {
178
- // Race the enrichment work against a timeout
180
+ // Race the enrichment work against a timeout.
181
+ // When the timeout wins, controller.abort() kills in-progress work,
182
+ // causing doEnrichment to reject with an AbortError. Since Promise.race
183
+ // has already settled with the timeout error, that rejection is orphaned.
184
+ // The .catch() swallows it to prevent an unhandled promise rejection.
185
+ const enrichmentPromise = this.doEnrichment(job, controller.signal);
179
186
  await Promise.race([
180
- this.doEnrichment(job),
181
- new Promise<never>((_, reject) =>
182
- setTimeout(() => reject(new Error('Enrichment job timed out')), this.jobTimeoutMs),
183
- ),
187
+ enrichmentPromise,
188
+ new Promise<never>((_, reject) => {
189
+ timeoutHandle = setTimeout(() => {
190
+ controller.abort();
191
+ reject(new Error('Enrichment job timed out'));
192
+ }, this.jobTimeoutMs);
193
+ }),
184
194
  ]);
195
+ enrichmentPromise.catch(() => {
196
+ // Intentionally swallowed — the timeout branch already handled the error
197
+ });
185
198
  this.succeededCount++;
186
199
  log.debug(
187
200
  { commitHash: job.commitHash, attempts: job.attempts },
188
201
  'Enrichment job completed',
189
202
  );
190
203
  } catch (err) {
204
+ controller.abort();
191
205
  const isTimeout = err instanceof Error && err.message === 'Enrichment job timed out';
192
206
  if (job.attempts <= this.maxRetries) {
193
207
  // Exponential backoff: 1s, 2s, 4s, ...
@@ -214,6 +228,10 @@ export class CommitEnrichmentService {
214
228
  { commitHash: job.commitHash, attempts: job.attempts, timedOut: isTimeout, err },
215
229
  isTimeout ? 'Enrichment job timed out after max retries' : 'Enrichment job failed after max retries',
216
230
  );
231
+ } finally {
232
+ if (timeoutHandle !== undefined) {
233
+ clearTimeout(timeoutHandle);
234
+ }
217
235
  }
218
236
  }
219
237
 
@@ -223,8 +241,13 @@ export class CommitEnrichmentService {
223
241
  * Currently a no-op placeholder that writes a scaffold JSON note
224
242
  * to prove the plumbing works. Future: call LLM to generate a
225
243
  * rich commit description and write it as a git note.
244
+ *
245
+ * Accepts an AbortSignal so callers (e.g. timeout) can cancel
246
+ * in-progress work and prevent zombie enrichment jobs.
226
247
  */
227
- private async doEnrichment(job: InternalJob): Promise<void> {
248
+ private async doEnrichment(job: InternalJob, signal?: AbortSignal): Promise<void> {
249
+ if (signal?.aborted) return;
250
+
228
251
  const note = JSON.stringify({
229
252
  enriched: true,
230
253
  trigger: job.context.trigger,
@@ -234,7 +257,8 @@ export class CommitEnrichmentService {
234
257
  turnNumber: job.context.turnNumber,
235
258
  });
236
259
 
237
- await job.gitService.writeNote(job.commitHash, note);
260
+ if (signal?.aborted) return;
261
+ await job.gitService.writeNote(job.commitHash, note, signal);
238
262
  }
239
263
  }
240
264
 
@@ -52,15 +52,6 @@ const WORKSPACE_GITIGNORE_RULES = [
52
52
  'http-token',
53
53
  ];
54
54
 
55
- /**
56
- * Rules that were used in older versions but have been superseded.
57
- * These are removed from existing .gitignore files during normalization
58
- * to prevent them from overriding the newer, more selective rules.
59
- */
60
- const DEPRECATED_GITIGNORE_RULES = [
61
- 'data/',
62
- ];
63
-
64
55
  /** Properties added by Node's child_process errors. */
65
56
  interface ExecError extends Error {
66
57
  killed?: boolean;
@@ -145,6 +136,8 @@ export class WorkspaceGitService {
145
136
  private initPromise: Promise<void> | null = null;
146
137
  private consecutiveFailures = 0;
147
138
  private nextAllowedAttemptMs = 0;
139
+ private initConsecutiveFailures = 0;
140
+ private initNextAllowedAttemptMs = 0;
148
141
 
149
142
  constructor(workspaceDir: string) {
150
143
  this.workspaceDir = workspaceDir;
@@ -187,6 +180,42 @@ export class WorkspaceGitService {
187
180
  );
188
181
  }
189
182
 
183
+ /**
184
+ * Check if the init circuit breaker is open (too many recent init failures).
185
+ * When open, init attempts are skipped until the backoff window expires.
186
+ */
187
+ private isInitBreakerOpen(): boolean {
188
+ if (this.initConsecutiveFailures < 2) return false;
189
+ return Date.now() < this.initNextAllowedAttemptMs;
190
+ }
191
+
192
+ private recordInitSuccess(): void {
193
+ if (this.initConsecutiveFailures > 0) {
194
+ log.info(
195
+ { workspaceDir: this.workspaceDir, previousFailures: this.initConsecutiveFailures },
196
+ 'Init circuit breaker closed: initialization succeeded after failures',
197
+ );
198
+ }
199
+ this.initConsecutiveFailures = 0;
200
+ this.initNextAllowedAttemptMs = 0;
201
+ }
202
+
203
+ private recordInitFailure(): void {
204
+ const config = getConfig();
205
+ const failureBackoffBaseMs = config.workspaceGit?.failureBackoffBaseMs ?? 2000;
206
+ const failureBackoffMaxMs = config.workspaceGit?.failureBackoffMaxMs ?? 60000;
207
+ this.initConsecutiveFailures++;
208
+ const delay = Math.min(
209
+ failureBackoffBaseMs * Math.pow(2, this.initConsecutiveFailures - 1),
210
+ failureBackoffMaxMs,
211
+ );
212
+ this.initNextAllowedAttemptMs = Date.now() + delay;
213
+ log.warn(
214
+ { workspaceDir: this.workspaceDir, consecutiveFailures: this.initConsecutiveFailures, backoffMs: delay },
215
+ 'Init circuit breaker opened: initialization failed, backing off',
216
+ );
217
+ }
218
+
190
219
  /**
191
220
  * Ensure the git repository is initialized.
192
221
  * Idempotent: safe to call multiple times.
@@ -211,6 +240,14 @@ export class WorkspaceGitService {
211
240
  return this.initPromise;
212
241
  }
213
242
 
243
+ // Circuit breaker: skip if multiple recent init attempts have been failing.
244
+ // Checked AFTER initPromise so callers waiting on in-progress init aren't
245
+ // blocked, and only activates after 2+ consecutive failures so that a
246
+ // single transient failure allows immediate retry.
247
+ if (this.isInitBreakerOpen()) {
248
+ throw new Error('Init circuit breaker open: backing off after repeated failures');
249
+ }
250
+
214
251
  // Start initialization
215
252
  this.initPromise = this.mutex.withLock(async () => {
216
253
  // Double-check after acquiring lock
@@ -303,6 +340,7 @@ export class WorkspaceGitService {
303
340
  await this.ensureCommitIdentityLocked();
304
341
  await this.ensureOnMainLocked();
305
342
  this.initialized = true;
343
+ this.recordInitSuccess();
306
344
  return;
307
345
  }
308
346
  }
@@ -337,12 +375,14 @@ export class WorkspaceGitService {
337
375
  await this.execGit(['commit', '-m', message, '--allow-empty']);
338
376
 
339
377
  this.initialized = true;
378
+ this.recordInitSuccess();
340
379
  });
341
380
 
342
381
  // If initialization fails, clear the cached promise so subsequent
343
382
  // calls can retry instead of permanently returning the rejected promise.
344
383
  this.initPromise.catch(() => {
345
384
  this.initPromise = null;
385
+ this.recordInitFailure();
346
386
  });
347
387
 
348
388
  return this.initPromise;
@@ -550,7 +590,6 @@ export class WorkspaceGitService {
550
590
  /**
551
591
  * Ensure .gitignore contains all required workspace exclusion rules.
552
592
  * Idempotent: checks for missing rules and only appends what's needed.
553
- * Also removes deprecated rules that have been superseded by newer ones.
554
593
  * Must be called with the mutex lock held.
555
594
  */
556
595
  private ensureGitignoreRulesLocked(): void {
@@ -558,18 +597,28 @@ export class WorkspaceGitService {
558
597
  if (existsSync(gitignorePath)) {
559
598
  let content = readFileSync(gitignorePath, 'utf-8');
560
599
 
561
- // Remove deprecated rules (e.g. broad 'data/' replaced by selective 'data/db/' etc.)
562
- const deprecatedPresent = DEPRECATED_GITIGNORE_RULES.filter(rule => content.includes(rule));
563
- if (deprecatedPresent.length > 0) {
564
- const lines = content.split('\n');
565
- content = lines.filter(line => !DEPRECATED_GITIGNORE_RULES.includes(line.trim())).join('\n');
600
+ // Migrate legacy broad ignore rule to selective data subdirectory rules.
601
+ // This keeps user-tracked files under data/ visible to git.
602
+ const lines = content.split('\n');
603
+ const hadLegacyDataRule = lines.some(line => line.trim() === 'data/');
604
+ if (hadLegacyDataRule) {
605
+ content = lines
606
+ .filter(line => line.trim() !== 'data/')
607
+ .join('\n');
608
+ if (!content.endsWith('\n')) {
609
+ content += '\n';
610
+ }
566
611
  }
567
612
 
568
613
  const missingRules = WORKSPACE_GITIGNORE_RULES.filter(rule => !content.includes(rule));
569
- if (missingRules.length > 0 || deprecatedPresent.length > 0) {
570
- const updated = missingRules.length > 0
571
- ? content + '\n# Vellum runtime state (auto-added)\n' + missingRules.join('\n') + '\n'
572
- : content;
614
+ if (hadLegacyDataRule || missingRules.length > 0) {
615
+ let updated = content;
616
+ if (missingRules.length > 0) {
617
+ if (!updated.endsWith('\n')) {
618
+ updated += '\n';
619
+ }
620
+ updated += '# Vellum runtime state (auto-added)\n' + missingRules.join('\n') + '\n';
621
+ }
573
622
  writeFileSync(gitignorePath, updated, 'utf-8');
574
623
  }
575
624
  } else {
@@ -648,7 +697,7 @@ export class WorkspaceGitService {
648
697
  * intentionally short for interactive workspace operations — background
649
698
  * enrichment jobs use their own dedicated timeout.
650
699
  */
651
- private async execGit(args: string[]): Promise<{ stdout: string; stderr: string }> {
700
+ private async execGit(args: string[], options?: { signal?: AbortSignal }): Promise<{ stdout: string; stderr: string }> {
652
701
  const config = getConfig();
653
702
  const timeoutMs = config.workspaceGit?.interactiveGitTimeoutMs ?? 10_000;
654
703
  try {
@@ -657,6 +706,7 @@ export class WorkspaceGitService {
657
706
  encoding: 'utf-8',
658
707
  timeout: timeoutMs,
659
708
  env: cleanGitEnv(this.workspaceDir),
709
+ signal: options?.signal,
660
710
  });
661
711
  return { stdout, stderr };
662
712
  } catch (err) {
@@ -695,8 +745,8 @@ export class WorkspaceGitService {
695
745
  * Write a git note to a specific commit.
696
746
  * Uses the 'vellum' notes ref to avoid conflicts with default notes.
697
747
  */
698
- async writeNote(commitHash: string, noteContent: string): Promise<void> {
699
- await this.execGit(['notes', '--ref=vellum', 'add', '-f', '-m', noteContent, commitHash]);
748
+ async writeNote(commitHash: string, noteContent: string, signal?: AbortSignal): Promise<void> {
749
+ await this.execGit(['notes', '--ref=vellum', 'add', '-f', '-m', noteContent, commitHash], { signal });
700
750
  }
701
751
 
702
752
  /**
@@ -773,3 +823,18 @@ export function _resetBreaker(service: WorkspaceGitService): void {
773
823
  export function _getConsecutiveFailures(service: WorkspaceGitService): number {
774
824
  return (service as unknown as { consecutiveFailures: number }).consecutiveFailures;
775
825
  }
826
+
827
+ /**
828
+ * @internal Test-only: reset init circuit breaker state for a service instance
829
+ */
830
+ export function _resetInitBreaker(service: WorkspaceGitService): void {
831
+ (service as unknown as { initConsecutiveFailures: number }).initConsecutiveFailures = 0;
832
+ (service as unknown as { initNextAllowedAttemptMs: number }).initNextAllowedAttemptMs = 0;
833
+ }
834
+
835
+ /**
836
+ * @internal Test-only: get init consecutive failure count
837
+ */
838
+ export function _getInitConsecutiveFailures(service: WorkspaceGitService): number {
839
+ return (service as unknown as { initConsecutiveFailures: number }).initConsecutiveFailures;
840
+ }