@desplega.ai/agent-swarm 1.20.0 → 1.49.0

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 (810) hide show
  1. package/.claude/settings.json +84 -0
  2. package/.claude/settings.local.json +3 -1
  3. package/.entire/settings.json +4 -0
  4. package/.env.docker.example +40 -23
  5. package/.env.example +39 -1
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +78 -0
  7. package/.github/ISSUE_TEMPLATE/community-template.yml +77 -0
  8. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  9. package/.github/ISSUE_TEMPLATE/feature_request.yml +60 -0
  10. package/.github/PULL_REQUEST_TEMPLATE/community-template.md +29 -0
  11. package/.github/workflows/ci.yml +3 -27
  12. package/.github/workflows/docker-and-deploy.yml +20 -5
  13. package/.github/workflows/merge-gate.yml +233 -0
  14. package/.opencode/plugins/entire.ts +133 -0
  15. package/.superset/config.json +6 -0
  16. package/.wts-setup.ts +103 -34
  17. package/CHANGELOG.md +447 -0
  18. package/CLAUDE.md +422 -5
  19. package/CONTRIBUTING.md +46 -1
  20. package/DEPLOYMENT.md +26 -9
  21. package/Dockerfile +15 -7
  22. package/Dockerfile.worker +99 -67
  23. package/MCP.md +452 -17
  24. package/README.md +271 -169
  25. package/api-entrypoint.sh +56 -0
  26. package/depot.json +1 -0
  27. package/docker-compose.example.yml +194 -3
  28. package/docker-compose.local.yml +119 -0
  29. package/docker-entrypoint.sh +393 -113
  30. package/docs-site/app/api/search/route.ts +4 -0
  31. package/docs-site/app/docs/[[...slug]]/page.tsx +87 -0
  32. package/docs-site/app/docs/layout.tsx +12 -0
  33. package/docs-site/app/globals.css +24 -0
  34. package/docs-site/app/layout.config.tsx +34 -0
  35. package/docs-site/app/layout.tsx +119 -0
  36. package/docs-site/app/llms-full.txt/route.ts +11 -0
  37. package/docs-site/app/llms.mdx/docs/[[...slug]]/route.ts +24 -0
  38. package/docs-site/app/llms.txt/route.ts +8 -0
  39. package/docs-site/app/page.tsx +5 -0
  40. package/docs-site/app/robots.ts +13 -0
  41. package/docs-site/app/sitemap.ts +37 -0
  42. package/docs-site/components/api-page.client.tsx +4 -0
  43. package/docs-site/components/api-page.tsx +7 -0
  44. package/docs-site/components/mdx/mermaid.tsx +55 -0
  45. package/docs-site/content/docs/(documentation)/architecture/agents.mdx +117 -0
  46. package/docs-site/content/docs/(documentation)/architecture/hooks.mdx +77 -0
  47. package/docs-site/content/docs/(documentation)/architecture/memory.mdx +96 -0
  48. package/docs-site/content/docs/(documentation)/architecture/meta.json +4 -0
  49. package/docs-site/content/docs/(documentation)/architecture/overview.mdx +172 -0
  50. package/docs-site/content/docs/(documentation)/concepts/epics.mdx +98 -0
  51. package/docs-site/content/docs/(documentation)/concepts/meta.json +4 -0
  52. package/docs-site/content/docs/(documentation)/concepts/scheduling.mdx +136 -0
  53. package/docs-site/content/docs/(documentation)/concepts/services.mdx +104 -0
  54. package/docs-site/content/docs/(documentation)/concepts/task-lifecycle.mdx +148 -0
  55. package/docs-site/content/docs/(documentation)/concepts/workflows.mdx +209 -0
  56. package/docs-site/content/docs/(documentation)/contributing.mdx +158 -0
  57. package/docs-site/content/docs/(documentation)/getting-started.mdx +157 -0
  58. package/docs-site/content/docs/(documentation)/guides/agentmail-integration.mdx +79 -0
  59. package/docs-site/content/docs/(documentation)/guides/deployment.mdx +171 -0
  60. package/docs-site/content/docs/(documentation)/guides/github-integration.mdx +81 -0
  61. package/docs-site/content/docs/(documentation)/guides/gitlab-integration.mdx +93 -0
  62. package/docs-site/content/docs/(documentation)/guides/linear-integration.mdx +98 -0
  63. package/docs-site/content/docs/(documentation)/guides/meta.json +13 -0
  64. package/docs-site/content/docs/(documentation)/guides/sentry-integration.mdx +52 -0
  65. package/docs-site/content/docs/(documentation)/guides/slack-integration.mdx +179 -0
  66. package/docs-site/content/docs/(documentation)/guides/x402-payments.mdx +154 -0
  67. package/docs-site/content/docs/(documentation)/index.mdx +65 -0
  68. package/docs-site/content/docs/(documentation)/meta.json +19 -0
  69. package/docs-site/content/docs/(documentation)/reference/cli.mdx +241 -0
  70. package/docs-site/content/docs/(documentation)/reference/environment-variables.mdx +205 -0
  71. package/docs-site/content/docs/(documentation)/reference/mcp-tools.mdx +449 -0
  72. package/docs-site/content/docs/(documentation)/reference/meta.json +4 -0
  73. package/docs-site/content/docs/api-reference/active-sessions.mdx +9 -0
  74. package/docs-site/content/docs/api-reference/agents.mdx +9 -0
  75. package/docs-site/content/docs/api-reference/channels.mdx +9 -0
  76. package/docs-site/content/docs/api-reference/config.mdx +9 -0
  77. package/docs-site/content/docs/api-reference/debug.mdx +9 -0
  78. package/docs-site/content/docs/api-reference/ecosystem.mdx +9 -0
  79. package/docs-site/content/docs/api-reference/epics.mdx +9 -0
  80. package/docs-site/content/docs/api-reference/index.mdx +32 -0
  81. package/docs-site/content/docs/api-reference/memory.mdx +9 -0
  82. package/docs-site/content/docs/api-reference/meta.json +25 -0
  83. package/docs-site/content/docs/api-reference/poll.mdx +9 -0
  84. package/docs-site/content/docs/api-reference/repos.mdx +9 -0
  85. package/docs-site/content/docs/api-reference/schedules.mdx +9 -0
  86. package/docs-site/content/docs/api-reference/session-data.mdx +9 -0
  87. package/docs-site/content/docs/api-reference/stats.mdx +9 -0
  88. package/docs-site/content/docs/api-reference/tasks.mdx +9 -0
  89. package/docs-site/content/docs/api-reference/trackers.mdx +9 -0
  90. package/docs-site/content/docs/api-reference/webhooks.mdx +9 -0
  91. package/docs-site/content/docs/api-reference/workflows.mdx +9 -0
  92. package/docs-site/content/docs/meta.json +3 -0
  93. package/docs-site/lib/get-llm-text.ts +10 -0
  94. package/docs-site/lib/openapi.ts +23 -0
  95. package/docs-site/lib/source.ts +8 -0
  96. package/docs-site/mdx-components.tsx +13 -0
  97. package/docs-site/next.config.mjs +29 -0
  98. package/docs-site/package.json +35 -0
  99. package/docs-site/pnpm-lock.yaml +5407 -0
  100. package/docs-site/postcss.config.mjs +8 -0
  101. package/docs-site/scripts/generate-docs.ts +171 -0
  102. package/docs-site/source.config.ts +17 -0
  103. package/docs-site/tsconfig.json +46 -0
  104. package/ecosystem.config.cjs +4 -4
  105. package/landing/next.config.ts +14 -0
  106. package/landing/package.json +31 -0
  107. package/landing/pnpm-lock.yaml +1091 -0
  108. package/landing/postcss.config.mjs +8 -0
  109. package/landing/public/apple-touch-icon.png +0 -0
  110. package/landing/public/favicon.ico +0 -0
  111. package/landing/public/logo.png +0 -0
  112. package/landing/public/og-image.png +0 -0
  113. package/landing/public/omghost-desplega.svg +30 -0
  114. package/landing/public/omghost-openfort.svg +9 -0
  115. package/landing/src/app/actions/waitlist.ts +25 -0
  116. package/landing/src/app/blog/openfort-hackathon/page.tsx +863 -0
  117. package/landing/src/app/blog/page.tsx +162 -0
  118. package/landing/src/app/blog/swarm-metrics/page.tsx +685 -0
  119. package/landing/src/app/examples/page.tsx +174 -0
  120. package/landing/src/app/examples/x402/page.tsx +456 -0
  121. package/landing/src/app/globals.css +122 -0
  122. package/landing/src/app/layout.tsx +134 -0
  123. package/landing/src/app/page.tsx +27 -0
  124. package/landing/src/app/robots.ts +13 -0
  125. package/landing/src/app/sitemap.ts +44 -0
  126. package/landing/src/components/architecture.tsx +163 -0
  127. package/landing/src/components/cta.tsx +52 -0
  128. package/landing/src/components/features.tsx +160 -0
  129. package/landing/src/components/footer.tsx +100 -0
  130. package/landing/src/components/hero.tsx +217 -0
  131. package/landing/src/components/how-it-works.tsx +165 -0
  132. package/landing/src/components/navbar.tsx +147 -0
  133. package/landing/src/components/waitlist.tsx +110 -0
  134. package/landing/src/components/why-choose.tsx +149 -0
  135. package/landing/src/components/workshops.tsx +328 -0
  136. package/landing/src/lib/utils.ts +6 -0
  137. package/landing/tsconfig.json +41 -0
  138. package/misc/transcripts/2026-03-09-pi-mono-e2e-verification.md +154 -0
  139. package/new-ui/CLAUDE.md +92 -0
  140. package/new-ui/README.md +73 -0
  141. package/new-ui/biome.json +42 -0
  142. package/new-ui/components.json +21 -0
  143. package/new-ui/index.html +25 -0
  144. package/new-ui/package.json +49 -0
  145. package/new-ui/pnpm-lock.yaml +4845 -0
  146. package/new-ui/public/logo.png +0 -0
  147. package/new-ui/src/api/client.ts +814 -0
  148. package/new-ui/src/api/hooks/index.ts +64 -0
  149. package/new-ui/src/api/hooks/use-agents.ts +58 -0
  150. package/new-ui/src/api/hooks/use-channels.ts +115 -0
  151. package/new-ui/src/api/hooks/use-config-api.ts +46 -0
  152. package/new-ui/src/api/hooks/use-costs.ts +122 -0
  153. package/new-ui/src/api/hooks/use-db-query.ts +29 -0
  154. package/new-ui/src/api/hooks/use-epics.ts +75 -0
  155. package/new-ui/src/api/hooks/use-repos.ts +61 -0
  156. package/new-ui/src/api/hooks/use-schedules.ts +81 -0
  157. package/new-ui/src/api/hooks/use-services.ts +16 -0
  158. package/new-ui/src/api/hooks/use-stats.ts +27 -0
  159. package/new-ui/src/api/hooks/use-tasks.ts +89 -0
  160. package/new-ui/src/api/hooks/use-workflows.ts +109 -0
  161. package/{ui/src/types/api.ts → new-ui/src/api/types.ts} +236 -10
  162. package/new-ui/src/app/App.tsx +13 -0
  163. package/new-ui/src/app/providers.tsx +32 -0
  164. package/new-ui/src/app/router.tsx +52 -0
  165. package/new-ui/src/components/layout/app-header.tsx +47 -0
  166. package/new-ui/src/components/layout/app-sidebar.tsx +128 -0
  167. package/new-ui/src/components/layout/breadcrumbs.tsx +57 -0
  168. package/new-ui/src/components/layout/config-guard.tsx +22 -0
  169. package/new-ui/src/components/layout/root-layout.tsx +40 -0
  170. package/new-ui/src/components/layout/swarm-switcher.tsx +85 -0
  171. package/new-ui/src/components/shared/command-menu.tsx +131 -0
  172. package/new-ui/src/components/shared/data-grid.tsx +141 -0
  173. package/new-ui/src/components/shared/empty-state.tsx +24 -0
  174. package/new-ui/src/components/shared/error-boundary.tsx +72 -0
  175. package/new-ui/src/components/shared/json-viewer.tsx +47 -0
  176. package/new-ui/src/components/shared/name-connection-modal.tsx +99 -0
  177. package/new-ui/src/components/shared/page-skeleton.tsx +16 -0
  178. package/new-ui/src/components/shared/session-log-viewer.tsx +364 -0
  179. package/new-ui/src/components/shared/stats-bar.tsx +132 -0
  180. package/new-ui/src/components/shared/status-badge.tsx +131 -0
  181. package/new-ui/src/components/shared/usage-summary.tsx +179 -0
  182. package/new-ui/src/components/ui/alert-dialog.tsx +176 -0
  183. package/new-ui/src/components/ui/alert.tsx +60 -0
  184. package/new-ui/src/components/ui/avatar.tsx +96 -0
  185. package/new-ui/src/components/ui/badge.tsx +46 -0
  186. package/new-ui/src/components/ui/button.tsx +62 -0
  187. package/new-ui/src/components/ui/card.tsx +75 -0
  188. package/new-ui/src/components/ui/command.tsx +160 -0
  189. package/new-ui/src/components/ui/dialog.tsx +143 -0
  190. package/new-ui/src/components/ui/dropdown-menu.tsx +226 -0
  191. package/new-ui/src/components/ui/input.tsx +21 -0
  192. package/new-ui/src/components/ui/label.tsx +19 -0
  193. package/new-ui/src/components/ui/progress.tsx +26 -0
  194. package/new-ui/src/components/ui/scroll-area.tsx +54 -0
  195. package/new-ui/src/components/ui/select.tsx +175 -0
  196. package/new-ui/src/components/ui/separator.tsx +28 -0
  197. package/new-ui/src/components/ui/sheet.tsx +132 -0
  198. package/new-ui/src/components/ui/sidebar.tsx +691 -0
  199. package/new-ui/src/components/ui/skeleton.tsx +13 -0
  200. package/new-ui/src/components/ui/sonner.tsx +35 -0
  201. package/new-ui/src/components/ui/switch.tsx +33 -0
  202. package/new-ui/src/components/ui/table.tsx +92 -0
  203. package/new-ui/src/components/ui/tabs.tsx +79 -0
  204. package/new-ui/src/components/ui/textarea.tsx +18 -0
  205. package/new-ui/src/components/ui/tooltip.tsx +51 -0
  206. package/new-ui/src/components/workflows/action-node.tsx +53 -0
  207. package/new-ui/src/components/workflows/condition-node.tsx +50 -0
  208. package/new-ui/src/components/workflows/graph-utils.ts +124 -0
  209. package/new-ui/src/components/workflows/json-tree.tsx +189 -0
  210. package/new-ui/src/components/workflows/node-styles.ts +10 -0
  211. package/new-ui/src/components/workflows/step-detail-sheet.tsx +87 -0
  212. package/new-ui/src/components/workflows/trigger-node.tsx +41 -0
  213. package/new-ui/src/components/workflows/workflow-graph.tsx +65 -0
  214. package/{ui/src/hooks/useAutoScroll.ts → new-ui/src/hooks/use-auto-scroll.ts} +0 -1
  215. package/new-ui/src/hooks/use-config.ts +203 -0
  216. package/new-ui/src/hooks/use-keyboard-shortcuts.ts +41 -0
  217. package/new-ui/src/hooks/use-mobile.ts +19 -0
  218. package/new-ui/src/hooks/use-theme.ts +60 -0
  219. package/new-ui/src/lib/config.ts +188 -0
  220. package/new-ui/src/lib/slugs.ts +71 -0
  221. package/{ui → new-ui}/src/lib/utils.ts +32 -0
  222. package/new-ui/src/main.tsx +11 -0
  223. package/new-ui/src/pages/agents/[id]/page.tsx +492 -0
  224. package/new-ui/src/pages/agents/page.tsx +134 -0
  225. package/new-ui/src/pages/chat/page.tsx +674 -0
  226. package/new-ui/src/pages/config/page.tsx +1109 -0
  227. package/new-ui/src/pages/dashboard/page.tsx +454 -0
  228. package/new-ui/src/pages/debug/page.tsx +275 -0
  229. package/new-ui/src/pages/epics/[id]/page.tsx +809 -0
  230. package/new-ui/src/pages/epics/page.tsx +321 -0
  231. package/new-ui/src/pages/not-found/page.tsx +18 -0
  232. package/new-ui/src/pages/repos/page.tsx +369 -0
  233. package/new-ui/src/pages/schedules/[id]/page.tsx +664 -0
  234. package/new-ui/src/pages/schedules/page.tsx +477 -0
  235. package/new-ui/src/pages/services/page.tsx +128 -0
  236. package/new-ui/src/pages/tasks/[id]/page.tsx +670 -0
  237. package/new-ui/src/pages/tasks/page.tsx +592 -0
  238. package/new-ui/src/pages/usage/page.tsx +195 -0
  239. package/new-ui/src/pages/workflow-runs/[id]/page.tsx +363 -0
  240. package/new-ui/src/pages/workflows/[id]/page.tsx +417 -0
  241. package/new-ui/src/pages/workflows/page.tsx +266 -0
  242. package/new-ui/src/styles/ag-grid.css +36 -0
  243. package/new-ui/src/styles/globals.css +213 -0
  244. package/new-ui/test-results/.last-run.json +4 -0
  245. package/{ui/tsconfig.json → new-ui/tsconfig.app.json} +7 -4
  246. package/new-ui/tsconfig.json +4 -0
  247. package/new-ui/tsconfig.node.json +26 -0
  248. package/new-ui/vercel.json +4 -0
  249. package/new-ui/vite.config.ts +28 -0
  250. package/openapi.json +4436 -0
  251. package/package.json +31 -7
  252. package/plugin/build-pi-skills.ts +233 -0
  253. package/plugin/commands/close-issue.md +7 -3
  254. package/plugin/commands/create-pr.md +18 -12
  255. package/plugin/commands/implement-issue.md +7 -3
  256. package/plugin/commands/respond-github.md +8 -4
  257. package/plugin/commands/review-pr.md +44 -10
  258. package/plugin/commands/start-leader.md +1 -3
  259. package/plugin/commands/start-worker.md +1 -3
  260. package/plugin/commands/work-on-task.md +22 -3
  261. package/plugin/pi-skills/close-issue/SKILL.md +90 -0
  262. package/plugin/pi-skills/create-pr/SKILL.md +99 -0
  263. package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
  264. package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
  265. package/plugin/pi-skills/respond-github/SKILL.md +98 -0
  266. package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
  267. package/plugin/pi-skills/review-pr/SKILL.md +261 -0
  268. package/plugin/pi-skills/start-leader/SKILL.md +121 -0
  269. package/plugin/pi-skills/start-worker/SKILL.md +60 -0
  270. package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
  271. package/plugin/pi-skills/todos/SKILL.md +66 -0
  272. package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
  273. package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
  274. package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
  275. package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
  276. package/plugin/skills/artifacts/examples/static-report.sh +17 -0
  277. package/plugin/skills/artifacts/skill.md +71 -0
  278. package/prek.toml +75 -0
  279. package/scripts/check-db-boundary.sh +60 -0
  280. package/scripts/e2e-docker-provider.ts +820 -0
  281. package/scripts/e2e-io-schemas-test.ts +807 -0
  282. package/scripts/e2e-provider-test.ts +220 -0
  283. package/scripts/e2e-workflow-redesign.sh +229 -0
  284. package/scripts/e2e-workflow-test.sh +285 -0
  285. package/scripts/e2e-workflow-test.ts +857 -0
  286. package/scripts/generate-openapi.ts +26 -0
  287. package/scripts/measure-tool-tokens.ts +118 -0
  288. package/scripts/x402-e2e-test.ts +195 -0
  289. package/scripts/x402-test-server.ts +236 -0
  290. package/scripts/x402-testnet-e2e.ts +668 -0
  291. package/slack-manifest.json +17 -0
  292. package/src/agentmail/app.ts +65 -0
  293. package/src/agentmail/handlers.ts +207 -0
  294. package/src/agentmail/index.ts +9 -0
  295. package/src/agentmail/types.ts +51 -0
  296. package/src/artifact-sdk/browser-sdk.ts +30 -0
  297. package/src/artifact-sdk/index.ts +2 -0
  298. package/src/artifact-sdk/localtunnel.d.ts +20 -0
  299. package/src/artifact-sdk/port.ts +12 -0
  300. package/src/artifact-sdk/server.ts +156 -0
  301. package/src/artifact-sdk/tunnel.ts +19 -0
  302. package/src/be/chunking.ts +193 -0
  303. package/src/be/db-queries/oauth.ts +90 -0
  304. package/src/be/db-queries/tracker.ts +182 -0
  305. package/src/be/db.ts +2729 -770
  306. package/src/be/embedding.ts +80 -0
  307. package/src/be/migrations/001_initial.sql +409 -0
  308. package/src/be/migrations/002_one_time_schedules.sql +59 -0
  309. package/src/be/migrations/003_workflows.sql +51 -0
  310. package/src/be/migrations/004_workflow_source.sql +81 -0
  311. package/src/be/migrations/005_epic_next_steps.sql +2 -0
  312. package/src/be/migrations/006_vcs_provider.sql +94 -0
  313. package/src/be/migrations/007_task_dir.sql +2 -0
  314. package/src/be/migrations/008_workflow_redesign.sql +85 -0
  315. package/src/be/migrations/009_tracker_integration.sql +144 -0
  316. package/src/be/migrations/010_step_diagnostics.sql +1 -0
  317. package/src/be/migrations/011_step_next_port.sql +1 -0
  318. package/src/be/migrations/012_trigger_schema.sql +1 -0
  319. package/src/be/migrations/013_task_output_schema.sql +2 -0
  320. package/src/be/migrations/runner.ts +188 -0
  321. package/src/cli.tsx +231 -299
  322. package/src/commands/artifact.ts +241 -0
  323. package/src/commands/onboard/compose-generator.ts +169 -0
  324. package/src/commands/onboard/env-generator.ts +79 -0
  325. package/src/commands/onboard/manifest.ts +37 -0
  326. package/src/commands/onboard/presets.ts +85 -0
  327. package/src/commands/onboard/service-names.ts +47 -0
  328. package/src/commands/onboard/steps/core-credentials.tsx +111 -0
  329. package/src/commands/onboard/steps/custom-templates.tsx +168 -0
  330. package/src/commands/onboard/steps/generate.tsx +154 -0
  331. package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
  332. package/src/commands/onboard/steps/harness.tsx +21 -0
  333. package/src/commands/onboard/steps/health-check.tsx +171 -0
  334. package/src/commands/onboard/steps/integration-github.tsx +105 -0
  335. package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
  336. package/src/commands/onboard/steps/integration-menu.tsx +58 -0
  337. package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
  338. package/src/commands/onboard/steps/integration-slack.tsx +165 -0
  339. package/src/commands/onboard/steps/post-connect.tsx +145 -0
  340. package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
  341. package/src/commands/onboard/steps/post-task.tsx +103 -0
  342. package/src/commands/onboard/steps/prereq-check.tsx +178 -0
  343. package/src/commands/onboard/steps/review.tsx +82 -0
  344. package/src/commands/onboard/steps/start.tsx +97 -0
  345. package/src/commands/onboard/templates.ts +34 -0
  346. package/src/commands/onboard/types.ts +259 -0
  347. package/src/commands/onboard.tsx +425 -0
  348. package/src/commands/runner.ts +1376 -559
  349. package/src/commands/setup.tsx +23 -38
  350. package/src/commands/shared/client-config.ts +41 -0
  351. package/src/github/app.ts +8 -0
  352. package/src/github/handlers.ts +93 -75
  353. package/src/github/index.ts +1 -0
  354. package/src/github/types.ts +1 -0
  355. package/src/gitlab/auth.ts +63 -0
  356. package/src/gitlab/handlers.ts +327 -0
  357. package/src/gitlab/index.ts +19 -0
  358. package/src/gitlab/reactions.ts +104 -0
  359. package/src/gitlab/types.ts +130 -0
  360. package/src/heartbeat/heartbeat.ts +425 -0
  361. package/src/heartbeat/index.ts +1 -0
  362. package/src/hooks/hook.ts +555 -4
  363. package/src/hooks/tool-loop-detection.test.ts +158 -0
  364. package/src/hooks/tool-loop-detection.ts +167 -0
  365. package/src/http/active-sessions.ts +172 -0
  366. package/src/http/agents.ts +328 -0
  367. package/src/http/config.ts +191 -0
  368. package/src/http/core.ts +309 -0
  369. package/src/http/db-query.ts +91 -0
  370. package/src/http/ecosystem.ts +63 -0
  371. package/src/http/epics.ts +460 -0
  372. package/src/http/index.ts +213 -0
  373. package/src/http/mcp.ts +77 -0
  374. package/src/http/memory.ts +168 -0
  375. package/src/http/openapi.ts +109 -0
  376. package/src/http/poll.ts +178 -0
  377. package/src/http/repos.ts +195 -0
  378. package/src/http/route-def.ts +123 -0
  379. package/src/http/schedules.ts +391 -0
  380. package/src/http/session-data.ts +241 -0
  381. package/src/http/stats.ts +174 -0
  382. package/src/http/tasks.ts +468 -0
  383. package/src/http/trackers/index.ts +10 -0
  384. package/src/http/trackers/linear.ts +187 -0
  385. package/src/http/types.ts +12 -0
  386. package/src/http/utils.ts +87 -0
  387. package/src/http/webhooks.ts +432 -0
  388. package/src/http/workflows.ts +522 -0
  389. package/src/http.ts +1 -1890
  390. package/src/linear/README.md +65 -0
  391. package/src/linear/app.ts +48 -0
  392. package/src/linear/client.ts +18 -0
  393. package/src/linear/index.ts +1 -0
  394. package/src/linear/oauth.ts +35 -0
  395. package/src/linear/outbound.ts +212 -0
  396. package/src/linear/sync.ts +543 -0
  397. package/src/linear/types.ts +7 -0
  398. package/src/linear/webhook.ts +104 -0
  399. package/src/oauth/README.md +66 -0
  400. package/src/oauth/index.ts +6 -0
  401. package/src/oauth/wrapper.ts +204 -0
  402. package/src/prompts/base-prompt.ts +366 -24
  403. package/src/prompts/defaults.ts +196 -0
  404. package/src/providers/claude-adapter.ts +429 -0
  405. package/src/providers/index.ts +24 -0
  406. package/src/providers/pi-mono-adapter.ts +442 -0
  407. package/src/providers/pi-mono-extension.ts +620 -0
  408. package/src/providers/pi-mono-mcp-client.ts +124 -0
  409. package/src/providers/types.ts +75 -0
  410. package/src/scheduler/scheduler.test.ts +2 -0
  411. package/src/scheduler/scheduler.ts +186 -29
  412. package/src/server.ts +82 -6
  413. package/src/slack/HEURISTICS.md +105 -0
  414. package/src/slack/actions.ts +133 -0
  415. package/src/slack/app.ts +7 -0
  416. package/src/slack/assistant.ts +115 -0
  417. package/src/slack/blocks.ts +233 -0
  418. package/src/slack/commands.ts +31 -17
  419. package/src/slack/files.ts +1 -1
  420. package/src/slack/handlers.test.ts +114 -1
  421. package/src/slack/handlers.ts +212 -52
  422. package/src/slack/responses.ts +120 -67
  423. package/src/slack/router.ts +17 -99
  424. package/src/slack/thread-buffer.ts +213 -0
  425. package/src/slack/watcher.ts +119 -4
  426. package/src/tests/agent-activity.test.ts +247 -0
  427. package/src/tests/agentmail-filters.test.ts +97 -0
  428. package/src/tests/artifact-sdk.test.ts +800 -0
  429. package/src/tests/base-prompt.test.ts +264 -0
  430. package/src/tests/build-pi-skills.test.ts +127 -0
  431. package/src/tests/claude-adapter.test.ts +126 -0
  432. package/src/tests/context-versioning.test.ts +425 -0
  433. package/src/tests/db-queries-oauth.test.ts +197 -0
  434. package/src/tests/db-queries-tracker.test.ts +230 -0
  435. package/src/tests/epics.test.ts +3 -3
  436. package/src/tests/error-tracker.test.ts +368 -0
  437. package/src/tests/fetch-resolved-env.test.ts +167 -0
  438. package/src/tests/generate-default-claude-md.test.ts +9 -1
  439. package/src/tests/generate-identity-templates.test.ts +124 -0
  440. package/src/tests/gitlab-auth.test.ts +109 -0
  441. package/src/tests/gitlab-handlers.test.ts +691 -0
  442. package/src/tests/gitlab-vcs-db.test.ts +177 -0
  443. package/src/tests/heartbeat.test.ts +363 -0
  444. package/src/tests/http-api-integration.test.ts +1698 -0
  445. package/src/tests/linear-outbound-sync.test.ts +200 -0
  446. package/src/tests/linear-webhook.test.ts +402 -0
  447. package/src/tests/match-route.test.ts +187 -0
  448. package/src/tests/memory.test.ts +737 -0
  449. package/src/tests/migration-runner-regressions.test.ts +86 -0
  450. package/src/tests/model-control.test.ts +338 -0
  451. package/src/tests/oauth-wrapper.test.ts +147 -0
  452. package/src/tests/onboard-compose.test.ts +138 -0
  453. package/src/tests/onboard-env.test.ts +174 -0
  454. package/src/tests/onboard-manifest.test.ts +137 -0
  455. package/src/tests/pi-mono-adapter.test.ts +234 -0
  456. package/src/tests/progress-dedup.test.ts +98 -0
  457. package/src/tests/provider-adapter.test.ts +122 -0
  458. package/src/tests/provider-command-format.test.ts +98 -0
  459. package/src/tests/reload-config.test.ts +170 -0
  460. package/src/tests/runner-polling-api.test.ts +25 -20
  461. package/src/tests/scheduled-tasks.test.ts +104 -0
  462. package/src/tests/scheduler-backoff.test.ts +166 -0
  463. package/src/tests/self-improvement.test.ts +540 -0
  464. package/src/tests/session-attach.test.ts +536 -0
  465. package/src/tests/session-costs.test.ts +267 -1
  466. package/src/tests/slack-actions.test.ts +133 -0
  467. package/src/tests/slack-assistant.test.ts +136 -0
  468. package/src/tests/slack-blocks.test.ts +246 -0
  469. package/src/tests/slack-metadata-inheritance.test.ts +243 -0
  470. package/src/tests/slack-queue-offline.test.ts +174 -0
  471. package/src/tests/slack-router.test.ts +181 -0
  472. package/src/tests/slack-thread-buffer.test.ts +305 -0
  473. package/src/tests/slack-thread-followups.test.ts +298 -0
  474. package/src/tests/slack-watcher.test.ts +101 -0
  475. package/src/tests/structured-output.test.ts +311 -0
  476. package/src/tests/swarm-repos.test.ts +198 -0
  477. package/src/tests/task-cancellation.test.ts +6 -4
  478. package/src/tests/task-working-dir.test.ts +176 -0
  479. package/src/tests/template-fetch.test.ts +490 -0
  480. package/src/tests/tool-annotations.test.ts +371 -0
  481. package/src/tests/tracker-tools.test.ts +184 -0
  482. package/src/tests/update-profile-api.test.ts +143 -3
  483. package/src/tests/validation-adapters.test.ts +86 -0
  484. package/src/tests/vcs-provider.test.ts +27 -0
  485. package/src/tests/workflow-agent-task.test.ts +196 -0
  486. package/src/tests/workflow-async-v2.test.ts +386 -0
  487. package/src/tests/workflow-convergence.test.ts +541 -0
  488. package/src/tests/workflow-definition-validation.test.ts +290 -0
  489. package/src/tests/workflow-engine-v2.test.ts +691 -0
  490. package/src/tests/workflow-executors.test.ts +736 -0
  491. package/src/tests/workflow-http-v2.test.ts +599 -0
  492. package/src/tests/workflow-integration-io.test.ts +902 -0
  493. package/src/tests/workflow-io-schemas.test.ts +624 -0
  494. package/src/tests/workflow-registry.test.ts +592 -0
  495. package/src/tests/workflow-retry-v2.test.ts +401 -0
  496. package/src/tests/workflow-retry-validation.test.ts +282 -0
  497. package/src/tests/workflow-template.test.ts +288 -0
  498. package/src/tests/workflow-trigger-schema.test.ts +359 -0
  499. package/src/tests/workflow-triggers-v2.test.ts +264 -0
  500. package/src/tests/workflow-versions.test.ts +208 -0
  501. package/src/tests/x402-client.test.ts +117 -0
  502. package/src/tests/x402-config.test.ts +182 -0
  503. package/src/tests/x402-spending-tracker.test.ts +185 -0
  504. package/src/tools/cancel-task.ts +2 -0
  505. package/src/tools/context-diff.ts +171 -0
  506. package/src/tools/context-history.ts +138 -0
  507. package/src/tools/create-channel.ts +1 -0
  508. package/src/tools/db-query.ts +78 -0
  509. package/src/tools/delete-channel.ts +132 -0
  510. package/src/tools/epics/assign-task-to-epic.ts +1 -0
  511. package/src/tools/epics/create-epic.ts +3 -2
  512. package/src/tools/epics/delete-epic.ts +2 -0
  513. package/src/tools/epics/get-epic-details.ts +2 -0
  514. package/src/tools/epics/list-epics.ts +2 -0
  515. package/src/tools/epics/unassign-task-from-epic.ts +1 -0
  516. package/src/tools/epics/update-epic.ts +7 -4
  517. package/src/tools/get-swarm.ts +2 -0
  518. package/src/tools/get-task-details.ts +2 -0
  519. package/src/tools/get-tasks.ts +27 -1
  520. package/src/tools/inject-learning.ts +106 -0
  521. package/src/tools/join-swarm.ts +17 -7
  522. package/src/tools/list-channels.ts +2 -0
  523. package/src/tools/list-services.ts +2 -0
  524. package/src/tools/memory-get.ts +56 -0
  525. package/src/tools/memory-search.ts +131 -0
  526. package/src/tools/my-agent-info.ts +2 -0
  527. package/src/tools/poll-task.ts +2 -20
  528. package/src/tools/post-message.ts +1 -0
  529. package/src/tools/read-messages.ts +2 -0
  530. package/src/tools/register-agentmail-inbox.ts +166 -0
  531. package/src/tools/register-service.ts +2 -0
  532. package/src/tools/schedules/create-schedule.ts +134 -24
  533. package/src/tools/schedules/delete-schedule.ts +2 -0
  534. package/src/tools/schedules/list-schedules.ts +20 -4
  535. package/src/tools/schedules/run-schedule-now.ts +1 -0
  536. package/src/tools/schedules/update-schedule.ts +49 -17
  537. package/src/tools/send-task.ts +132 -10
  538. package/src/tools/slack-download-file.ts +4 -2
  539. package/src/tools/slack-list-channels.ts +2 -0
  540. package/src/tools/slack-post.ts +2 -0
  541. package/src/tools/slack-read.ts +2 -0
  542. package/src/tools/slack-reply.ts +2 -0
  543. package/src/tools/slack-upload-file.ts +2 -0
  544. package/src/tools/store-progress.ts +187 -4
  545. package/src/tools/swarm-config/delete-config.ts +87 -0
  546. package/src/tools/swarm-config/get-config.ts +108 -0
  547. package/src/tools/swarm-config/index.ts +4 -0
  548. package/src/tools/swarm-config/list-config.ts +99 -0
  549. package/src/tools/swarm-config/set-config.ts +118 -0
  550. package/src/tools/task-action.ts +30 -5
  551. package/src/tools/task-dedup.ts +97 -0
  552. package/src/tools/tool-config.ts +117 -0
  553. package/src/tools/tracker/index.ts +6 -0
  554. package/src/tools/tracker/tracker-link-epic.ts +64 -0
  555. package/src/tools/tracker/tracker-link-task.ts +64 -0
  556. package/src/tools/tracker/tracker-map-agent.ts +57 -0
  557. package/src/tools/tracker/tracker-status.ts +56 -0
  558. package/src/tools/tracker/tracker-sync-status.ts +42 -0
  559. package/src/tools/tracker/tracker-unlink.ts +41 -0
  560. package/src/tools/unregister-service.ts +2 -0
  561. package/src/tools/update-profile.ts +93 -10
  562. package/src/tools/update-service-status.ts +2 -0
  563. package/src/tools/utils.ts +10 -1
  564. package/src/tools/workflows/create-workflow.ts +111 -0
  565. package/src/tools/workflows/delete-workflow.ts +42 -0
  566. package/src/tools/workflows/get-workflow-run.ts +59 -0
  567. package/src/tools/workflows/get-workflow.ts +53 -0
  568. package/src/tools/workflows/index.ts +9 -0
  569. package/src/tools/workflows/list-workflow-runs.ts +48 -0
  570. package/src/tools/workflows/list-workflows.ts +42 -0
  571. package/src/tools/workflows/retry-workflow-run.ts +40 -0
  572. package/src/tools/workflows/trigger-workflow.ts +96 -0
  573. package/src/tools/workflows/update-workflow.ts +118 -0
  574. package/src/tracker/types.ts +51 -0
  575. package/src/types.ts +477 -14
  576. package/src/utils/credentials.test.ts +156 -0
  577. package/src/utils/credentials.ts +50 -0
  578. package/src/utils/error-tracker.ts +190 -0
  579. package/src/vcs/index.ts +15 -0
  580. package/src/vcs/types.ts +5 -0
  581. package/src/workflows/checkpoint.ts +121 -0
  582. package/src/workflows/cooldown.ts +28 -0
  583. package/src/workflows/definition.ts +217 -0
  584. package/src/workflows/engine.ts +554 -0
  585. package/src/workflows/event-bus.ts +29 -0
  586. package/src/workflows/executors/agent-task.ts +92 -0
  587. package/src/workflows/executors/base.ts +86 -0
  588. package/src/workflows/executors/code-match.ts +88 -0
  589. package/src/workflows/executors/index.ts +16 -0
  590. package/src/workflows/executors/notify.ts +93 -0
  591. package/src/workflows/executors/property-match.ts +104 -0
  592. package/src/workflows/executors/raw-llm.ts +83 -0
  593. package/src/workflows/executors/registry.ts +76 -0
  594. package/src/workflows/executors/script.ts +103 -0
  595. package/src/workflows/executors/validate.ts +215 -0
  596. package/src/workflows/executors/vcs.ts +58 -0
  597. package/src/workflows/index.ts +61 -0
  598. package/src/workflows/input.ts +46 -0
  599. package/src/workflows/json-schema-validator.ts +118 -0
  600. package/src/workflows/recovery.ts +139 -0
  601. package/src/workflows/resume.ts +143 -0
  602. package/src/workflows/retry-poller.ts +216 -0
  603. package/src/workflows/template.ts +74 -0
  604. package/src/workflows/templates.ts +86 -0
  605. package/src/workflows/triggers.ts +95 -0
  606. package/src/workflows/validation.ts +104 -0
  607. package/src/workflows/version.ts +42 -0
  608. package/src/x402/cli.ts +140 -0
  609. package/src/x402/client.ts +192 -0
  610. package/src/x402/config.ts +131 -0
  611. package/src/x402/index.ts +37 -0
  612. package/src/x402/openfort-signer.ts +83 -0
  613. package/src/x402/spending-tracker.ts +109 -0
  614. package/templates/community/.gitkeep +0 -0
  615. package/templates/official/coder/CLAUDE.md +49 -0
  616. package/templates/official/coder/IDENTITY.md +28 -0
  617. package/templates/official/coder/SOUL.md +43 -0
  618. package/templates/official/coder/TOOLS.md +40 -0
  619. package/templates/official/coder/config.json +23 -0
  620. package/templates/official/coder/start-up.sh +23 -0
  621. package/templates/official/content-reviewer/CLAUDE.md +68 -0
  622. package/templates/official/content-reviewer/IDENTITY.md +28 -0
  623. package/templates/official/content-reviewer/SOUL.md +44 -0
  624. package/templates/official/content-reviewer/TOOLS.md +37 -0
  625. package/templates/official/content-reviewer/config.json +23 -0
  626. package/templates/official/content-reviewer/start-up.sh +23 -0
  627. package/templates/official/content-strategist/CLAUDE.md +63 -0
  628. package/templates/official/content-strategist/IDENTITY.md +33 -0
  629. package/templates/official/content-strategist/SOUL.md +48 -0
  630. package/templates/official/content-strategist/TOOLS.md +47 -0
  631. package/templates/official/content-strategist/config.json +23 -0
  632. package/templates/official/content-strategist/start-up.sh +23 -0
  633. package/templates/official/content-writer/CLAUDE.md +72 -0
  634. package/templates/official/content-writer/IDENTITY.md +30 -0
  635. package/templates/official/content-writer/SOUL.md +46 -0
  636. package/templates/official/content-writer/TOOLS.md +44 -0
  637. package/templates/official/content-writer/config.json +23 -0
  638. package/templates/official/content-writer/start-up.sh +23 -0
  639. package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
  640. package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
  641. package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
  642. package/templates/official/forward-deployed-engineer/config.json +21 -0
  643. package/templates/official/lead/CLAUDE.md +33 -0
  644. package/templates/official/lead/IDENTITY.md +36 -0
  645. package/templates/official/lead/SOUL.md +51 -0
  646. package/templates/official/lead/config.json +22 -0
  647. package/templates/official/researcher/CLAUDE.md +46 -0
  648. package/templates/official/researcher/IDENTITY.md +28 -0
  649. package/templates/official/researcher/SOUL.md +43 -0
  650. package/templates/official/researcher/config.json +21 -0
  651. package/templates/official/reviewer/CLAUDE.md +63 -0
  652. package/templates/official/reviewer/IDENTITY.md +28 -0
  653. package/templates/official/reviewer/SOUL.md +45 -0
  654. package/templates/official/reviewer/config.json +21 -0
  655. package/templates/official/tester/CLAUDE.md +53 -0
  656. package/templates/official/tester/IDENTITY.md +28 -0
  657. package/templates/official/tester/SOUL.md +55 -0
  658. package/templates/official/tester/config.json +21 -0
  659. package/templates/schema.ts +35 -0
  660. package/templates-ui/README.md +46 -0
  661. package/templates-ui/components.json +17 -0
  662. package/templates-ui/eslint.config.mjs +18 -0
  663. package/templates-ui/next.config.ts +7 -0
  664. package/templates-ui/package.json +35 -0
  665. package/templates-ui/pnpm-lock.yaml +4571 -0
  666. package/templates-ui/postcss.config.mjs +7 -0
  667. package/templates-ui/public/file.svg +1 -0
  668. package/templates-ui/public/globe.svg +1 -0
  669. package/templates-ui/public/logo.png +0 -0
  670. package/templates-ui/public/next.svg +1 -0
  671. package/templates-ui/public/vercel.svg +1 -0
  672. package/templates-ui/public/window.svg +1 -0
  673. package/templates-ui/src/app/[category]/[name]/page.tsx +89 -0
  674. package/templates-ui/src/app/api/templates/[...slug]/route.ts +52 -0
  675. package/templates-ui/src/app/api/templates/route.ts +18 -0
  676. package/templates-ui/src/app/builder/page.tsx +37 -0
  677. package/templates-ui/src/app/globals.css +94 -0
  678. package/templates-ui/src/app/layout.tsx +79 -0
  679. package/templates-ui/src/app/page.tsx +38 -0
  680. package/templates-ui/src/app/robots.ts +11 -0
  681. package/templates-ui/src/app/sitemap.ts +31 -0
  682. package/templates-ui/src/components/compose-builder.tsx +442 -0
  683. package/templates-ui/src/components/compose-preview.tsx +117 -0
  684. package/templates-ui/src/components/file-preview.tsx +77 -0
  685. package/templates-ui/src/components/footer.tsx +40 -0
  686. package/templates-ui/src/components/header.tsx +41 -0
  687. package/templates-ui/src/components/template-card.tsx +87 -0
  688. package/templates-ui/src/components/template-detail.tsx +125 -0
  689. package/templates-ui/src/components/template-gallery.tsx +263 -0
  690. package/templates-ui/src/components/ui/badge.tsx +36 -0
  691. package/templates-ui/src/components/ui/button.tsx +57 -0
  692. package/templates-ui/src/components/ui/card.tsx +76 -0
  693. package/templates-ui/src/components/ui/separator.tsx +31 -0
  694. package/templates-ui/src/components/ui/tooltip.tsx +32 -0
  695. package/templates-ui/src/lib/compose-generator.ts +241 -0
  696. package/templates-ui/src/lib/templates.ts +137 -0
  697. package/templates-ui/src/lib/utils.ts +6 -0
  698. package/templates-ui/tsconfig.json +34 -0
  699. package/thoughts/research/2026-02-28-openfort-viem-x402-research.md +679 -0
  700. package/thoughts/research/2026-02-28-x402-payments-research.md +686 -0
  701. package/thoughts/researcher/plans/2026-02-20-agent-self-improvement-plan.md +282 -0
  702. package/thoughts/researcher/research/2026-02-20-agent-self-improvement.md +492 -0
  703. package/thoughts/shared/plans/2026-02-26-mcp-tool-context-reduction.md +282 -0
  704. package/thoughts/shared/plans/2026-03-02-claude-context-mode-integration.md +328 -0
  705. package/thoughts/shared/plans/2026-03-02-code-level-heartbeat.md +224 -0
  706. package/thoughts/shared/research/2026-02-26-cliffy-mcp-tools.md +159 -0
  707. package/thoughts/shared/research/2026-03-03-database-migration-system-refactor.md +337 -0
  708. package/thoughts/swarm-researcher/plans/2026-02-23-openclaw-improvements-plan.md +778 -0
  709. package/thoughts/swarm-researcher/plans/2026-02-26-artifacts-localtunnel-plan.md +1269 -0
  710. package/thoughts/swarm-researcher/research/2026-02-23-openclaw-vs-agent-swarm-comparison.md +411 -0
  711. package/thoughts/swarm-researcher/research/2026-02-26-artifacts-localtunnel.md +724 -0
  712. package/thoughts/taras/brainstorms/2026-03-20-prompt-template-registry.md +443 -0
  713. package/thoughts/taras/brainstorms/2026-03-20-setup-cli-onboarding.md +307 -0
  714. package/thoughts/taras/plans/2026-02-20-auto-improvement.md +803 -0
  715. package/thoughts/taras/plans/2026-02-20-env-management.md +538 -0
  716. package/thoughts/taras/plans/2026-02-20-memory-system.md +882 -0
  717. package/thoughts/taras/plans/2026-02-20-repos-knowledge.md +806 -0
  718. package/thoughts/taras/plans/2026-02-20-session-attach.md +647 -0
  719. package/thoughts/taras/plans/2026-02-20-worker-identity.md +820 -0
  720. package/thoughts/taras/plans/2026-02-25-feat-new-ui-visual-redesign-plan.md +768 -0
  721. package/thoughts/taras/plans/2026-03-04-fix-buildSystemPrompt-missing-fields.md +77 -0
  722. package/thoughts/taras/plans/2026-03-04-new-ui-missing-actions.md +543 -0
  723. package/thoughts/taras/plans/2026-03-06-one-time-scheduled-tasks.md +373 -0
  724. package/thoughts/taras/plans/2026-03-08-memory-self-improvement-enhancements.md +512 -0
  725. package/thoughts/taras/plans/2026-03-08-pi-mono-provider-implementation.md +919 -0
  726. package/thoughts/taras/plans/2026-03-09-templates-registry.md +723 -0
  727. package/thoughts/taras/plans/2026-03-10-task-working-directory.md +371 -0
  728. package/thoughts/taras/plans/2026-03-11-archil-per-agent-write-strategy.md +621 -0
  729. package/thoughts/taras/plans/2026-03-12-eliminate-inbox-route-to-tasks.md +61 -0
  730. package/thoughts/taras/plans/2026-03-12-slack-thread-followup-additive.md +488 -0
  731. package/thoughts/taras/plans/2026-03-13-slack-ai-improvements.md +644 -0
  732. package/thoughts/taras/plans/2026-03-16-route-wrapper-openapi.md +636 -0
  733. package/thoughts/taras/plans/2026-03-17-multi-api-config.md +444 -0
  734. package/thoughts/taras/plans/2026-03-18-agent-fs-integration.md +591 -0
  735. package/thoughts/taras/plans/2026-03-18-debug-db-explorer.md +446 -0
  736. package/thoughts/taras/plans/2026-03-18-workflow-redesign.md +987 -0
  737. package/thoughts/taras/plans/2026-03-19-compound-learnings.md +403 -0
  738. package/thoughts/taras/plans/2026-03-19-ticket-tracker-linear-integration.md +860 -0
  739. package/thoughts/taras/plans/2026-03-19-workflow-io-schemas-and-bugs.md +899 -0
  740. package/thoughts/taras/plans/2026-03-20-setup-cli-onboarding.md +874 -0
  741. package/thoughts/taras/plans/2026-03-20-workflow-structured-output-validation-workspace.md +723 -0
  742. package/thoughts/taras/research/2026-02-19-agent-native-swarm-architecture.md +390 -0
  743. package/thoughts/taras/research/2026-02-19-swarm-gaps-implementation.md +594 -0
  744. package/thoughts/taras/research/2026-02-25-dashboard-ui-design-best-practices.md +825 -0
  745. package/thoughts/taras/research/2026-02-26-task-detail-page-redesign.md +393 -0
  746. package/thoughts/taras/research/2026-03-03-new-ui-missing-actions.md +168 -0
  747. package/thoughts/taras/research/2026-03-05-pi-mono-provider-research.md +230 -0
  748. package/thoughts/taras/research/2026-03-06-workflow-engine-design.md +445 -0
  749. package/thoughts/taras/research/2026-03-08-drive-loop-concept.md +375 -0
  750. package/thoughts/taras/research/2026-03-08-pi-mono-deep-dive.md +869 -0
  751. package/thoughts/taras/research/2026-03-09-templates-registry.md +373 -0
  752. package/thoughts/taras/research/2026-03-10-agent-working-directory.md +223 -0
  753. package/thoughts/taras/research/2026-03-10-configurable-event-prompts.md +339 -0
  754. package/thoughts/taras/research/2026-03-11-archil-production-setup.md +181 -0
  755. package/thoughts/taras/research/2026-03-11-archil-shared-disk-write-strategies.md +437 -0
  756. package/thoughts/taras/research/2026-03-13-slack-ai-features.md +258 -0
  757. package/thoughts/taras/research/2026-03-16-openapi-docs-generation.md +335 -0
  758. package/thoughts/taras/research/2026-03-16-route-wrapper-openapi.md +670 -0
  759. package/thoughts/taras/research/2026-03-16-slack-thread-followups-e2e.md +54 -0
  760. package/thoughts/taras/research/2026-03-18-agent-fs-integration.md +558 -0
  761. package/thoughts/taras/research/2026-03-18-linear-integration-finalization.md +526 -0
  762. package/thoughts/taras/research/2026-03-18-workflow-redesign.md +797 -0
  763. package/thoughts/taras/research/2026-03-19-workflow-node-io-schemas-and-bugs.md +563 -0
  764. package/thoughts/taras/research/2026-03-19-workflow-structured-output-validation-workspace.md +486 -0
  765. package/thoughts/taras/research/2026-03-20-prompt-template-registry.md +469 -0
  766. package/tsconfig.json +1 -1
  767. package/FAQ.md +0 -19
  768. package/src/tests/get-inbox-message.test.ts +0 -145
  769. package/src/tools/get-inbox-message.ts +0 -89
  770. package/src/tools/inbox-delegate.ts +0 -113
  771. package/ui/CLAUDE.md +0 -49
  772. package/ui/bun.lock +0 -771
  773. package/ui/index.html +0 -22
  774. package/ui/package-lock.json +0 -5290
  775. package/ui/package.json +0 -33
  776. package/ui/pnpm-lock.yaml +0 -3341
  777. package/ui/postcss.config.js +0 -6
  778. package/ui/src/App.tsx +0 -63
  779. package/ui/src/components/ActivityFeed.tsx +0 -440
  780. package/ui/src/components/AgentDetailPanel.tsx +0 -733
  781. package/ui/src/components/AgentsPanel.tsx +0 -815
  782. package/ui/src/components/ChatPanel.tsx +0 -1920
  783. package/ui/src/components/ConfigModal.tsx +0 -253
  784. package/ui/src/components/Dashboard.tsx +0 -832
  785. package/ui/src/components/EditAgentProfileModal.tsx +0 -433
  786. package/ui/src/components/EpicDetailPage.tsx +0 -741
  787. package/ui/src/components/EpicsPanel.tsx +0 -566
  788. package/ui/src/components/Header.tsx +0 -160
  789. package/ui/src/components/JsonViewer.tsx +0 -171
  790. package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
  791. package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
  792. package/ui/src/components/ServicesPanel.tsx +0 -622
  793. package/ui/src/components/SessionLogPanel.tsx +0 -1219
  794. package/ui/src/components/StatsBar.tsx +0 -321
  795. package/ui/src/components/StatusBadge.tsx +0 -168
  796. package/ui/src/components/TaskDetailPanel.tsx +0 -903
  797. package/ui/src/components/TasksPanel.tsx +0 -614
  798. package/ui/src/components/UsageCharts.tsx +0 -216
  799. package/ui/src/components/UsageTab.tsx +0 -394
  800. package/ui/src/hooks/queries.ts +0 -353
  801. package/ui/src/index.css +0 -257
  802. package/ui/src/lib/api.ts +0 -268
  803. package/ui/src/lib/config.ts +0 -35
  804. package/ui/src/lib/contentPreview.ts +0 -208
  805. package/ui/src/lib/theme.ts +0 -214
  806. package/ui/src/main.tsx +0 -28
  807. package/ui/src/vite-env.d.ts +0 -1
  808. package/ui/tailwind.config.js +0 -37
  809. package/ui/vite.config.ts +0 -35
  810. /package/{ui → docs-site}/public/logo.png +0 -0
package/src/be/db.ts CHANGED
@@ -1,27 +1,49 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import type {
3
+ ActiveSession,
3
4
  Agent,
4
5
  AgentLog,
5
6
  AgentLogEventType,
7
+ AgentMemory,
8
+ AgentMemoryScope,
9
+ AgentMemorySource,
6
10
  AgentStatus,
7
11
  AgentTask,
8
12
  AgentTaskSource,
9
13
  AgentTaskStatus,
10
14
  AgentWithTasks,
15
+ ChangeSource,
11
16
  Channel,
12
17
  ChannelMessage,
13
18
  ChannelType,
19
+ ContextVersion,
20
+ CooldownConfig,
14
21
  Epic,
15
22
  EpicStatus,
16
23
  EpicWithProgress,
17
24
  InboxMessage,
18
25
  InboxMessageStatus,
26
+ InputValue,
19
27
  ScheduledTask,
20
28
  Service,
21
29
  ServiceStatus,
22
30
  SessionCost,
23
31
  SessionLog,
32
+ SwarmConfig,
33
+ SwarmRepo,
34
+ TriggerConfig,
35
+ VersionableField,
36
+ VersionMeta,
37
+ Workflow,
38
+ WorkflowDefinition,
39
+ WorkflowRun,
40
+ WorkflowRunStatus,
41
+ WorkflowRunStep,
42
+ WorkflowRunStepStatus,
43
+ WorkflowSnapshot,
44
+ WorkflowVersion,
24
45
  } from "../types";
46
+ import { runMigrations } from "./migrations/runner";
25
47
 
26
48
  let db: Database | null = null;
27
49
 
@@ -39,607 +61,131 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
39
61
  database.run("PRAGMA journal_mode = WAL;");
40
62
  database.run("PRAGMA foreign_keys = ON;");
41
63
 
42
- // Schema initialization - wrapped in transaction for atomicity
43
- // Individual statements ensure compatibility with older Bun versions (< 1.0.26)
44
- // that don't support multi-statement queries
45
- const initSchema = database.transaction(() => {
46
- // Tables
47
- database.run(`
48
- CREATE TABLE IF NOT EXISTS agents (
49
- id TEXT PRIMARY KEY,
50
- name TEXT NOT NULL,
51
- isLead INTEGER NOT NULL DEFAULT 0,
52
- status TEXT NOT NULL CHECK(status IN ('idle', 'busy', 'offline')),
53
- description TEXT,
54
- role TEXT,
55
- capabilities TEXT DEFAULT '[]',
56
- createdAt TEXT NOT NULL,
57
- lastUpdatedAt TEXT NOT NULL
58
- )
59
- `);
60
-
61
- database.run(`
62
- CREATE TABLE IF NOT EXISTS agent_tasks (
63
- id TEXT PRIMARY KEY,
64
- agentId TEXT,
65
- creatorAgentId TEXT,
66
- task TEXT NOT NULL,
67
- status TEXT NOT NULL DEFAULT 'pending',
68
- source TEXT NOT NULL DEFAULT 'mcp',
69
- taskType TEXT,
70
- tags TEXT DEFAULT '[]',
71
- priority INTEGER DEFAULT 50,
72
- dependsOn TEXT DEFAULT '[]',
73
- offeredTo TEXT,
74
- offeredAt TEXT,
75
- acceptedAt TEXT,
76
- rejectionReason TEXT,
77
- slackChannelId TEXT,
78
- slackThreadTs TEXT,
79
- slackUserId TEXT,
80
- createdAt TEXT NOT NULL,
81
- lastUpdatedAt TEXT NOT NULL,
82
- finishedAt TEXT,
83
- failureReason TEXT,
84
- output TEXT,
85
- progress TEXT,
86
- notifiedAt TEXT
87
- )
88
- `);
89
-
90
- database.run(`
91
- CREATE TABLE IF NOT EXISTS agent_log (
92
- id TEXT PRIMARY KEY,
93
- eventType TEXT NOT NULL,
94
- agentId TEXT,
95
- taskId TEXT,
96
- oldValue TEXT,
97
- newValue TEXT,
98
- metadata TEXT,
99
- createdAt TEXT NOT NULL
100
- )
101
- `);
102
-
103
- database.run(`
104
- CREATE TABLE IF NOT EXISTS channels (
105
- id TEXT PRIMARY KEY,
106
- name TEXT NOT NULL UNIQUE,
107
- description TEXT,
108
- type TEXT NOT NULL DEFAULT 'public' CHECK(type IN ('public', 'dm')),
109
- createdBy TEXT,
110
- participants TEXT DEFAULT '[]',
111
- createdAt TEXT NOT NULL,
112
- FOREIGN KEY (createdBy) REFERENCES agents(id) ON DELETE SET NULL
113
- )
114
- `);
115
-
116
- database.run(`
117
- CREATE TABLE IF NOT EXISTS channel_messages (
118
- id TEXT PRIMARY KEY,
119
- channelId TEXT NOT NULL,
120
- agentId TEXT,
121
- content TEXT NOT NULL,
122
- replyToId TEXT,
123
- mentions TEXT DEFAULT '[]',
124
- createdAt TEXT NOT NULL,
125
- FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE,
126
- FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
127
- FOREIGN KEY (replyToId) REFERENCES channel_messages(id) ON DELETE SET NULL
128
- )
129
- `);
130
-
131
- database.run(`
132
- CREATE TABLE IF NOT EXISTS channel_read_state (
133
- agentId TEXT NOT NULL,
134
- channelId TEXT NOT NULL,
135
- lastReadAt TEXT NOT NULL,
136
- processing_since TEXT,
137
- PRIMARY KEY (agentId, channelId),
138
- FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
139
- FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE
140
- )
141
- `);
142
-
143
- database.run(`
144
- CREATE TABLE IF NOT EXISTS services (
145
- id TEXT PRIMARY KEY,
146
- agentId TEXT NOT NULL,
147
- name TEXT NOT NULL,
148
- port INTEGER NOT NULL DEFAULT 3000,
149
- description TEXT,
150
- url TEXT,
151
- healthCheckPath TEXT DEFAULT '/health',
152
- status TEXT NOT NULL DEFAULT 'starting' CHECK(status IN ('starting', 'healthy', 'unhealthy', 'stopped')),
153
- script TEXT NOT NULL DEFAULT '',
154
- cwd TEXT,
155
- interpreter TEXT,
156
- args TEXT,
157
- env TEXT,
158
- metadata TEXT DEFAULT '{}',
159
- createdAt TEXT NOT NULL,
160
- lastUpdatedAt TEXT NOT NULL,
161
- FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
162
- UNIQUE(agentId, name)
163
- )
164
- `);
165
-
166
- database.run(`
167
- CREATE TABLE IF NOT EXISTS session_logs (
168
- id TEXT PRIMARY KEY,
169
- taskId TEXT,
170
- sessionId TEXT NOT NULL,
171
- iteration INTEGER NOT NULL,
172
- cli TEXT NOT NULL DEFAULT 'claude',
173
- content TEXT NOT NULL,
174
- lineNumber INTEGER NOT NULL,
175
- createdAt TEXT NOT NULL
176
- )
177
- `);
178
-
179
- database.run(`
180
- CREATE TABLE IF NOT EXISTS session_costs (
181
- id TEXT PRIMARY KEY,
182
- sessionId TEXT NOT NULL,
183
- taskId TEXT,
184
- agentId TEXT NOT NULL,
185
- totalCostUsd REAL NOT NULL,
186
- inputTokens INTEGER NOT NULL DEFAULT 0,
187
- outputTokens INTEGER NOT NULL DEFAULT 0,
188
- cacheReadTokens INTEGER NOT NULL DEFAULT 0,
189
- cacheWriteTokens INTEGER NOT NULL DEFAULT 0,
190
- durationMs INTEGER NOT NULL,
191
- numTurns INTEGER NOT NULL,
192
- model TEXT NOT NULL,
193
- isError INTEGER NOT NULL DEFAULT 0,
194
- createdAt TEXT NOT NULL,
195
- FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
196
- FOREIGN KEY (taskId) REFERENCES agent_tasks(id) ON DELETE SET NULL
197
- )
198
- `);
199
-
200
- database.run(`
201
- CREATE TABLE IF NOT EXISTS inbox_messages (
202
- id TEXT PRIMARY KEY,
203
- agentId TEXT NOT NULL,
204
- content TEXT NOT NULL,
205
- source TEXT NOT NULL DEFAULT 'slack',
206
- status TEXT NOT NULL DEFAULT 'unread' CHECK(status IN ('unread', 'processing', 'read', 'responded', 'delegated')),
207
- slackChannelId TEXT,
208
- slackThreadTs TEXT,
209
- slackUserId TEXT,
210
- matchedText TEXT,
211
- delegatedToTaskId TEXT,
212
- responseText TEXT,
213
- createdAt TEXT NOT NULL,
214
- lastUpdatedAt TEXT NOT NULL,
215
- FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
216
- FOREIGN KEY (delegatedToTaskId) REFERENCES agent_tasks(id) ON DELETE SET NULL
217
- )
218
- `);
219
-
220
- // Indexes
221
- database.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId)`);
222
- database.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status)`);
223
- database.run(`CREATE INDEX IF NOT EXISTS idx_agent_log_agentId ON agent_log(agentId)`);
224
- database.run(`CREATE INDEX IF NOT EXISTS idx_agent_log_taskId ON agent_log(taskId)`);
225
- database.run(`CREATE INDEX IF NOT EXISTS idx_agent_log_eventType ON agent_log(eventType)`);
226
- database.run(`CREATE INDEX IF NOT EXISTS idx_agent_log_createdAt ON agent_log(createdAt)`);
227
- database.run(
228
- `CREATE INDEX IF NOT EXISTS idx_channel_messages_channelId ON channel_messages(channelId)`,
229
- );
230
- database.run(
231
- `CREATE INDEX IF NOT EXISTS idx_channel_messages_agentId ON channel_messages(agentId)`,
232
- );
233
- database.run(
234
- `CREATE INDEX IF NOT EXISTS idx_channel_messages_createdAt ON channel_messages(createdAt)`,
235
- );
236
- database.run(`CREATE INDEX IF NOT EXISTS idx_services_agentId ON services(agentId)`);
237
- database.run(`CREATE INDEX IF NOT EXISTS idx_services_status ON services(status)`);
238
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_logs_taskId ON session_logs(taskId)`);
239
- database.run(
240
- `CREATE INDEX IF NOT EXISTS idx_session_logs_sessionId ON session_logs(sessionId)`,
241
- );
242
- // Session costs indexes for timeseries queries
243
- database.run(
244
- `CREATE INDEX IF NOT EXISTS idx_session_costs_createdAt ON session_costs(createdAt)`,
245
- );
246
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_costs_taskId ON session_costs(taskId)`);
247
- database.run(`CREATE INDEX IF NOT EXISTS idx_session_costs_agentId ON session_costs(agentId)`);
248
- database.run(
249
- `CREATE INDEX IF NOT EXISTS idx_session_costs_agent_createdAt ON session_costs(agentId, createdAt)`,
250
- );
251
- database.run(
252
- `CREATE INDEX IF NOT EXISTS idx_inbox_messages_agentId ON inbox_messages(agentId)`,
253
- );
254
- database.run(`CREATE INDEX IF NOT EXISTS idx_inbox_messages_status ON inbox_messages(status)`);
255
-
256
- // Scheduled tasks table
257
- database.run(`
258
- CREATE TABLE IF NOT EXISTS scheduled_tasks (
259
- id TEXT PRIMARY KEY,
260
- name TEXT NOT NULL UNIQUE,
261
- description TEXT,
262
- cronExpression TEXT,
263
- intervalMs INTEGER,
264
- taskTemplate TEXT NOT NULL,
265
- taskType TEXT,
266
- tags TEXT DEFAULT '[]',
267
- priority INTEGER DEFAULT 50,
268
- targetAgentId TEXT,
269
- enabled INTEGER DEFAULT 1,
270
- lastRunAt TEXT,
271
- nextRunAt TEXT,
272
- createdByAgentId TEXT,
273
- timezone TEXT DEFAULT 'UTC',
274
- createdAt TEXT NOT NULL,
275
- lastUpdatedAt TEXT NOT NULL,
276
- CHECK (cronExpression IS NOT NULL OR intervalMs IS NOT NULL)
277
- )
278
- `);
279
-
280
- // Scheduled tasks indexes
281
- database.run(
282
- `CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_enabled ON scheduled_tasks(enabled)`,
283
- );
284
- database.run(
285
- `CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_nextRunAt ON scheduled_tasks(nextRunAt)`,
286
- );
287
-
288
- // Epics table - project-level task organization
289
- database.run(`
290
- CREATE TABLE IF NOT EXISTS epics (
291
- id TEXT PRIMARY KEY,
292
- name TEXT NOT NULL UNIQUE,
293
- description TEXT,
294
- goal TEXT NOT NULL,
295
- prd TEXT,
296
- plan TEXT,
297
- status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'active', 'paused', 'completed', 'cancelled')),
298
- priority INTEGER DEFAULT 50,
299
- tags TEXT DEFAULT '[]',
300
- createdByAgentId TEXT,
301
- leadAgentId TEXT,
302
- channelId TEXT,
303
- researchDocPath TEXT,
304
- planDocPath TEXT,
305
- slackChannelId TEXT,
306
- slackThreadTs TEXT,
307
- githubRepo TEXT,
308
- githubMilestone TEXT,
309
- createdAt TEXT NOT NULL,
310
- lastUpdatedAt TEXT NOT NULL,
311
- startedAt TEXT,
312
- completedAt TEXT,
313
- FOREIGN KEY (createdByAgentId) REFERENCES agents(id) ON DELETE SET NULL,
314
- FOREIGN KEY (leadAgentId) REFERENCES agents(id) ON DELETE SET NULL,
315
- FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE SET NULL
316
- )
317
- `);
318
-
319
- // Epics indexes
320
- database.run(`CREATE INDEX IF NOT EXISTS idx_epics_status ON epics(status)`);
321
- database.run(
322
- `CREATE INDEX IF NOT EXISTS idx_epics_createdByAgentId ON epics(createdByAgentId)`,
323
- );
324
- database.run(`CREATE INDEX IF NOT EXISTS idx_epics_leadAgentId ON epics(leadAgentId)`);
325
- });
326
-
327
- initSchema();
328
-
329
- // Seed default general channel if it doesn't exist
330
- // Use a stable UUID for the general channel so it's consistent across restarts
331
- const generalChannelId = "00000000-0000-4000-8000-000000000001";
332
- try {
333
- // Migration: Fix old 'general' channel ID that wasn't a valid UUID
334
- db.run(`UPDATE channels SET id = ? WHERE id = 'general'`, [generalChannelId]);
335
- db.run(`UPDATE channel_messages SET channelId = ? WHERE channelId = 'general'`, [
336
- generalChannelId,
337
- ]);
338
- db.run(`UPDATE channel_read_state SET channelId = ? WHERE channelId = 'general'`, [
339
- generalChannelId,
340
- ]);
341
- } catch {
342
- /* Migration not needed or already applied */
343
- }
344
- try {
345
- db.run(
346
- `
347
- INSERT OR IGNORE INTO channels (id, name, description, type, createdAt)
348
- VALUES (?, 'general', 'Default channel for all agents', 'public', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
349
- `,
350
- [generalChannelId],
351
- );
352
- } catch {
353
- /* Channel already exists */
354
- }
355
-
356
- // Migration: Add new columns to existing databases (SQLite doesn't support IF NOT EXISTS for columns)
357
- // Agent task columns
358
- try {
359
- db.run(
360
- `ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api'))`,
361
- );
362
- } catch {
363
- /* exists */
364
- }
365
- try {
366
- db.run(`ALTER TABLE agent_tasks ADD COLUMN slackChannelId TEXT`);
367
- } catch {
368
- /* exists */
369
- }
370
- try {
371
- db.run(`ALTER TABLE agent_tasks ADD COLUMN slackThreadTs TEXT`);
372
- } catch {
373
- /* exists */
374
- }
375
- try {
376
- db.run(`ALTER TABLE agent_tasks ADD COLUMN slackUserId TEXT`);
377
- } catch {
378
- /* exists */
379
- }
380
- try {
381
- db.run(`ALTER TABLE agent_tasks ADD COLUMN taskType TEXT`);
382
- } catch {
383
- /* exists */
384
- }
385
- try {
386
- db.run(`ALTER TABLE agent_tasks ADD COLUMN tags TEXT DEFAULT '[]'`);
387
- } catch {
388
- /* exists */
389
- }
390
- try {
391
- db.run(`ALTER TABLE agent_tasks ADD COLUMN priority INTEGER DEFAULT 50`);
392
- } catch {
393
- /* exists */
394
- }
395
- try {
396
- db.run(`ALTER TABLE agent_tasks ADD COLUMN dependsOn TEXT DEFAULT '[]'`);
397
- } catch {
398
- /* exists */
399
- }
400
- try {
401
- db.run(`ALTER TABLE agent_tasks ADD COLUMN offeredTo TEXT`);
402
- } catch {
403
- /* exists */
404
- }
405
- try {
406
- db.run(`ALTER TABLE agent_tasks ADD COLUMN offeredAt TEXT`);
407
- } catch {
408
- /* exists */
409
- }
410
- try {
411
- db.run(`ALTER TABLE agent_tasks ADD COLUMN acceptedAt TEXT`);
412
- } catch {
413
- /* exists */
414
- }
415
- try {
416
- db.run(`ALTER TABLE agent_tasks ADD COLUMN rejectionReason TEXT`);
417
- } catch {
418
- /* exists */
419
- }
420
- try {
421
- db.run(`ALTER TABLE agent_tasks ADD COLUMN creatorAgentId TEXT`);
422
- } catch {
423
- /* exists */
424
- }
425
- // Mention-to-task columns
426
- try {
427
- db.run(`ALTER TABLE agent_tasks ADD COLUMN mentionMessageId TEXT`);
428
- } catch {
429
- /* exists */
430
- }
431
- try {
432
- db.run(`ALTER TABLE agent_tasks ADD COLUMN mentionChannelId TEXT`);
433
- } catch {
434
- /* exists */
435
- }
436
- // GitHub-specific columns
437
- try {
438
- db.run(`ALTER TABLE agent_tasks ADD COLUMN githubRepo TEXT`);
439
- } catch {
440
- /* exists */
441
- }
442
- try {
443
- db.run(`ALTER TABLE agent_tasks ADD COLUMN githubEventType TEXT`);
444
- } catch {
445
- /* exists */
446
- }
447
- try {
448
- db.run(`ALTER TABLE agent_tasks ADD COLUMN githubNumber INTEGER`);
449
- } catch {
450
- /* exists */
451
- }
452
- try {
453
- db.run(`ALTER TABLE agent_tasks ADD COLUMN githubCommentId INTEGER`);
454
- } catch {
455
- /* exists */
456
- }
457
- try {
458
- db.run(`ALTER TABLE agent_tasks ADD COLUMN githubAuthor TEXT`);
459
- } catch {
460
- /* exists */
461
- }
462
- try {
463
- db.run(`ALTER TABLE agent_tasks ADD COLUMN githubUrl TEXT`);
464
- } catch {
465
- /* exists */
466
- }
467
- // Agent profile columns
468
- try {
469
- db.run(`ALTER TABLE agents ADD COLUMN description TEXT`);
470
- } catch {
471
- /* exists */
472
- }
473
- try {
474
- db.run(`ALTER TABLE agents ADD COLUMN role TEXT`);
475
- } catch {
476
- /* exists */
477
- }
478
- try {
479
- db.run(`ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]'`);
480
- } catch {
481
- /* exists */
482
- }
483
- // Concurrency limit column
484
- try {
485
- db.run(`ALTER TABLE agents ADD COLUMN maxTasks INTEGER DEFAULT 1`);
486
- } catch {
487
- /* exists */
488
- }
489
-
490
- // Polling limit tracking column
491
- try {
492
- db.run(`ALTER TABLE agents ADD COLUMN emptyPollCount INTEGER DEFAULT 0`);
493
- } catch {
494
- /* exists */
495
- }
496
-
497
- // CLAUDE.md storage column
498
- try {
499
- db.run(`ALTER TABLE agents ADD COLUMN claudeMd TEXT`);
500
- } catch {
501
- /* exists */
502
- }
503
-
504
- // Service PM2 columns migration
505
- try {
506
- db.run(`ALTER TABLE services ADD COLUMN script TEXT NOT NULL DEFAULT ''`);
507
- } catch {
508
- /* exists */
509
- }
510
- try {
511
- db.run(`ALTER TABLE services ADD COLUMN cwd TEXT`);
512
- } catch {
513
- /* exists */
514
- }
515
- try {
516
- db.run(`ALTER TABLE services ADD COLUMN interpreter TEXT`);
517
- } catch {
518
- /* exists */
519
- }
520
- try {
521
- db.run(`ALTER TABLE services ADD COLUMN args TEXT`);
522
- } catch {
523
- /* exists */
524
- }
525
- try {
526
- db.run(`ALTER TABLE services ADD COLUMN env TEXT`);
527
- } catch {
528
- /* exists */
529
- }
64
+ // Run database migrations (schema creation + incremental changes)
65
+ runMigrations(database);
530
66
 
531
- // Migration: Add processing_since column to channel_read_state for Phase 3
532
- try {
533
- db.run(`ALTER TABLE channel_read_state ADD COLUMN processing_since TEXT`);
534
- } catch {
535
- /* exists */
536
- }
537
-
538
- // Migration: Add notifiedAt column to agent_tasks for Phase 4
539
- try {
540
- db.run(`ALTER TABLE agent_tasks ADD COLUMN notifiedAt TEXT`);
541
- } catch {
542
- /* exists */
543
- }
67
+ // Compatibility migration for legacy databases that predate profile fields
68
+ ensureAgentProfileColumns(database);
544
69
 
545
- // Migration: Update inbox_messages CHECK constraint to include 'processing' status
546
- // SQLite doesn't support ALTER TABLE MODIFY COLUMN, so we need to recreate the table
70
+ // Migration: Remove restrictive CHECK constraint on agent_tasks.status
71
+ // Old databases have CHECK(status IN ('pending','in_progress','completed','failed'))
72
+ // which blocks 'cancelled', 'paused', 'offered', 'unassigned' statuses
547
73
  try {
548
- // Check if the table schema already includes 'processing' in the CHECK constraint
549
- const schemaInfo = db
74
+ const taskSchemaInfo = db
550
75
  .prepare<{ sql: string | null }, []>(
551
- "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'inbox_messages'",
76
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'agent_tasks'",
552
77
  )
553
78
  .get();
554
79
 
555
- const needsMigration = schemaInfo?.sql && !schemaInfo.sql.includes("'processing'");
80
+ const schemaSql = taskSchemaInfo?.sql ?? "";
81
+ const hasStatusCheck = /status\s+TEXT\b[^,]*\bCHECK\s*\(\s*status\s+IN\s*\(/i.test(schemaSql);
82
+ const statusAllowsCancelled = /status\s+IN\s*\([^)]*'cancelled'/i.test(schemaSql);
83
+ const needsStatusMigration = hasStatusCheck && !statusAllowsCancelled;
556
84
 
557
- if (needsMigration) {
558
- console.log(
559
- "[Migration] Updating inbox_messages CHECK constraint to include 'processing' status",
560
- );
85
+ if (needsStatusMigration) {
86
+ console.log("[Migration] Removing restrictive CHECK constraint on agent_tasks.status");
561
87
  db.run("PRAGMA foreign_keys=off");
562
88
 
563
89
  db.run(`
564
- CREATE TABLE inbox_messages_new (
90
+ CREATE TABLE agent_tasks_new (
565
91
  id TEXT PRIMARY KEY,
566
- agentId TEXT NOT NULL,
567
- content TEXT NOT NULL,
568
- source TEXT NOT NULL DEFAULT 'slack',
569
- status TEXT NOT NULL DEFAULT 'unread' CHECK(status IN ('unread', 'processing', 'read', 'responded', 'delegated')),
92
+ agentId TEXT,
93
+ creatorAgentId TEXT,
94
+ task TEXT NOT NULL,
95
+ status TEXT NOT NULL DEFAULT 'pending',
96
+ source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api', 'github', 'agentmail', 'system', 'schedule')),
97
+ taskType TEXT,
98
+ tags TEXT DEFAULT '[]',
99
+ priority INTEGER DEFAULT 50,
100
+ dependsOn TEXT DEFAULT '[]',
101
+ offeredTo TEXT,
102
+ offeredAt TEXT,
103
+ acceptedAt TEXT,
104
+ rejectionReason TEXT,
570
105
  slackChannelId TEXT,
571
106
  slackThreadTs TEXT,
572
107
  slackUserId TEXT,
573
- matchedText TEXT,
574
- delegatedToTaskId TEXT,
575
- responseText TEXT,
576
108
  createdAt TEXT NOT NULL,
577
109
  lastUpdatedAt TEXT NOT NULL,
578
- FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
579
- FOREIGN KEY (delegatedToTaskId) REFERENCES agent_tasks(id) ON DELETE SET NULL
110
+ finishedAt TEXT,
111
+ failureReason TEXT,
112
+ output TEXT,
113
+ progress TEXT,
114
+ notifiedAt TEXT,
115
+ mentionMessageId TEXT,
116
+ mentionChannelId TEXT,
117
+ githubRepo TEXT,
118
+ githubEventType TEXT,
119
+ githubNumber INTEGER,
120
+ githubCommentId INTEGER,
121
+ githubAuthor TEXT,
122
+ githubUrl TEXT,
123
+ epicId TEXT REFERENCES epics(id) ON DELETE SET NULL,
124
+ parentTaskId TEXT,
125
+ claudeSessionId TEXT,
126
+ agentmailInboxId TEXT,
127
+ agentmailMessageId TEXT,
128
+ agentmailThreadId TEXT,
129
+ model TEXT,
130
+ scheduleId TEXT
580
131
  )
581
132
  `);
582
133
 
583
- db.run("INSERT INTO inbox_messages_new SELECT * FROM inbox_messages");
584
- db.run("DROP TABLE inbox_messages");
585
- db.run("ALTER TABLE inbox_messages_new RENAME TO inbox_messages");
134
+ // Copy all data use column list to handle any column ordering differences
135
+ db.run(`
136
+ INSERT INTO agent_tasks_new (
137
+ id, agentId, creatorAgentId, task, status, source, taskType, tags,
138
+ priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
139
+ slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt,
140
+ finishedAt, failureReason, output, progress, notifiedAt,
141
+ mentionMessageId, mentionChannelId, githubRepo, githubEventType,
142
+ githubNumber, githubCommentId, githubAuthor, githubUrl,
143
+ epicId, parentTaskId, claudeSessionId,
144
+ agentmailInboxId, agentmailMessageId, agentmailThreadId,
145
+ model, scheduleId
146
+ )
147
+ SELECT
148
+ id, agentId, creatorAgentId, task, status, source, taskType, tags,
149
+ priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
150
+ slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt,
151
+ finishedAt, failureReason, output, progress, notifiedAt,
152
+ mentionMessageId, mentionChannelId, githubRepo, githubEventType,
153
+ githubNumber, githubCommentId, githubAuthor, githubUrl,
154
+ epicId, parentTaskId, claudeSessionId,
155
+ agentmailInboxId, agentmailMessageId, agentmailThreadId,
156
+ model, scheduleId
157
+ FROM agent_tasks
158
+ `);
586
159
 
587
- // Recreate indexes
588
- db.run("CREATE INDEX IF NOT EXISTS idx_inbox_messages_agentId ON inbox_messages(agentId)");
589
- db.run("CREATE INDEX IF NOT EXISTS idx_inbox_messages_status ON inbox_messages(status)");
160
+ db.run("DROP TABLE agent_tasks");
161
+ db.run("ALTER TABLE agent_tasks_new RENAME TO agent_tasks");
162
+
163
+ // Recreate all indexes
164
+ db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId)");
165
+ db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status)");
166
+ db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_offeredTo ON agent_tasks(offeredTo)");
167
+ db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_taskType ON agent_tasks(taskType)");
168
+ db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_epicId ON agent_tasks(epicId)");
169
+ db.run(
170
+ "CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentmailThreadId ON agent_tasks(agentmailThreadId)",
171
+ );
172
+ db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_schedule_id ON agent_tasks(scheduleId)");
590
173
 
591
174
  db.run("PRAGMA foreign_keys=on");
592
- console.log("[Migration] Successfully updated inbox_messages table");
175
+ console.log("[Migration] Successfully removed CHECK constraint on agent_tasks.status");
593
176
  }
594
177
  } catch (e) {
595
- console.error("[Migration] Failed to update inbox_messages CHECK constraint:", e);
178
+ console.error("[Migration] Failed to update agent_tasks CHECK constraint:", e);
596
179
  try {
597
180
  db.run("PRAGMA foreign_keys=on");
598
- } catch {}
181
+ } catch (cleanupError) {
182
+ console.error("[Migration] Failed to re-enable SQLite foreign_keys pragma:", cleanupError);
183
+ }
599
184
  throw e;
600
185
  }
601
186
 
602
- // Create indexes on new columns (after migrations add them)
603
- try {
604
- db.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_offeredTo ON agent_tasks(offeredTo)`);
605
- } catch {
606
- /* exists or column missing */
607
- }
608
- try {
609
- db.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_taskType ON agent_tasks(taskType)`);
610
- } catch {
611
- /* exists or column missing */
612
- }
613
-
614
- // Epic feature migration: Add epicId to agent_tasks
615
- try {
616
- db.run(
617
- `ALTER TABLE agent_tasks ADD COLUMN epicId TEXT REFERENCES epics(id) ON DELETE SET NULL`,
618
- );
619
- } catch {
620
- /* exists */
621
- }
622
- try {
623
- db.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_epicId ON agent_tasks(epicId)`);
624
- } catch {
625
- /* exists */
626
- }
627
-
628
- // Epic progress trigger migration: Add progressNotifiedAt to epics
629
- try {
630
- db.run(`ALTER TABLE epics ADD COLUMN progressNotifiedAt TEXT`);
631
- } catch {
632
- /* exists */
633
- }
634
-
635
- // Epic channel migration: Add channelId to epics
636
- try {
637
- db.run(
638
- `ALTER TABLE epics ADD COLUMN channelId TEXT REFERENCES channels(id) ON DELETE SET NULL`,
639
- );
640
- } catch {
641
- /* exists */
642
- }
187
+ // Backfill: Seed v1 for existing agents that don't have any context versions yet
188
+ seedContextVersions();
643
189
 
644
190
  return db;
645
191
  }
@@ -659,63 +205,285 @@ export function closeDb(): void {
659
205
  }
660
206
 
661
207
  // ============================================================================
662
- // Agent Queries
208
+ // Context Versioning
663
209
  // ============================================================================
664
210
 
665
- type AgentRow = {
211
+ const VERSIONABLE_FIELDS: VersionableField[] = [
212
+ "soulMd",
213
+ "identityMd",
214
+ "toolsMd",
215
+ "claudeMd",
216
+ "setupScript",
217
+ ];
218
+
219
+ function ensureAgentProfileColumns(database: Database): void {
220
+ const existingColumns = new Set(
221
+ database
222
+ .prepare<{ name: string }, []>("PRAGMA table_info(agents)")
223
+ .all()
224
+ .map((row) => row.name),
225
+ );
226
+
227
+ for (const column of VERSIONABLE_FIELDS) {
228
+ if (!existingColumns.has(column)) {
229
+ try {
230
+ database.run(`ALTER TABLE agents ADD COLUMN ${column} TEXT`);
231
+ } catch (error) {
232
+ console.error(`[Migration] Failed to add missing agents.${column} column`, error);
233
+ throw error;
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ function computeContentHash(content: string): string {
240
+ const hasher = new Bun.CryptoHasher("sha256");
241
+ hasher.update(content);
242
+ return hasher.digest("hex");
243
+ }
244
+
245
+ type ContextVersionRow = {
666
246
  id: string;
667
- name: string;
668
- isLead: number;
669
- status: AgentStatus;
670
- description: string | null;
671
- role: string | null;
672
- capabilities: string | null;
673
- maxTasks: number | null;
674
- emptyPollCount: number | null;
675
- claudeMd: string | null;
247
+ agentId: string;
248
+ field: string;
249
+ content: string;
250
+ version: number;
251
+ changeSource: string;
252
+ changedByAgentId: string | null;
253
+ changeReason: string | null;
254
+ contentHash: string;
255
+ previousVersionId: string | null;
676
256
  createdAt: string;
677
- lastUpdatedAt: string;
678
257
  };
679
258
 
680
- function rowToAgent(row: AgentRow): Agent {
259
+ function rowToContextVersion(row: ContextVersionRow): ContextVersion {
681
260
  return {
682
261
  id: row.id,
683
- name: row.name,
684
- isLead: row.isLead === 1,
685
- status: row.status,
686
- description: row.description ?? undefined,
687
- role: row.role ?? undefined,
688
- capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
689
- maxTasks: row.maxTasks ?? 1,
690
- emptyPollCount: row.emptyPollCount ?? 0,
691
- claudeMd: row.claudeMd ?? undefined,
262
+ agentId: row.agentId,
263
+ field: row.field as VersionableField,
264
+ content: row.content,
265
+ version: row.version,
266
+ changeSource: row.changeSource as ChangeSource,
267
+ changedByAgentId: row.changedByAgentId,
268
+ changeReason: row.changeReason,
269
+ contentHash: row.contentHash,
270
+ previousVersionId: row.previousVersionId,
692
271
  createdAt: row.createdAt,
693
- lastUpdatedAt: row.lastUpdatedAt,
694
272
  };
695
273
  }
696
274
 
697
- export const agentQueries = {
698
- insert: () =>
699
- getDb().prepare<AgentRow, [string, string, number, AgentStatus, number]>(
700
- "INSERT INTO agents (id, name, isLead, status, maxTasks, createdAt, lastUpdatedAt) VALUES (?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *",
701
- ),
275
+ export function createContextVersion(params: {
276
+ agentId: string;
277
+ field: VersionableField;
278
+ content: string;
279
+ version: number;
280
+ changeSource: ChangeSource;
281
+ changedByAgentId?: string | null;
282
+ changeReason?: string | null;
283
+ contentHash: string;
284
+ previousVersionId?: string | null;
285
+ }): ContextVersion {
286
+ const id = crypto.randomUUID();
287
+ const now = new Date().toISOString();
702
288
 
703
- getById: () => getDb().prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?"),
289
+ const row = getDb()
290
+ .prepare<
291
+ ContextVersionRow,
292
+ [
293
+ string,
294
+ string,
295
+ string,
296
+ string,
297
+ number,
298
+ string,
299
+ string | null,
300
+ string | null,
301
+ string,
302
+ string | null,
303
+ string,
304
+ ]
305
+ >(
306
+ `INSERT INTO context_versions (id, agentId, field, content, version, changeSource, changedByAgentId, changeReason, contentHash, previousVersionId, createdAt)
307
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
308
+ )
309
+ .get(
310
+ id,
311
+ params.agentId,
312
+ params.field,
313
+ params.content,
314
+ params.version,
315
+ params.changeSource,
316
+ params.changedByAgentId ?? null,
317
+ params.changeReason ?? null,
318
+ params.contentHash,
319
+ params.previousVersionId ?? null,
320
+ now,
321
+ );
704
322
 
705
- getAll: () => getDb().prepare<AgentRow, []>("SELECT * FROM agents ORDER BY name"),
323
+ if (!row) throw new Error("Failed to create context version");
324
+ return rowToContextVersion(row);
325
+ }
706
326
 
707
- updateStatus: () =>
708
- getDb().prepare<AgentRow, [AgentStatus, string]>(
709
- "UPDATE agents SET status = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
710
- ),
327
+ export function getLatestContextVersion(
328
+ agentId: string,
329
+ field: VersionableField,
330
+ ): ContextVersion | null {
331
+ const row = getDb()
332
+ .prepare<ContextVersionRow, [string, string]>(
333
+ `SELECT * FROM context_versions WHERE agentId = ? AND field = ? ORDER BY version DESC LIMIT 1`,
334
+ )
335
+ .get(agentId, field);
711
336
 
712
- delete: () => getDb().prepare<null, [string]>("DELETE FROM agents WHERE id = ?"),
713
- };
337
+ return row ? rowToContextVersion(row) : null;
338
+ }
714
339
 
715
- export function createAgent(
716
- agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
717
- ): Agent {
718
- const id = agent.id ?? crypto.randomUUID();
340
+ export function getContextVersion(id: string): ContextVersion | null {
341
+ const row = getDb()
342
+ .prepare<ContextVersionRow, [string]>(`SELECT * FROM context_versions WHERE id = ?`)
343
+ .get(id);
344
+
345
+ return row ? rowToContextVersion(row) : null;
346
+ }
347
+
348
+ export function getContextVersionHistory(params: {
349
+ agentId: string;
350
+ field?: VersionableField;
351
+ limit?: number;
352
+ }): ContextVersion[] {
353
+ const limit = params.limit ?? 10;
354
+
355
+ if (params.field) {
356
+ const rows = getDb()
357
+ .prepare<ContextVersionRow, [string, string, number]>(
358
+ `SELECT * FROM context_versions WHERE agentId = ? AND field = ? ORDER BY version DESC LIMIT ?`,
359
+ )
360
+ .all(params.agentId, params.field, limit);
361
+ return rows.map(rowToContextVersion);
362
+ }
363
+
364
+ const rows = getDb()
365
+ .prepare<ContextVersionRow, [string, number]>(
366
+ `SELECT * FROM context_versions WHERE agentId = ? ORDER BY createdAt DESC LIMIT ?`,
367
+ )
368
+ .all(params.agentId, limit);
369
+ return rows.map(rowToContextVersion);
370
+ }
371
+
372
+ /**
373
+ * Seed v1 context versions for existing agents that don't have any versions yet.
374
+ * Called during migration.
375
+ */
376
+ function seedContextVersions(): void {
377
+ const database = getDb();
378
+ const agents = database
379
+ .prepare<
380
+ {
381
+ id: string;
382
+ soulMd: string | null;
383
+ identityMd: string | null;
384
+ toolsMd: string | null;
385
+ claudeMd: string | null;
386
+ setupScript: string | null;
387
+ },
388
+ []
389
+ >(`SELECT id, soulMd, identityMd, toolsMd, claudeMd, setupScript FROM agents`)
390
+ .all();
391
+
392
+ for (const agent of agents) {
393
+ for (const field of VERSIONABLE_FIELDS) {
394
+ const content = agent[field];
395
+ if (!content) continue;
396
+
397
+ // Check if a version already exists for this agent+field
398
+ const existing = database
399
+ .prepare<{ id: string }, [string, string]>(
400
+ `SELECT id FROM context_versions WHERE agentId = ? AND field = ? LIMIT 1`,
401
+ )
402
+ .get(agent.id, field);
403
+ if (existing) continue;
404
+
405
+ const id = crypto.randomUUID();
406
+ const hash = computeContentHash(content);
407
+ const now = new Date().toISOString();
408
+
409
+ database
410
+ .prepare(
411
+ `INSERT INTO context_versions (id, agentId, field, content, version, changeSource, contentHash, createdAt)
412
+ VALUES (?, ?, ?, ?, 1, 'system', ?, ?)`,
413
+ )
414
+ .run(id, agent.id, field, content, hash, now);
415
+ }
416
+ }
417
+ }
418
+
419
+ // ============================================================================
420
+ // Agent Queries
421
+ // ============================================================================
422
+
423
+ type AgentRow = {
424
+ id: string;
425
+ name: string;
426
+ isLead: number;
427
+ status: AgentStatus;
428
+ description: string | null;
429
+ role: string | null;
430
+ capabilities: string | null;
431
+ maxTasks: number | null;
432
+ emptyPollCount: number | null;
433
+ claudeMd: string | null;
434
+ soulMd: string | null;
435
+ identityMd: string | null;
436
+ setupScript: string | null;
437
+ toolsMd: string | null;
438
+ lastActivityAt: string | null;
439
+ createdAt: string;
440
+ lastUpdatedAt: string;
441
+ };
442
+
443
+ function rowToAgent(row: AgentRow): Agent {
444
+ return {
445
+ id: row.id,
446
+ name: row.name,
447
+ isLead: row.isLead === 1,
448
+ status: row.status,
449
+ description: row.description ?? undefined,
450
+ role: row.role ?? undefined,
451
+ capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
452
+ maxTasks: row.maxTasks ?? 1,
453
+ emptyPollCount: row.emptyPollCount ?? 0,
454
+ claudeMd: row.claudeMd ?? undefined,
455
+ soulMd: row.soulMd ?? undefined,
456
+ identityMd: row.identityMd ?? undefined,
457
+ setupScript: row.setupScript ?? undefined,
458
+ toolsMd: row.toolsMd ?? undefined,
459
+ lastActivityAt: row.lastActivityAt ?? undefined,
460
+ createdAt: row.createdAt,
461
+ lastUpdatedAt: row.lastUpdatedAt,
462
+ };
463
+ }
464
+
465
+ export const agentQueries = {
466
+ insert: () =>
467
+ getDb().prepare<AgentRow, [string, string, number, AgentStatus, number]>(
468
+ "INSERT INTO agents (id, name, isLead, status, maxTasks, createdAt, lastUpdatedAt) VALUES (?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *",
469
+ ),
470
+
471
+ getById: () => getDb().prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?"),
472
+
473
+ getAll: () => getDb().prepare<AgentRow, []>("SELECT * FROM agents ORDER BY name"),
474
+
475
+ updateStatus: () =>
476
+ getDb().prepare<AgentRow, [AgentStatus, string]>(
477
+ "UPDATE agents SET status = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
478
+ ),
479
+
480
+ delete: () => getDb().prepare<null, [string]>("DELETE FROM agents WHERE id = ?"),
481
+ };
482
+
483
+ export function createAgent(
484
+ agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
485
+ ): Agent {
486
+ const id = agent.id ?? crypto.randomUUID();
719
487
  const maxTasks = agent.maxTasks ?? 1;
720
488
  const row = agentQueries
721
489
  .insert()
@@ -736,6 +504,11 @@ export function getAllAgents(): Agent[] {
736
504
  return agentQueries.getAll().all().map(rowToAgent);
737
505
  }
738
506
 
507
+ export function getLeadAgent(): Agent | null {
508
+ const agents = getAllAgents();
509
+ return agents.find((a) => a.isLead) ?? null;
510
+ }
511
+
739
512
  export function updateAgentStatus(id: string, status: AgentStatus): Agent | null {
740
513
  const oldAgent = getAgentById(id);
741
514
  const row = agentQueries.updateStatus().get(status, id);
@@ -762,6 +535,14 @@ export function updateAgentMaxTasks(id: string, maxTasks: number): Agent | null
762
535
  return row ? rowToAgent(row) : null;
763
536
  }
764
537
 
538
+ export function updateAgentActivity(id: string): void {
539
+ getDb()
540
+ .prepare<null, [string]>(
541
+ `UPDATE agents SET lastActivityAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
542
+ )
543
+ .run(id);
544
+ }
545
+
765
546
  // ============================================================================
766
547
  // Agent Poll Tracking Functions
767
548
  // ============================================================================
@@ -895,15 +676,27 @@ type AgentTaskRow = {
895
676
  slackChannelId: string | null;
896
677
  slackThreadTs: string | null;
897
678
  slackUserId: string | null;
898
- githubRepo: string | null;
899
- githubEventType: string | null;
900
- githubNumber: number | null;
901
- githubCommentId: number | null;
902
- githubAuthor: string | null;
903
- githubUrl: string | null;
679
+ vcsProvider: string | null;
680
+ vcsRepo: string | null;
681
+ vcsEventType: string | null;
682
+ vcsNumber: number | null;
683
+ vcsCommentId: number | null;
684
+ vcsAuthor: string | null;
685
+ vcsUrl: string | null;
686
+ agentmailInboxId: string | null;
687
+ agentmailMessageId: string | null;
688
+ agentmailThreadId: string | null;
904
689
  mentionMessageId: string | null;
905
690
  mentionChannelId: string | null;
906
691
  epicId: string | null;
692
+ dir: string | null;
693
+ parentTaskId: string | null;
694
+ claudeSessionId: string | null;
695
+ model: string | null;
696
+ scheduleId: string | null;
697
+ workflowRunId: string | null;
698
+ workflowRunStepId: string | null;
699
+ outputSchema: string | null;
907
700
  createdAt: string;
908
701
  lastUpdatedAt: string;
909
702
  finishedAt: string | null;
@@ -932,15 +725,27 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
932
725
  slackChannelId: row.slackChannelId ?? undefined,
933
726
  slackThreadTs: row.slackThreadTs ?? undefined,
934
727
  slackUserId: row.slackUserId ?? undefined,
935
- githubRepo: row.githubRepo ?? undefined,
936
- githubEventType: row.githubEventType ?? undefined,
937
- githubNumber: row.githubNumber ?? undefined,
938
- githubCommentId: row.githubCommentId ?? undefined,
939
- githubAuthor: row.githubAuthor ?? undefined,
940
- githubUrl: row.githubUrl ?? undefined,
728
+ vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
729
+ vcsRepo: row.vcsRepo ?? undefined,
730
+ vcsEventType: row.vcsEventType ?? undefined,
731
+ vcsNumber: row.vcsNumber ?? undefined,
732
+ vcsCommentId: row.vcsCommentId ?? undefined,
733
+ vcsAuthor: row.vcsAuthor ?? undefined,
734
+ vcsUrl: row.vcsUrl ?? undefined,
735
+ agentmailInboxId: row.agentmailInboxId ?? undefined,
736
+ agentmailMessageId: row.agentmailMessageId ?? undefined,
737
+ agentmailThreadId: row.agentmailThreadId ?? undefined,
941
738
  mentionMessageId: row.mentionMessageId ?? undefined,
942
739
  mentionChannelId: row.mentionChannelId ?? undefined,
943
740
  epicId: row.epicId ?? undefined,
741
+ dir: row.dir ?? undefined,
742
+ parentTaskId: row.parentTaskId ?? undefined,
743
+ claudeSessionId: row.claudeSessionId ?? undefined,
744
+ model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
745
+ scheduleId: row.scheduleId ?? undefined,
746
+ workflowRunId: row.workflowRunId ?? undefined,
747
+ workflowRunStepId: row.workflowRunStepId ?? undefined,
748
+ outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
944
749
  createdAt: row.createdAt,
945
750
  lastUpdatedAt: row.lastUpdatedAt,
946
751
  finishedAt: row.finishedAt ?? undefined,
@@ -1006,7 +811,10 @@ export const taskQueries = {
1006
811
 
1007
812
  setProgress: () =>
1008
813
  getDb().prepare<AgentTaskRow, [string, string]>(
1009
- "UPDATE agent_tasks SET progress = ?, status = 'in_progress', lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
814
+ `UPDATE agent_tasks SET progress = ?,
815
+ status = CASE WHEN status IN ('completed', 'failed', 'cancelled') THEN status ELSE 'in_progress' END,
816
+ lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
817
+ WHERE id = ? RETURNING *`,
1010
818
  ),
1011
819
 
1012
820
  delete: () => getDb().prepare<null, [string]>("DELETE FROM agent_tasks WHERE id = ?"),
@@ -1071,10 +879,17 @@ export function getPendingTaskForAgent(agentId: string): AgentTask | null {
1071
879
 
1072
880
  export function startTask(taskId: string): AgentTask | null {
1073
881
  const oldTask = getTaskById(taskId);
882
+ if (!oldTask) return null;
883
+
884
+ // Guard: never revive tasks that are already in a terminal state
885
+ if (["completed", "failed", "cancelled"].includes(oldTask.status)) {
886
+ return null;
887
+ }
888
+
1074
889
  const row = getDb()
1075
890
  .prepare<AgentTaskRow, [string]>(
1076
891
  `UPDATE agent_tasks SET status = 'in_progress', lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
1077
- WHERE id = ? RETURNING *`,
892
+ WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled') RETURNING *`,
1078
893
  )
1079
894
  .get(taskId);
1080
895
  if (row && oldTask) {
@@ -1096,6 +911,18 @@ export function getTaskById(id: string): AgentTask | null {
1096
911
  return row ? rowToAgentTask(row) : null;
1097
912
  }
1098
913
 
914
+ export function updateTaskClaudeSessionId(
915
+ taskId: string,
916
+ claudeSessionId: string,
917
+ ): AgentTask | null {
918
+ const row = getDb()
919
+ .prepare<AgentTaskRow, [string, string, string]>(
920
+ `UPDATE agent_tasks SET claudeSessionId = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *`,
921
+ )
922
+ .get(claudeSessionId, new Date().toISOString(), taskId);
923
+ return row ? rowToAgentTask(row) : null;
924
+ }
925
+
1099
926
  export function getTasksByAgentId(agentId: string): AgentTask[] {
1100
927
  return taskQueries.getByAgentId().all(agentId).map(rowToAgentTask);
1101
928
  }
@@ -1105,25 +932,29 @@ export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
1105
932
  }
1106
933
 
1107
934
  /**
1108
- * Find a task by GitHub repo and issue/PR number
1109
- * Returns the most recent non-completed/failed task for this GitHub entity
935
+ * Find a task by VCS repo and issue/PR/MR number.
936
+ * Returns the most recent non-completed/failed task for this VCS entity.
1110
937
  */
1111
- export function findTaskByGitHub(githubRepo: string, githubNumber: number): AgentTask | null {
938
+ export function findTaskByVcs(vcsRepo: string, vcsNumber: number): AgentTask | null {
1112
939
  const row = getDb()
1113
940
  .prepare<AgentTaskRow, [string, number]>(
1114
941
  `SELECT * FROM agent_tasks
1115
- WHERE githubRepo = ? AND githubNumber = ?
942
+ WHERE vcsRepo = ? AND vcsNumber = ?
1116
943
  AND status NOT IN ('completed', 'failed')
1117
944
  ORDER BY createdAt DESC
1118
945
  LIMIT 1`,
1119
946
  )
1120
- .get(githubRepo, githubNumber);
947
+ .get(vcsRepo, vcsNumber);
1121
948
  return row ? rowToAgentTask(row) : null;
1122
949
  }
1123
950
 
951
+ /** @deprecated Use findTaskByVcs instead */
952
+ export const findTaskByGitHub = findTaskByVcs;
953
+
1124
954
  export interface TaskFilters {
1125
955
  status?: AgentTaskStatus;
1126
956
  agentId?: string;
957
+ epicId?: string;
1127
958
  search?: string;
1128
959
  // New filters
1129
960
  unassigned?: boolean;
@@ -1131,7 +962,10 @@ export interface TaskFilters {
1131
962
  readyOnly?: boolean;
1132
963
  taskType?: string;
1133
964
  tags?: string[];
965
+ scheduleId?: string;
1134
966
  limit?: number;
967
+ offset?: number;
968
+ includeHeartbeat?: boolean;
1135
969
  }
1136
970
 
1137
971
  export function getAllTasks(filters?: TaskFilters): AgentTask[] {
@@ -1148,6 +982,11 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
1148
982
  params.push(filters.agentId);
1149
983
  }
1150
984
 
985
+ if (filters?.epicId) {
986
+ conditions.push("epicId = ?");
987
+ params.push(filters.epicId);
988
+ }
989
+
1151
990
  if (filters?.search) {
1152
991
  conditions.push("task LIKE ?");
1153
992
  params.push(`%${filters.search}%`);
@@ -1177,9 +1016,20 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
1177
1016
  }
1178
1017
  }
1179
1018
 
1019
+ if (filters?.scheduleId) {
1020
+ conditions.push("scheduleId = ?");
1021
+ params.push(filters.scheduleId);
1022
+ }
1023
+
1024
+ // Exclude heartbeat tasks by default
1025
+ if (!filters?.includeHeartbeat) {
1026
+ conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
1027
+ }
1028
+
1180
1029
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1181
1030
  const limit = filters?.limit ?? 25;
1182
- const query = `SELECT * FROM agent_tasks ${whereClause} ORDER BY lastUpdatedAt DESC, priority DESC LIMIT ${limit}`;
1031
+ const offset = filters?.offset ?? 0;
1032
+ const query = `SELECT * FROM agent_tasks ${whereClause} ORDER BY lastUpdatedAt DESC, priority DESC LIMIT ${limit} OFFSET ${offset}`;
1183
1033
 
1184
1034
  let tasks = getDb()
1185
1035
  .prepare<AgentTaskRow, (string | AgentTaskStatus)[]>(query)
@@ -1215,6 +1065,11 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
1215
1065
  params.push(filters.agentId);
1216
1066
  }
1217
1067
 
1068
+ if (filters?.epicId) {
1069
+ conditions.push("epicId = ?");
1070
+ params.push(filters.epicId);
1071
+ }
1072
+
1218
1073
  if (filters?.search) {
1219
1074
  conditions.push("task LIKE ?");
1220
1075
  params.push(`%${filters.search}%`);
@@ -1242,6 +1097,16 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
1242
1097
  }
1243
1098
  }
1244
1099
 
1100
+ if (filters?.scheduleId) {
1101
+ conditions.push("scheduleId = ?");
1102
+ params.push(filters.scheduleId);
1103
+ }
1104
+
1105
+ // Exclude heartbeat tasks by default
1106
+ if (!filters?.includeHeartbeat) {
1107
+ conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
1108
+ }
1109
+
1245
1110
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1246
1111
  const query = `SELECT COUNT(*) as count FROM agent_tasks ${whereClause}`;
1247
1112
 
@@ -1315,10 +1180,10 @@ export function getCompletedSlackTasks(): AgentTask[] {
1315
1180
  return getDb()
1316
1181
  .prepare<AgentTaskRow, []>(
1317
1182
  `SELECT * FROM agent_tasks
1318
- WHERE source = 'slack'
1319
- AND slackChannelId IS NOT NULL
1183
+ WHERE slackChannelId IS NOT NULL
1320
1184
  AND status IN ('completed', 'failed')
1321
- ORDER BY lastUpdatedAt DESC`,
1185
+ ORDER BY lastUpdatedAt DESC
1186
+ LIMIT 200`,
1322
1187
  )
1323
1188
  .all()
1324
1189
  .map(rowToAgentTask);
@@ -1363,33 +1228,51 @@ export function markTasksNotified(taskIds: string[]): number {
1363
1228
  return result.changes;
1364
1229
  }
1365
1230
 
1231
+ /**
1232
+ * Reset notifiedAt for tasks, allowing them to be re-delivered on next poll.
1233
+ * Used when a trigger was consumed but the session that should process it failed.
1234
+ * This prevents permanent notification loss from the mark-before-process race.
1235
+ */
1236
+ export function resetTasksNotified(taskIds: string[]): number {
1237
+ if (taskIds.length === 0) return 0;
1238
+
1239
+ const placeholders = taskIds.map(() => "?").join(",");
1240
+
1241
+ const result = getDb().run(
1242
+ `UPDATE agent_tasks SET notifiedAt = NULL
1243
+ WHERE id IN (${placeholders}) AND notifiedAt IS NOT NULL`,
1244
+ taskIds,
1245
+ );
1246
+
1247
+ return result.changes;
1248
+ }
1249
+
1366
1250
  export function getInProgressSlackTasks(): AgentTask[] {
1367
1251
  return getDb()
1368
1252
  .prepare<AgentTaskRow, []>(
1369
1253
  `SELECT * FROM agent_tasks
1370
- WHERE source = 'slack'
1371
- AND slackChannelId IS NOT NULL
1254
+ WHERE slackChannelId IS NOT NULL
1372
1255
  AND status = 'in_progress'
1373
- ORDER BY lastUpdatedAt DESC`,
1256
+ ORDER BY lastUpdatedAt DESC
1257
+ LIMIT 200`,
1374
1258
  )
1375
1259
  .all()
1376
1260
  .map(rowToAgentTask);
1377
1261
  }
1378
1262
 
1379
1263
  /**
1380
- * Find an agent that has an active task or inbox message in a specific Slack thread.
1381
- * Used for routing thread follow-up messages to the same agent.
1382
- * Checks both tasks (for workers) and inbox_messages (for leads).
1264
+ * Find the most recent agent associated with a specific Slack thread.
1265
+ * No status filter returns the last agent that touched this thread regardless of task state.
1266
+ * This is intentional: follow-up messages should route to the same agent even after task completion.
1267
+ * Callers (e.g. assistant.ts) apply their own status checks (e.g. agent.status !== "offline").
1383
1268
  */
1384
1269
  export function getAgentWorkingOnThread(channelId: string, threadTs: string): Agent | null {
1385
- // First check tasks (for workers)
1386
1270
  const taskRow = getDb()
1387
1271
  .prepare<AgentTaskRow, [string, string]>(
1388
1272
  `SELECT * FROM agent_tasks
1389
1273
  WHERE source = 'slack'
1390
1274
  AND slackChannelId = ?
1391
1275
  AND slackThreadTs = ?
1392
- AND status IN ('in_progress', 'pending')
1393
1276
  ORDER BY createdAt DESC
1394
1277
  LIMIT 1`,
1395
1278
  )
@@ -1397,21 +1280,45 @@ export function getAgentWorkingOnThread(channelId: string, threadTs: string): Ag
1397
1280
 
1398
1281
  if (taskRow?.agentId) return getAgentById(taskRow.agentId);
1399
1282
 
1400
- // Then check inbox_messages (for leads)
1401
- const inboxRow = getDb()
1402
- .prepare<{ agentId: string }, [string, string]>(
1403
- `SELECT agentId FROM inbox_messages
1283
+ return null;
1284
+ }
1285
+
1286
+ /**
1287
+ * Find the latest active (in_progress or pending) task in a specific Slack thread.
1288
+ * Used for dependency chaining in additive Slack buffer.
1289
+ */
1290
+ export function getLatestActiveTaskInThread(channelId: string, threadTs: string): AgentTask | null {
1291
+ const row = getDb()
1292
+ .prepare<AgentTaskRow, [string, string]>(
1293
+ `SELECT * FROM agent_tasks
1404
1294
  WHERE source = 'slack'
1405
1295
  AND slackChannelId = ?
1406
1296
  AND slackThreadTs = ?
1407
- ORDER BY createdAt DESC
1297
+ AND status IN ('in_progress', 'pending')
1298
+ ORDER BY createdAt DESC, rowid DESC
1408
1299
  LIMIT 1`,
1409
1300
  )
1410
1301
  .get(channelId, threadTs);
1411
1302
 
1412
- if (inboxRow?.agentId) return getAgentById(inboxRow.agentId);
1303
+ return row ? rowToAgentTask(row) : null;
1304
+ }
1413
1305
 
1414
- return null;
1306
+ /**
1307
+ * Find the most recent task in a Slack thread, regardless of source or status.
1308
+ * Unlike getAgentWorkingOnThread (which filters source='slack'), this finds ALL tasks
1309
+ * including worker tasks that inherited Slack metadata via parentTaskId.
1310
+ */
1311
+ export function getMostRecentTaskInThread(channelId: string, threadTs: string): AgentTask | null {
1312
+ const row = getDb()
1313
+ .prepare<AgentTaskRow, [string, string]>(
1314
+ `SELECT * FROM agent_tasks
1315
+ WHERE slackChannelId = ?
1316
+ AND slackThreadTs = ?
1317
+ ORDER BY createdAt DESC
1318
+ LIMIT 1`,
1319
+ )
1320
+ .get(channelId, threadTs);
1321
+ return row ? rowToAgentTask(row) : null;
1415
1322
  }
1416
1323
 
1417
1324
  export function completeTask(id: string, output?: string): AgentTask | null {
@@ -1434,6 +1341,17 @@ export function completeTask(id: string, output?: string): AgentTask | null {
1434
1341
  newValue: "completed",
1435
1342
  });
1436
1343
  } catch {}
1344
+ try {
1345
+ import("../workflows/event-bus").then(({ workflowEventBus }) => {
1346
+ workflowEventBus.emit("task.completed", {
1347
+ taskId: id,
1348
+ output,
1349
+ agentId: row.agentId,
1350
+ workflowRunId: row.workflowRunId,
1351
+ workflowRunStepId: row.workflowRunStepId,
1352
+ });
1353
+ });
1354
+ } catch {}
1437
1355
  }
1438
1356
 
1439
1357
  return row ? rowToAgentTask(row) : null;
@@ -1454,6 +1372,17 @@ export function failTask(id: string, reason: string): AgentTask | null {
1454
1372
  metadata: { reason },
1455
1373
  });
1456
1374
  } catch {}
1375
+ try {
1376
+ import("../workflows/event-bus").then(({ workflowEventBus }) => {
1377
+ workflowEventBus.emit("task.failed", {
1378
+ taskId: id,
1379
+ failureReason: reason,
1380
+ agentId: row.agentId,
1381
+ workflowRunId: row.workflowRunId,
1382
+ workflowRunStepId: row.workflowRunStepId,
1383
+ });
1384
+ });
1385
+ } catch {}
1457
1386
  }
1458
1387
  return row ? rowToAgentTask(row) : null;
1459
1388
  }
@@ -1462,8 +1391,9 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
1462
1391
  const oldTask = getTaskById(id);
1463
1392
  if (!oldTask) return null;
1464
1393
 
1465
- // Only cancel tasks that are in progress or pending
1466
- if (!["pending", "in_progress"].includes(oldTask.status)) {
1394
+ // Only cancel tasks that are not already in a terminal state
1395
+ const terminalStatuses = ["completed", "failed", "cancelled"];
1396
+ if (terminalStatuses.includes(oldTask.status)) {
1467
1397
  return null;
1468
1398
  }
1469
1399
 
@@ -1482,6 +1412,16 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
1482
1412
  metadata: reason ? { reason } : undefined,
1483
1413
  });
1484
1414
  } catch {}
1415
+ try {
1416
+ import("../workflows/event-bus").then(({ workflowEventBus }) => {
1417
+ workflowEventBus.emit("task.cancelled", {
1418
+ taskId: id,
1419
+ agentId: row.agentId,
1420
+ workflowRunId: row.workflowRunId,
1421
+ workflowRunStepId: row.workflowRunStepId,
1422
+ });
1423
+ });
1424
+ } catch {}
1485
1425
  }
1486
1426
 
1487
1427
  return row ? rowToAgentTask(row) : null;
@@ -1612,6 +1552,15 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
1612
1552
  newValue: progress,
1613
1553
  });
1614
1554
  } catch {}
1555
+ try {
1556
+ import("../workflows/event-bus").then(({ workflowEventBus }) => {
1557
+ workflowEventBus.emit("task.progress", {
1558
+ taskId: id,
1559
+ progress,
1560
+ agentId: row.agentId,
1561
+ });
1562
+ });
1563
+ } catch {}
1615
1564
  }
1616
1565
  return row ? rowToAgentTask(row) : null;
1617
1566
  }
@@ -1770,15 +1719,62 @@ export interface CreateTaskOptions {
1770
1719
  slackChannelId?: string;
1771
1720
  slackThreadTs?: string;
1772
1721
  slackUserId?: string;
1773
- githubRepo?: string;
1774
- githubEventType?: string;
1775
- githubNumber?: number;
1776
- githubCommentId?: number;
1777
- githubAuthor?: string;
1778
- githubUrl?: string;
1722
+ vcsProvider?: "github" | "gitlab";
1723
+ vcsRepo?: string;
1724
+ vcsEventType?: string;
1725
+ vcsNumber?: number;
1726
+ vcsCommentId?: number;
1727
+ vcsAuthor?: string;
1728
+ vcsUrl?: string;
1729
+ agentmailInboxId?: string;
1730
+ agentmailMessageId?: string;
1731
+ agentmailThreadId?: string;
1779
1732
  mentionMessageId?: string;
1780
1733
  mentionChannelId?: string;
1781
1734
  epicId?: string;
1735
+ dir?: string;
1736
+ parentTaskId?: string;
1737
+ model?: string;
1738
+ scheduleId?: string;
1739
+ workflowRunId?: string;
1740
+ workflowRunStepId?: string;
1741
+ sourceTaskId?: string;
1742
+ outputSchema?: Record<string, unknown>;
1743
+ }
1744
+
1745
+ /**
1746
+ * Find recent tasks within a time window for deduplication checks.
1747
+ * Returns tasks created in the last N minutes, optionally filtered by creator or target agent.
1748
+ */
1749
+ export function findRecentSimilarTasks(opts: {
1750
+ windowMinutes?: number;
1751
+ creatorAgentId?: string;
1752
+ agentId?: string;
1753
+ limit?: number;
1754
+ }): AgentTask[] {
1755
+ const since = new Date(Date.now() - (opts.windowMinutes ?? 10) * 60 * 1000).toISOString();
1756
+ const conditions: string[] = ["createdAt > ?"];
1757
+ const params: (string | number)[] = [since];
1758
+
1759
+ // Exclude completed/failed/cancelled tasks — only active or recently created
1760
+ conditions.push("status NOT IN ('completed', 'failed', 'cancelled')");
1761
+
1762
+ if (opts.creatorAgentId) {
1763
+ conditions.push("creatorAgentId = ?");
1764
+ params.push(opts.creatorAgentId);
1765
+ }
1766
+ if (opts.agentId) {
1767
+ conditions.push("agentId = ?");
1768
+ params.push(opts.agentId);
1769
+ }
1770
+
1771
+ const limit = opts.limit ?? 50;
1772
+ const query = `SELECT * FROM agent_tasks WHERE ${conditions.join(" AND ")} ORDER BY createdAt DESC LIMIT ${limit}`;
1773
+
1774
+ return getDb()
1775
+ .prepare<AgentTaskRow, (string | number)[]>(query)
1776
+ .all(...params)
1777
+ .map(rowToAgentTask);
1782
1778
  }
1783
1779
 
1784
1780
  export function createTaskExtended(task: string, options?: CreateTaskOptions): AgentTask {
@@ -1792,15 +1788,51 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
1792
1788
  ? "backlog"
1793
1789
  : "unassigned";
1794
1790
 
1791
+ // Inherit Slack/AgentMail metadata from parent task (unless explicitly overridden)
1792
+ if (options?.parentTaskId) {
1793
+ const parent = getTaskById(options.parentTaskId);
1794
+ if (parent) {
1795
+ if (parent.slackChannelId && !options.slackChannelId) {
1796
+ options.slackChannelId = parent.slackChannelId;
1797
+ }
1798
+ if (parent.slackThreadTs && !options.slackThreadTs) {
1799
+ options.slackThreadTs = parent.slackThreadTs;
1800
+ }
1801
+ if (parent.slackUserId && !options.slackUserId) {
1802
+ options.slackUserId = parent.slackUserId;
1803
+ }
1804
+ if (parent.agentmailInboxId && !options.agentmailInboxId) {
1805
+ options.agentmailInboxId = parent.agentmailInboxId;
1806
+ }
1807
+ if (parent.agentmailThreadId && !options.agentmailThreadId) {
1808
+ options.agentmailThreadId = parent.agentmailThreadId;
1809
+ }
1810
+ }
1811
+ }
1812
+
1813
+ // Auto-inherit Slack metadata from the creator's source task (deterministic via sourceTaskId)
1814
+ // Priority: explicit params > parentTaskId inheritance > sourceTaskId lookup
1815
+ // sourceTaskId is set by the adapter's X-Source-Task-Id header — each adapter carries its taskId natively
1816
+ if (options?.creatorAgentId && !options.slackChannelId && options.sourceTaskId) {
1817
+ const sourceTask = getTaskById(options.sourceTaskId);
1818
+ if (sourceTask?.slackChannelId) {
1819
+ options.slackChannelId = sourceTask.slackChannelId;
1820
+ options.slackThreadTs = sourceTask.slackThreadTs;
1821
+ options.slackUserId = sourceTask.slackUserId;
1822
+ }
1823
+ }
1824
+
1795
1825
  const row = getDb()
1796
1826
  .prepare<AgentTaskRow, (string | number | null)[]>(
1797
1827
  `INSERT INTO agent_tasks (
1798
1828
  id, agentId, creatorAgentId, task, status, source,
1799
1829
  taskType, tags, priority, dependsOn, offeredTo, offeredAt,
1800
1830
  slackChannelId, slackThreadTs, slackUserId,
1801
- githubRepo, githubEventType, githubNumber, githubCommentId, githubAuthor, githubUrl,
1802
- mentionMessageId, mentionChannelId, epicId, createdAt, lastUpdatedAt
1803
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
1831
+ vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
1832
+ agentmailInboxId, agentmailMessageId, agentmailThreadId,
1833
+ mentionMessageId, mentionChannelId, epicId, dir, parentTaskId, model, scheduleId,
1834
+ workflowRunId, workflowRunStepId, outputSchema, createdAt, lastUpdatedAt
1835
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
1804
1836
  )
1805
1837
  .get(
1806
1838
  id,
@@ -1818,15 +1850,26 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
1818
1850
  options?.slackChannelId ?? null,
1819
1851
  options?.slackThreadTs ?? null,
1820
1852
  options?.slackUserId ?? null,
1821
- options?.githubRepo ?? null,
1822
- options?.githubEventType ?? null,
1823
- options?.githubNumber ?? null,
1824
- options?.githubCommentId ?? null,
1825
- options?.githubAuthor ?? null,
1826
- options?.githubUrl ?? null,
1853
+ options?.vcsProvider ?? null,
1854
+ options?.vcsRepo ?? null,
1855
+ options?.vcsEventType ?? null,
1856
+ options?.vcsNumber ?? null,
1857
+ options?.vcsCommentId ?? null,
1858
+ options?.vcsAuthor ?? null,
1859
+ options?.vcsUrl ?? null,
1860
+ options?.agentmailInboxId ?? null,
1861
+ options?.agentmailMessageId ?? null,
1862
+ options?.agentmailThreadId ?? null,
1827
1863
  options?.mentionMessageId ?? null,
1828
1864
  options?.mentionChannelId ?? null,
1829
1865
  options?.epicId ?? null,
1866
+ options?.dir ?? null,
1867
+ options?.parentTaskId ?? null,
1868
+ options?.model ?? null,
1869
+ options?.scheduleId ?? null,
1870
+ options?.workflowRunId ?? null,
1871
+ options?.workflowRunStepId ?? null,
1872
+ options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
1830
1873
  now,
1831
1874
  now,
1832
1875
  );
@@ -1843,18 +1886,32 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
1843
1886
  });
1844
1887
  } catch {}
1845
1888
 
1889
+ try {
1890
+ import("../workflows/event-bus").then(({ workflowEventBus }) => {
1891
+ workflowEventBus.emit("task.created", {
1892
+ taskId: row.id,
1893
+ task: row.task,
1894
+ source: row.source,
1895
+ tags: options?.tags ?? [],
1896
+ agentId: row.agentId,
1897
+ workflowRunId: row.workflowRunId,
1898
+ workflowRunStepId: row.workflowRunStepId,
1899
+ });
1900
+ });
1901
+ } catch {}
1902
+
1846
1903
  return rowToAgentTask(row);
1847
1904
  }
1848
1905
 
1849
1906
  export function claimTask(taskId: string, agentId: string): AgentTask | null {
1850
- const task = getTaskById(taskId);
1851
- if (!task) return null;
1852
- if (task.status !== "unassigned") return null;
1853
-
1907
+ // Atomic claim: single UPDATE with WHERE guard ensures exactly-once claiming.
1908
+ // No pre-read needed — the WHERE clause handles the race condition.
1909
+ // Status goes directly to 'in_progress' because the claiming session is
1910
+ // already working on the task (prevents duplicate task_assigned triggers).
1854
1911
  const now = new Date().toISOString();
1855
1912
  const row = getDb()
1856
1913
  .prepare<AgentTaskRow, [string, string, string]>(
1857
- `UPDATE agent_tasks SET agentId = ?, status = 'pending', lastUpdatedAt = ?
1914
+ `UPDATE agent_tasks SET agentId = ?, status = 'in_progress', lastUpdatedAt = ?
1858
1915
  WHERE id = ? AND status = 'unassigned' RETURNING *`,
1859
1916
  )
1860
1917
  .get(agentId, now, taskId);
@@ -1866,7 +1923,7 @@ export function claimTask(taskId: string, agentId: string): AgentTask | null {
1866
1923
  agentId,
1867
1924
  taskId,
1868
1925
  oldValue: "unassigned",
1869
- newValue: "pending",
1926
+ newValue: "in_progress",
1870
1927
  });
1871
1928
  } catch {}
1872
1929
  }
@@ -1877,13 +1934,14 @@ export function claimTask(taskId: string, agentId: string): AgentTask | null {
1877
1934
  export function releaseTask(taskId: string): AgentTask | null {
1878
1935
  const task = getTaskById(taskId);
1879
1936
  if (!task) return null;
1880
- if (task.status !== "pending") return null;
1937
+ // Allow releasing both 'pending' (directly assigned) and 'in_progress' (pool-claimed) tasks
1938
+ if (task.status !== "pending" && task.status !== "in_progress") return null;
1881
1939
 
1882
1940
  const now = new Date().toISOString();
1883
1941
  const row = getDb()
1884
1942
  .prepare<AgentTaskRow, [string, string]>(
1885
1943
  `UPDATE agent_tasks SET agentId = NULL, status = 'unassigned', lastUpdatedAt = ?
1886
- WHERE id = ? AND status = 'pending' RETURNING *`,
1944
+ WHERE id = ? AND status IN ('pending', 'in_progress') RETURNING *`,
1887
1945
  )
1888
1946
  .get(now, taskId);
1889
1947
 
@@ -1893,7 +1951,7 @@ export function releaseTask(taskId: string): AgentTask | null {
1893
1951
  eventType: "task_released",
1894
1952
  agentId: task.agentId ?? undefined,
1895
1953
  taskId,
1896
- oldValue: "pending",
1954
+ oldValue: task.status,
1897
1955
  newValue: "unassigned",
1898
1956
  });
1899
1957
  } catch {}
@@ -2123,50 +2181,14 @@ export function checkDependencies(taskId: string): {
2123
2181
  // Agent Profile Operations
2124
2182
  // ============================================================================
2125
2183
 
2126
- /**
2127
- * Generate default CLAUDE.md content for a new agent
2128
- */
2129
- export function generateDefaultClaudeMd(agent: {
2130
- name: string;
2131
- description?: string;
2132
- role?: string;
2133
- capabilities?: string[];
2134
- }): string {
2135
- const lines = [`# Agent: ${agent.name}`, ""];
2136
-
2137
- if (agent.description) {
2138
- lines.push(agent.description, "");
2139
- }
2140
-
2141
- if (agent.role) {
2142
- lines.push("## Role", agent.role, "");
2143
- }
2144
-
2145
- if (agent.capabilities && agent.capabilities.length > 0) {
2146
- lines.push("## Capabilities");
2147
- for (const cap of agent.capabilities) {
2148
- lines.push(`- ${cap}`);
2149
- }
2150
- lines.push("");
2151
- }
2152
-
2153
- lines.push(
2154
- "---",
2155
- "",
2156
- "## Notes",
2157
- "",
2158
- "If you need to remember something, write it down here. This section persists across sessions.",
2159
- "",
2160
- "### Learnings",
2161
- "",
2162
- "### Preferences",
2163
- "",
2164
- "### Important Context",
2165
- "",
2166
- );
2167
-
2168
- return lines.join("\n");
2169
- }
2184
+ // Default markdown template generators moved to src/prompts/defaults.ts
2185
+ // Re-export for backwards compatibility with any external consumers
2186
+ export {
2187
+ generateDefaultClaudeMd,
2188
+ generateDefaultIdentityMd,
2189
+ generateDefaultSoulMd,
2190
+ generateDefaultToolsMd,
2191
+ } from "../prompts/defaults.ts";
2170
2192
 
2171
2193
  export function updateAgentProfile(
2172
2194
  id: string,
@@ -2175,35 +2197,94 @@ export function updateAgentProfile(
2175
2197
  role?: string;
2176
2198
  capabilities?: string[];
2177
2199
  claudeMd?: string;
2200
+ soulMd?: string;
2201
+ identityMd?: string;
2202
+ setupScript?: string;
2203
+ toolsMd?: string;
2178
2204
  },
2205
+ meta?: VersionMeta,
2179
2206
  ): Agent | null {
2180
- const agent = getAgentById(id);
2181
- if (!agent) return null;
2207
+ const database = getDb();
2182
2208
 
2183
- const now = new Date().toISOString();
2184
- const row = getDb()
2185
- .prepare<
2186
- AgentRow,
2187
- [string | null, string | null, string | null, string | null, string, string]
2188
- >(
2189
- `UPDATE agents SET
2190
- description = COALESCE(?, description),
2191
- role = COALESCE(?, role),
2192
- capabilities = COALESCE(?, capabilities),
2193
- claudeMd = COALESCE(?, claudeMd),
2194
- lastUpdatedAt = ?
2195
- WHERE id = ? RETURNING *`,
2196
- )
2197
- .get(
2198
- updates.description ?? null,
2199
- updates.role ?? null,
2200
- updates.capabilities ? JSON.stringify(updates.capabilities) : null,
2201
- updates.claudeMd ?? null,
2202
- now,
2203
- id,
2204
- );
2209
+ return database.transaction(() => {
2210
+ // Get current agent state for version comparison
2211
+ const current = database
2212
+ .prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?")
2213
+ .get(id);
2214
+ if (!current) return null;
2205
2215
 
2206
- return row ? rowToAgent(row) : null;
2216
+ // Create context versions for changed fields
2217
+ for (const field of VERSIONABLE_FIELDS) {
2218
+ const newValue = updates[field];
2219
+ if (newValue === undefined || newValue === null) continue;
2220
+
2221
+ const currentValue = current[field] ?? "";
2222
+ const newHash = computeContentHash(newValue);
2223
+ const currentHash = computeContentHash(currentValue);
2224
+
2225
+ if (newHash === currentHash) continue; // No actual change
2226
+
2227
+ const latestVersion = getLatestContextVersion(id, field);
2228
+ const version = (latestVersion?.version ?? 0) + 1;
2229
+
2230
+ createContextVersion({
2231
+ agentId: id,
2232
+ field,
2233
+ content: newValue,
2234
+ version,
2235
+ changeSource: meta?.changeSource ?? "api",
2236
+ changedByAgentId: meta?.changedByAgentId ?? null,
2237
+ changeReason: meta?.changeReason ?? null,
2238
+ contentHash: newHash,
2239
+ previousVersionId: latestVersion?.id ?? null,
2240
+ });
2241
+ }
2242
+
2243
+ // Proceed with existing UPDATE logic
2244
+ const now = new Date().toISOString();
2245
+ const row = database
2246
+ .prepare<
2247
+ AgentRow,
2248
+ [
2249
+ string | null,
2250
+ string | null,
2251
+ string | null,
2252
+ string | null,
2253
+ string | null,
2254
+ string | null,
2255
+ string | null,
2256
+ string | null,
2257
+ string,
2258
+ string,
2259
+ ]
2260
+ >(
2261
+ `UPDATE agents SET
2262
+ description = COALESCE(?, description),
2263
+ role = COALESCE(?, role),
2264
+ capabilities = COALESCE(?, capabilities),
2265
+ claudeMd = COALESCE(?, claudeMd),
2266
+ soulMd = COALESCE(?, soulMd),
2267
+ identityMd = COALESCE(?, identityMd),
2268
+ setupScript = COALESCE(?, setupScript),
2269
+ toolsMd = COALESCE(?, toolsMd),
2270
+ lastUpdatedAt = ?
2271
+ WHERE id = ? RETURNING *`,
2272
+ )
2273
+ .get(
2274
+ updates.description ?? null,
2275
+ updates.role ?? null,
2276
+ updates.capabilities ? JSON.stringify(updates.capabilities) : null,
2277
+ updates.claudeMd ?? null,
2278
+ updates.soulMd ?? null,
2279
+ updates.identityMd ?? null,
2280
+ updates.setupScript ?? null,
2281
+ updates.toolsMd ?? null,
2282
+ now,
2283
+ id,
2284
+ );
2285
+
2286
+ return row ? rowToAgent(row) : null;
2287
+ })();
2207
2288
  }
2208
2289
 
2209
2290
  export function updateAgentName(id: string, newName: string): Agent | null {
@@ -2337,6 +2418,11 @@ export function getAllChannels(): Channel[] {
2337
2418
  .map(rowToChannel);
2338
2419
  }
2339
2420
 
2421
+ export function deleteChannel(id: string): boolean {
2422
+ const result = getDb().prepare("DELETE FROM channels WHERE id = ?").run(id);
2423
+ return result.changes > 0;
2424
+ }
2425
+
2340
2426
  export function postMessage(
2341
2427
  channelId: string,
2342
2428
  agentId: string | null,
@@ -3183,8 +3269,8 @@ const sessionCostQueries = {
3183
3269
  ),
3184
3270
 
3185
3271
  getByTaskId: () =>
3186
- getDb().prepare<SessionCostRow, [string]>(
3187
- "SELECT * FROM session_costs WHERE taskId = ? ORDER BY createdAt DESC",
3272
+ getDb().prepare<SessionCostRow, [string, number]>(
3273
+ "SELECT * FROM session_costs WHERE taskId = ? ORDER BY createdAt DESC LIMIT ?",
3188
3274
  ),
3189
3275
 
3190
3276
  getByAgentId: () =>
@@ -3251,8 +3337,8 @@ export function createSessionCost(input: CreateSessionCostInput): SessionCost {
3251
3337
  };
3252
3338
  }
3253
3339
 
3254
- export function getSessionCostsByTaskId(taskId: string): SessionCost[] {
3255
- return sessionCostQueries.getByTaskId().all(taskId).map(rowToSessionCost);
3340
+ export function getSessionCostsByTaskId(taskId: string, limit = 500): SessionCost[] {
3341
+ return sessionCostQueries.getByTaskId().all(taskId, limit).map(rowToSessionCost);
3256
3342
  }
3257
3343
 
3258
3344
  export function getSessionCostsByAgentId(agentId: string, limit = 100): SessionCost[] {
@@ -3263,6 +3349,224 @@ export function getAllSessionCosts(limit = 100): SessionCost[] {
3263
3349
  return sessionCostQueries.getAll().all(limit).map(rowToSessionCost);
3264
3350
  }
3265
3351
 
3352
+ // --- Date-filtered session costs (P1) ---
3353
+
3354
+ export function getSessionCostsFiltered(opts: {
3355
+ agentId?: string;
3356
+ startDate?: string;
3357
+ endDate?: string;
3358
+ limit?: number;
3359
+ }): SessionCost[] {
3360
+ const conditions: string[] = [];
3361
+ const params: (string | number)[] = [];
3362
+
3363
+ if (opts.agentId) {
3364
+ conditions.push("agentId = ?");
3365
+ params.push(opts.agentId);
3366
+ }
3367
+ if (opts.startDate) {
3368
+ conditions.push("createdAt >= ?");
3369
+ params.push(opts.startDate);
3370
+ }
3371
+ if (opts.endDate) {
3372
+ conditions.push("createdAt <= ?");
3373
+ params.push(opts.endDate);
3374
+ }
3375
+
3376
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3377
+ const limit = opts.limit ?? 100;
3378
+ params.push(limit);
3379
+
3380
+ return getDb()
3381
+ .prepare<SessionCostRow, (string | number)[]>(
3382
+ `SELECT * FROM session_costs ${where} ORDER BY createdAt DESC LIMIT ?`,
3383
+ )
3384
+ .all(...params)
3385
+ .map(rowToSessionCost);
3386
+ }
3387
+
3388
+ // --- Aggregation queries (P0) ---
3389
+
3390
+ export interface SessionCostSummaryTotals {
3391
+ totalCostUsd: number;
3392
+ totalInputTokens: number;
3393
+ totalOutputTokens: number;
3394
+ totalCacheReadTokens: number;
3395
+ totalCacheWriteTokens: number;
3396
+ totalDurationMs: number;
3397
+ totalSessions: number;
3398
+ avgCostPerSession: number;
3399
+ }
3400
+
3401
+ export interface SessionCostDailyRow {
3402
+ date: string;
3403
+ costUsd: number;
3404
+ inputTokens: number;
3405
+ outputTokens: number;
3406
+ sessions: number;
3407
+ }
3408
+
3409
+ export interface SessionCostByAgentRow {
3410
+ agentId: string;
3411
+ costUsd: number;
3412
+ inputTokens: number;
3413
+ outputTokens: number;
3414
+ sessions: number;
3415
+ durationMs: number;
3416
+ }
3417
+
3418
+ export function getSessionCostSummary(opts: {
3419
+ startDate?: string;
3420
+ endDate?: string;
3421
+ agentId?: string;
3422
+ groupBy?: "day" | "agent" | "both";
3423
+ }): {
3424
+ totals: SessionCostSummaryTotals;
3425
+ daily: SessionCostDailyRow[];
3426
+ byAgent: SessionCostByAgentRow[];
3427
+ } {
3428
+ const conditions: string[] = [];
3429
+ const params: string[] = [];
3430
+
3431
+ if (opts.startDate) {
3432
+ conditions.push("createdAt >= ?");
3433
+ params.push(opts.startDate);
3434
+ }
3435
+ if (opts.endDate) {
3436
+ conditions.push("createdAt <= ?");
3437
+ params.push(opts.endDate);
3438
+ }
3439
+ if (opts.agentId) {
3440
+ conditions.push("agentId = ?");
3441
+ params.push(opts.agentId);
3442
+ }
3443
+
3444
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3445
+
3446
+ // Totals
3447
+ type TotalsRow = {
3448
+ totalCostUsd: number;
3449
+ totalInputTokens: number;
3450
+ totalOutputTokens: number;
3451
+ totalCacheReadTokens: number;
3452
+ totalCacheWriteTokens: number;
3453
+ totalDurationMs: number;
3454
+ totalSessions: number;
3455
+ };
3456
+
3457
+ const totalsRow = getDb()
3458
+ .prepare<TotalsRow, string[]>(
3459
+ `SELECT
3460
+ COALESCE(SUM(totalCostUsd), 0) as totalCostUsd,
3461
+ COALESCE(SUM(inputTokens), 0) as totalInputTokens,
3462
+ COALESCE(SUM(outputTokens), 0) as totalOutputTokens,
3463
+ COALESCE(SUM(cacheReadTokens), 0) as totalCacheReadTokens,
3464
+ COALESCE(SUM(cacheWriteTokens), 0) as totalCacheWriteTokens,
3465
+ COALESCE(SUM(durationMs), 0) as totalDurationMs,
3466
+ COUNT(*) as totalSessions
3467
+ FROM session_costs ${where}`,
3468
+ )
3469
+ .get(...params);
3470
+
3471
+ const totals: SessionCostSummaryTotals = totalsRow
3472
+ ? {
3473
+ ...totalsRow,
3474
+ avgCostPerSession:
3475
+ totalsRow.totalSessions > 0 ? totalsRow.totalCostUsd / totalsRow.totalSessions : 0,
3476
+ }
3477
+ : {
3478
+ totalCostUsd: 0,
3479
+ totalInputTokens: 0,
3480
+ totalOutputTokens: 0,
3481
+ totalCacheReadTokens: 0,
3482
+ totalCacheWriteTokens: 0,
3483
+ totalDurationMs: 0,
3484
+ totalSessions: 0,
3485
+ avgCostPerSession: 0,
3486
+ };
3487
+
3488
+ // Daily breakdown
3489
+ const groupBy = opts.groupBy ?? "both";
3490
+ let daily: SessionCostDailyRow[] = [];
3491
+ if (groupBy === "day" || groupBy === "both") {
3492
+ daily = getDb()
3493
+ .prepare<
3494
+ {
3495
+ date: string;
3496
+ costUsd: number;
3497
+ inputTokens: number;
3498
+ outputTokens: number;
3499
+ sessions: number;
3500
+ },
3501
+ string[]
3502
+ >(
3503
+ `SELECT
3504
+ DATE(createdAt) as date,
3505
+ COALESCE(SUM(totalCostUsd), 0) as costUsd,
3506
+ COALESCE(SUM(inputTokens), 0) as inputTokens,
3507
+ COALESCE(SUM(outputTokens), 0) as outputTokens,
3508
+ COUNT(*) as sessions
3509
+ FROM session_costs ${where}
3510
+ GROUP BY DATE(createdAt)
3511
+ ORDER BY date ASC`,
3512
+ )
3513
+ .all(...params);
3514
+ }
3515
+
3516
+ // Per-agent breakdown
3517
+ let byAgent: SessionCostByAgentRow[] = [];
3518
+ if (groupBy === "agent" || groupBy === "both") {
3519
+ byAgent = getDb()
3520
+ .prepare<
3521
+ {
3522
+ agentId: string;
3523
+ costUsd: number;
3524
+ inputTokens: number;
3525
+ outputTokens: number;
3526
+ sessions: number;
3527
+ durationMs: number;
3528
+ },
3529
+ string[]
3530
+ >(
3531
+ `SELECT
3532
+ agentId,
3533
+ COALESCE(SUM(totalCostUsd), 0) as costUsd,
3534
+ COALESCE(SUM(inputTokens), 0) as inputTokens,
3535
+ COALESCE(SUM(outputTokens), 0) as outputTokens,
3536
+ COUNT(*) as sessions,
3537
+ COALESCE(SUM(durationMs), 0) as durationMs
3538
+ FROM session_costs ${where}
3539
+ GROUP BY agentId
3540
+ ORDER BY costUsd DESC`,
3541
+ )
3542
+ .all(...params);
3543
+ }
3544
+
3545
+ return { totals, daily, byAgent };
3546
+ }
3547
+
3548
+ // --- Dashboard cost summary (P4) ---
3549
+
3550
+ export interface DashboardCostSummary {
3551
+ costToday: number;
3552
+ costMtd: number;
3553
+ }
3554
+
3555
+ export function getDashboardCostSummary(): DashboardCostSummary {
3556
+ type CostRow = { costToday: number; costMtd: number };
3557
+ const row = getDb()
3558
+ .prepare<CostRow, []>(
3559
+ `SELECT
3560
+ COALESCE(SUM(CASE WHEN createdAt >= date('now') THEN totalCostUsd ELSE 0 END), 0) as costToday,
3561
+ COALESCE(SUM(totalCostUsd), 0) as costMtd
3562
+ FROM session_costs
3563
+ WHERE createdAt >= date('now', 'start of month')`,
3564
+ )
3565
+ .get();
3566
+
3567
+ return row ?? { costToday: 0, costMtd: 0 };
3568
+ }
3569
+
3266
3570
  // ============================================================================
3267
3571
  // Inbox Message Operations
3268
3572
  // ============================================================================
@@ -3302,7 +3606,7 @@ function rowToInboxMessage(row: InboxMessageRow): InboxMessage {
3302
3606
  }
3303
3607
 
3304
3608
  export interface CreateInboxMessageOptions {
3305
- source?: "slack";
3609
+ source?: "slack" | "agentmail";
3306
3610
  slackChannelId?: string;
3307
3611
  slackThreadTs?: string;
3308
3612
  slackUserId?: string;
@@ -3435,6 +3739,115 @@ export function releaseStaleProcessingInbox(timeoutMinutes: number = 30): number
3435
3739
  return result.changes;
3436
3740
  }
3437
3741
 
3742
+ // ============================================================================
3743
+ // Concurrent Context (for lead session awareness)
3744
+ // ============================================================================
3745
+
3746
+ export interface ConcurrentContext {
3747
+ processingInboxMessages: Array<{
3748
+ id: string;
3749
+ content: string;
3750
+ source: string;
3751
+ slackChannelId: string | null;
3752
+ slackThreadTs: string | null;
3753
+ createdAt: string;
3754
+ }>;
3755
+ recentTaskDelegations: Array<{
3756
+ id: string;
3757
+ task: string;
3758
+ agentId: string | null;
3759
+ agentName: string | null;
3760
+ creatorAgentId: string | null;
3761
+ status: string;
3762
+ createdAt: string;
3763
+ }>;
3764
+ activeSwarmTasks: Array<{
3765
+ id: string;
3766
+ task: string;
3767
+ agentId: string | null;
3768
+ agentName: string | null;
3769
+ status: string;
3770
+ createdAt: string;
3771
+ progress: string | null;
3772
+ }>;
3773
+ }
3774
+
3775
+ /**
3776
+ * Get concurrent context for lead session awareness.
3777
+ * Returns processing inbox messages, recent task delegations by leads,
3778
+ * and currently active (in-progress) tasks across the swarm.
3779
+ */
3780
+ export function getConcurrentContext(): ConcurrentContext {
3781
+ // 1. Inbox messages currently being processed (status = 'processing')
3782
+ const processingInboxMessages = getDb()
3783
+ .prepare<
3784
+ {
3785
+ id: string;
3786
+ content: string;
3787
+ source: string;
3788
+ slackChannelId: string | null;
3789
+ slackThreadTs: string | null;
3790
+ createdAt: string;
3791
+ },
3792
+ []
3793
+ >(
3794
+ "SELECT id, content, source, slackChannelId, slackThreadTs, createdAt FROM inbox_messages WHERE status = 'processing' ORDER BY createdAt DESC",
3795
+ )
3796
+ .all();
3797
+
3798
+ // 2. Tasks created in the last 5 minutes by lead agents
3799
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
3800
+ const recentTaskDelegations = getDb()
3801
+ .prepare<
3802
+ {
3803
+ id: string;
3804
+ task: string;
3805
+ agentId: string | null;
3806
+ agentName: string | null;
3807
+ creatorAgentId: string | null;
3808
+ status: string;
3809
+ createdAt: string;
3810
+ },
3811
+ [string]
3812
+ >(
3813
+ `SELECT t.id, t.task, t.agentId, a.name as agentName, t.creatorAgentId, t.status, t.createdAt
3814
+ FROM agent_tasks t
3815
+ LEFT JOIN agents a ON t.agentId = a.id
3816
+ WHERE t.createdAt > ?
3817
+ AND t.creatorAgentId IN (SELECT id FROM agents WHERE isLead = 1)
3818
+ ORDER BY t.createdAt DESC`,
3819
+ )
3820
+ .all(fiveMinutesAgo);
3821
+
3822
+ // 3. Currently in-progress tasks across the swarm
3823
+ const activeSwarmTasks = getDb()
3824
+ .prepare<
3825
+ {
3826
+ id: string;
3827
+ task: string;
3828
+ agentId: string | null;
3829
+ agentName: string | null;
3830
+ status: string;
3831
+ createdAt: string;
3832
+ progress: string | null;
3833
+ },
3834
+ []
3835
+ >(
3836
+ `SELECT t.id, t.task, t.agentId, a.name as agentName, t.status, t.createdAt, t.progress
3837
+ FROM agent_tasks t
3838
+ LEFT JOIN agents a ON t.agentId = a.id
3839
+ WHERE t.status = 'in_progress'
3840
+ ORDER BY t.createdAt DESC`,
3841
+ )
3842
+ .all();
3843
+
3844
+ return {
3845
+ processingInboxMessages,
3846
+ recentTaskDelegations,
3847
+ activeSwarmTasks,
3848
+ };
3849
+ }
3850
+
3438
3851
  // ============================================================================
3439
3852
  // Scheduled Task Queries
3440
3853
  // ============================================================================
@@ -3455,6 +3868,11 @@ type ScheduledTaskRow = {
3455
3868
  nextRunAt: string | null;
3456
3869
  createdByAgentId: string | null;
3457
3870
  timezone: string;
3871
+ consecutiveErrors: number | null;
3872
+ lastErrorAt: string | null;
3873
+ lastErrorMessage: string | null;
3874
+ model: string | null;
3875
+ scheduleType: string;
3458
3876
  createdAt: string;
3459
3877
  lastUpdatedAt: string;
3460
3878
  };
@@ -3476,6 +3894,11 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
3476
3894
  nextRunAt: row.nextRunAt ?? undefined,
3477
3895
  createdByAgentId: row.createdByAgentId ?? undefined,
3478
3896
  timezone: row.timezone,
3897
+ consecutiveErrors: row.consecutiveErrors ?? 0,
3898
+ lastErrorAt: row.lastErrorAt ?? undefined,
3899
+ lastErrorMessage: row.lastErrorMessage ?? undefined,
3900
+ model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
3901
+ scheduleType: row.scheduleType as "recurring" | "one_time",
3479
3902
  createdAt: row.createdAt,
3480
3903
  lastUpdatedAt: row.lastUpdatedAt,
3481
3904
  };
@@ -3484,6 +3907,8 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
3484
3907
  export interface ScheduledTaskFilters {
3485
3908
  enabled?: boolean;
3486
3909
  name?: string;
3910
+ scheduleType?: "recurring" | "one_time";
3911
+ hideCompleted?: boolean;
3487
3912
  }
3488
3913
 
3489
3914
  export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask[] {
@@ -3500,6 +3925,15 @@ export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask
3500
3925
  params.push(`%${filters.name}%`);
3501
3926
  }
3502
3927
 
3928
+ if (filters?.scheduleType) {
3929
+ query += " AND scheduleType = ?";
3930
+ params.push(filters.scheduleType);
3931
+ }
3932
+
3933
+ if (filters?.hideCompleted !== false) {
3934
+ query += " AND NOT (scheduleType = 'one_time' AND enabled = 0)";
3935
+ }
3936
+
3503
3937
  query += " ORDER BY name ASC";
3504
3938
 
3505
3939
  return getDb()
@@ -3536,6 +3970,8 @@ export interface CreateScheduledTaskData {
3536
3970
  nextRunAt?: string;
3537
3971
  createdByAgentId?: string;
3538
3972
  timezone?: string;
3973
+ model?: string;
3974
+ scheduleType?: "recurring" | "one_time";
3539
3975
  }
3540
3976
 
3541
3977
  export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTask {
@@ -3547,8 +3983,8 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
3547
3983
  `INSERT INTO scheduled_tasks (
3548
3984
  id, name, description, cronExpression, intervalMs, taskTemplate,
3549
3985
  taskType, tags, priority, targetAgentId, enabled, nextRunAt,
3550
- createdByAgentId, timezone, createdAt, lastUpdatedAt
3551
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3986
+ createdByAgentId, timezone, model, scheduleType, createdAt, lastUpdatedAt
3987
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3552
3988
  )
3553
3989
  .get(
3554
3990
  id,
@@ -3565,6 +4001,8 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
3565
4001
  data.nextRunAt ?? null,
3566
4002
  data.createdByAgentId ?? null,
3567
4003
  data.timezone ?? "UTC",
4004
+ data.model ?? null,
4005
+ data.scheduleType ?? "recurring",
3568
4006
  now,
3569
4007
  now,
3570
4008
  );
@@ -3585,8 +4023,13 @@ export interface UpdateScheduledTaskData {
3585
4023
  targetAgentId?: string | null;
3586
4024
  enabled?: boolean;
3587
4025
  lastRunAt?: string;
3588
- nextRunAt?: string;
4026
+ nextRunAt?: string | null;
3589
4027
  timezone?: string;
4028
+ consecutiveErrors?: number;
4029
+ lastErrorAt?: string | null;
4030
+ lastErrorMessage?: string | null;
4031
+ model?: string | null;
4032
+ scheduleType?: "recurring" | "one_time";
3590
4033
  lastUpdatedAt?: string;
3591
4034
  }
3592
4035
 
@@ -3649,6 +4092,26 @@ export function updateScheduledTask(
3649
4092
  updates.push("timezone = ?");
3650
4093
  params.push(data.timezone);
3651
4094
  }
4095
+ if (data.consecutiveErrors !== undefined) {
4096
+ updates.push("consecutiveErrors = ?");
4097
+ params.push(data.consecutiveErrors);
4098
+ }
4099
+ if (data.lastErrorAt !== undefined) {
4100
+ updates.push("lastErrorAt = ?");
4101
+ params.push(data.lastErrorAt);
4102
+ }
4103
+ if (data.lastErrorMessage !== undefined) {
4104
+ updates.push("lastErrorMessage = ?");
4105
+ params.push(data.lastErrorMessage);
4106
+ }
4107
+ if (data.model !== undefined) {
4108
+ updates.push("model = ?");
4109
+ params.push(data.model);
4110
+ }
4111
+ if (data.scheduleType !== undefined) {
4112
+ updates.push("scheduleType = ?");
4113
+ params.push(data.scheduleType);
4114
+ }
3652
4115
 
3653
4116
  if (updates.length === 0) {
3654
4117
  return getScheduledTaskById(id);
@@ -3710,8 +4173,10 @@ type EpicRow = {
3710
4173
  planDocPath: string | null;
3711
4174
  slackChannelId: string | null;
3712
4175
  slackThreadTs: string | null;
3713
- githubRepo: string | null;
3714
- githubMilestone: string | null;
4176
+ vcsProvider: string | null;
4177
+ vcsRepo: string | null;
4178
+ vcsMilestone: string | null;
4179
+ nextSteps: string | null;
3715
4180
  createdAt: string;
3716
4181
  lastUpdatedAt: string;
3717
4182
  startedAt: string | null;
@@ -3737,8 +4202,10 @@ function rowToEpic(row: EpicRow): Epic {
3737
4202
  planDocPath: row.planDocPath ?? undefined,
3738
4203
  slackChannelId: row.slackChannelId ?? undefined,
3739
4204
  slackThreadTs: row.slackThreadTs ?? undefined,
3740
- githubRepo: row.githubRepo ?? undefined,
3741
- githubMilestone: row.githubMilestone ?? undefined,
4205
+ vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
4206
+ vcsRepo: row.vcsRepo ?? undefined,
4207
+ vcsMilestone: row.vcsMilestone ?? undefined,
4208
+ nextSteps: row.nextSteps ?? undefined,
3742
4209
  createdAt: row.createdAt,
3743
4210
  lastUpdatedAt: row.lastUpdatedAt,
3744
4211
  startedAt: row.startedAt ?? undefined,
@@ -3814,8 +4281,9 @@ export interface CreateEpicData {
3814
4281
  planDocPath?: string;
3815
4282
  slackChannelId?: string;
3816
4283
  slackThreadTs?: string;
3817
- githubRepo?: string;
3818
- githubMilestone?: string;
4284
+ vcsProvider?: "github" | "gitlab";
4285
+ vcsRepo?: string;
4286
+ vcsMilestone?: string;
3819
4287
  }
3820
4288
 
3821
4289
  export function createEpic(data: CreateEpicData): Epic {
@@ -3838,9 +4306,9 @@ export function createEpic(data: CreateEpicData): Epic {
3838
4306
  `INSERT INTO epics (
3839
4307
  id, name, description, goal, prd, plan, status, priority, tags,
3840
4308
  createdByAgentId, leadAgentId, channelId, researchDocPath, planDocPath,
3841
- slackChannelId, slackThreadTs, githubRepo, githubMilestone,
4309
+ slackChannelId, slackThreadTs, vcsProvider, vcsRepo, vcsMilestone,
3842
4310
  createdAt, lastUpdatedAt
3843
- ) VALUES (?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
4311
+ ) VALUES (?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3844
4312
  )
3845
4313
  .get(
3846
4314
  id,
@@ -3858,8 +4326,9 @@ export function createEpic(data: CreateEpicData): Epic {
3858
4326
  data.planDocPath ?? null,
3859
4327
  data.slackChannelId ?? null,
3860
4328
  data.slackThreadTs ?? null,
3861
- data.githubRepo ?? null,
3862
- data.githubMilestone ?? null,
4329
+ data.vcsProvider ?? null,
4330
+ data.vcsRepo ?? null,
4331
+ data.vcsMilestone ?? null,
3863
4332
  now,
3864
4333
  now,
3865
4334
  );
@@ -3898,8 +4367,10 @@ export interface UpdateEpicData {
3898
4367
  planDocPath?: string;
3899
4368
  slackChannelId?: string;
3900
4369
  slackThreadTs?: string;
3901
- githubRepo?: string;
3902
- githubMilestone?: string;
4370
+ vcsProvider?: "github" | "gitlab";
4371
+ vcsRepo?: string;
4372
+ vcsMilestone?: string;
4373
+ nextSteps?: string;
3903
4374
  }
3904
4375
 
3905
4376
  export function updateEpic(id: string, data: UpdateEpicData): Epic | null {
@@ -3973,13 +4444,21 @@ export function updateEpic(id: string, data: UpdateEpicData): Epic | null {
3973
4444
  updates.push("slackThreadTs = ?");
3974
4445
  params.push(data.slackThreadTs);
3975
4446
  }
3976
- if (data.githubRepo !== undefined) {
3977
- updates.push("githubRepo = ?");
3978
- params.push(data.githubRepo);
4447
+ if (data.vcsProvider !== undefined) {
4448
+ updates.push("vcsProvider = ?");
4449
+ params.push(data.vcsProvider);
4450
+ }
4451
+ if (data.vcsRepo !== undefined) {
4452
+ updates.push("vcsRepo = ?");
4453
+ params.push(data.vcsRepo);
3979
4454
  }
3980
- if (data.githubMilestone !== undefined) {
3981
- updates.push("githubMilestone = ?");
3982
- params.push(data.githubMilestone);
4455
+ if (data.vcsMilestone !== undefined) {
4456
+ updates.push("vcsMilestone = ?");
4457
+ params.push(data.vcsMilestone);
4458
+ }
4459
+ if (data.nextSteps !== undefined) {
4460
+ updates.push("nextSteps = ?");
4461
+ params.push(data.nextSteps);
3983
4462
  }
3984
4463
 
3985
4464
  params.push(id);
@@ -4179,3 +4658,1483 @@ export function markEpicsProgressNotified(epicIds: string[]): number {
4179
4658
 
4180
4659
  return result.changes;
4181
4660
  }
4661
+
4662
+ // ============================================================================
4663
+ // Swarm Config Operations (Centralized Environment/Config Management)
4664
+ // ============================================================================
4665
+
4666
+ type SwarmConfigRow = {
4667
+ id: string;
4668
+ scope: string;
4669
+ scopeId: string | null;
4670
+ key: string;
4671
+ value: string;
4672
+ isSecret: number; // SQLite boolean
4673
+ envPath: string | null;
4674
+ description: string | null;
4675
+ createdAt: string;
4676
+ lastUpdatedAt: string;
4677
+ };
4678
+
4679
+ function rowToSwarmConfig(row: SwarmConfigRow): SwarmConfig {
4680
+ return {
4681
+ id: row.id,
4682
+ scope: row.scope as "global" | "agent" | "repo",
4683
+ scopeId: row.scopeId ?? null,
4684
+ key: row.key,
4685
+ value: row.value,
4686
+ isSecret: row.isSecret === 1,
4687
+ envPath: row.envPath ?? null,
4688
+ description: row.description ?? null,
4689
+ createdAt: row.createdAt,
4690
+ lastUpdatedAt: row.lastUpdatedAt,
4691
+ };
4692
+ }
4693
+
4694
+ /**
4695
+ * Mask secret values in config entries for API responses.
4696
+ */
4697
+ export function maskSecrets(configs: SwarmConfig[]): SwarmConfig[] {
4698
+ return configs.map((c) => (c.isSecret ? { ...c, value: "********" } : c));
4699
+ }
4700
+
4701
+ /**
4702
+ * Write config values to .env files on disk when `envPath` is set.
4703
+ * Groups configs by envPath, reads existing file, updates/adds matching keys, writes back.
4704
+ */
4705
+ function writeEnvFile(configs: SwarmConfig[]): void {
4706
+ const { readFileSync, writeFileSync } = require("node:fs");
4707
+
4708
+ const byPath = new Map<string, SwarmConfig[]>();
4709
+ for (const config of configs) {
4710
+ if (!config.envPath) continue;
4711
+ const existing = byPath.get(config.envPath) ?? [];
4712
+ existing.push(config);
4713
+ byPath.set(config.envPath, existing);
4714
+ }
4715
+
4716
+ for (const [envPath, entries] of byPath) {
4717
+ let lines: string[] = [];
4718
+ try {
4719
+ const content = readFileSync(envPath, "utf-8") as string;
4720
+ lines = content.split("\n");
4721
+ } catch {
4722
+ // File doesn't exist yet, start empty
4723
+ }
4724
+
4725
+ for (const entry of entries) {
4726
+ const prefix = `${entry.key}=`;
4727
+ const lineIndex = lines.findIndex((l) => l.startsWith(prefix));
4728
+ const newLine = `${entry.key}=${entry.value}`;
4729
+ if (lineIndex >= 0) {
4730
+ lines[lineIndex] = newLine;
4731
+ } else {
4732
+ lines.push(newLine);
4733
+ }
4734
+ }
4735
+
4736
+ const output = `${lines.filter((l) => l !== "").join("\n")}\n`;
4737
+ writeFileSync(envPath, output, "utf-8");
4738
+ }
4739
+ }
4740
+
4741
+ /**
4742
+ * List config entries with optional filters.
4743
+ */
4744
+ export function getSwarmConfigs(filters?: {
4745
+ scope?: string;
4746
+ scopeId?: string;
4747
+ key?: string;
4748
+ }): SwarmConfig[] {
4749
+ const conditions: string[] = [];
4750
+ const params: string[] = [];
4751
+
4752
+ if (filters?.scope) {
4753
+ conditions.push("scope = ?");
4754
+ params.push(filters.scope);
4755
+ }
4756
+ if (filters?.scopeId) {
4757
+ conditions.push("scopeId = ?");
4758
+ params.push(filters.scopeId);
4759
+ }
4760
+ if (filters?.key) {
4761
+ conditions.push("key = ?");
4762
+ params.push(filters.key);
4763
+ }
4764
+
4765
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4766
+ const query = `SELECT * FROM swarm_config ${whereClause} ORDER BY key ASC`;
4767
+
4768
+ return getDb()
4769
+ .prepare<SwarmConfigRow, string[]>(query)
4770
+ .all(...params)
4771
+ .map(rowToSwarmConfig);
4772
+ }
4773
+
4774
+ /**
4775
+ * Get a single config entry by ID.
4776
+ */
4777
+ export function getSwarmConfigById(id: string): SwarmConfig | null {
4778
+ const row = getDb()
4779
+ .prepare<SwarmConfigRow, [string]>("SELECT * FROM swarm_config WHERE id = ?")
4780
+ .get(id);
4781
+ return row ? rowToSwarmConfig(row) : null;
4782
+ }
4783
+
4784
+ /**
4785
+ * Upsert a config entry. Inserts or updates by (scope, scopeId, key) unique constraint.
4786
+ */
4787
+ export function upsertSwarmConfig(data: {
4788
+ scope: "global" | "agent" | "repo";
4789
+ scopeId?: string | null;
4790
+ key: string;
4791
+ value: string;
4792
+ isSecret?: boolean;
4793
+ envPath?: string | null;
4794
+ description?: string | null;
4795
+ }): SwarmConfig {
4796
+ const now = new Date().toISOString();
4797
+ const scopeId = data.scope === "global" ? null : (data.scopeId ?? null);
4798
+ const isSecret = data.isSecret ? 1 : 0;
4799
+ const envPath = data.envPath ?? null;
4800
+ const description = data.description ?? null;
4801
+
4802
+ // Manual check for existing entry because SQLite's UNIQUE constraint
4803
+ // treats NULL != NULL, so ON CONFLICT never fires when scopeId is NULL (global scope).
4804
+ const existing =
4805
+ scopeId === null
4806
+ ? getDb()
4807
+ .prepare<{ id: string }, [string, string]>(
4808
+ "SELECT id FROM swarm_config WHERE scope = ? AND scopeId IS NULL AND key = ?",
4809
+ )
4810
+ .get(data.scope, data.key)
4811
+ : getDb()
4812
+ .prepare<{ id: string }, [string, string, string]>(
4813
+ "SELECT id FROM swarm_config WHERE scope = ? AND scopeId = ? AND key = ?",
4814
+ )
4815
+ .get(data.scope, scopeId, data.key);
4816
+
4817
+ let row: SwarmConfigRow | null;
4818
+
4819
+ if (existing) {
4820
+ row = getDb()
4821
+ .prepare<SwarmConfigRow, [string, number, string | null, string | null, string, string]>(
4822
+ `UPDATE swarm_config SET value = ?, isSecret = ?, envPath = ?, description = ?, lastUpdatedAt = ?
4823
+ WHERE id = ? RETURNING *`,
4824
+ )
4825
+ .get(data.value, isSecret, envPath, description, now, existing.id);
4826
+ } else {
4827
+ const id = crypto.randomUUID();
4828
+ row = getDb()
4829
+ .prepare<
4830
+ SwarmConfigRow,
4831
+ [
4832
+ string,
4833
+ string,
4834
+ string | null,
4835
+ string,
4836
+ string,
4837
+ number,
4838
+ string | null,
4839
+ string | null,
4840
+ string,
4841
+ string,
4842
+ ]
4843
+ >(
4844
+ `INSERT INTO swarm_config (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt)
4845
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
4846
+ )
4847
+ .get(id, data.scope, scopeId, data.key, data.value, isSecret, envPath, description, now, now);
4848
+ }
4849
+
4850
+ if (!row) throw new Error("Failed to upsert swarm config");
4851
+
4852
+ const config = rowToSwarmConfig(row);
4853
+
4854
+ // Write to envPath if set
4855
+ if (config.envPath) {
4856
+ try {
4857
+ writeEnvFile([config]);
4858
+ } catch (e) {
4859
+ console.error(`Failed to write env file ${config.envPath}:`, e);
4860
+ }
4861
+ }
4862
+
4863
+ return config;
4864
+ }
4865
+
4866
+ /**
4867
+ * Delete a config entry by ID.
4868
+ */
4869
+ export function deleteSwarmConfig(id: string): boolean {
4870
+ const result = getDb().run("DELETE FROM swarm_config WHERE id = ?", [id]);
4871
+ return result.changes > 0;
4872
+ }
4873
+
4874
+ /**
4875
+ * Get resolved (merged) config for a given agent and/or repo.
4876
+ * Scope resolution: repo > agent > global (most-specific wins).
4877
+ * Returns one entry per unique key with the most-specific scope winning.
4878
+ */
4879
+ export function getResolvedConfig(agentId?: string, repoId?: string): SwarmConfig[] {
4880
+ // Start with global configs
4881
+ const configMap = new Map<string, SwarmConfig>();
4882
+
4883
+ const globalConfigs = getSwarmConfigs({ scope: "global" });
4884
+ for (const config of globalConfigs) {
4885
+ configMap.set(config.key, config);
4886
+ }
4887
+
4888
+ // Overlay agent configs (agent wins over global)
4889
+ if (agentId) {
4890
+ const agentConfigs = getSwarmConfigs({ scope: "agent", scopeId: agentId });
4891
+ for (const config of agentConfigs) {
4892
+ configMap.set(config.key, config);
4893
+ }
4894
+ }
4895
+
4896
+ // Overlay repo configs (repo wins over agent and global)
4897
+ if (repoId) {
4898
+ const repoConfigs = getSwarmConfigs({ scope: "repo", scopeId: repoId });
4899
+ for (const config of repoConfigs) {
4900
+ configMap.set(config.key, config);
4901
+ }
4902
+ }
4903
+
4904
+ return Array.from(configMap.values()).sort((a, b) => a.key.localeCompare(b.key));
4905
+ }
4906
+
4907
+ // ============================================================================
4908
+ // Swarm Repos Functions (Centralized Repository Management)
4909
+ // ============================================================================
4910
+
4911
+ type SwarmRepoRow = {
4912
+ id: string;
4913
+ url: string;
4914
+ name: string;
4915
+ clonePath: string;
4916
+ defaultBranch: string;
4917
+ autoClone: number; // SQLite boolean
4918
+ createdAt: string;
4919
+ lastUpdatedAt: string;
4920
+ };
4921
+
4922
+ function rowToSwarmRepo(row: SwarmRepoRow): SwarmRepo {
4923
+ return {
4924
+ id: row.id,
4925
+ url: row.url,
4926
+ name: row.name,
4927
+ clonePath: row.clonePath,
4928
+ defaultBranch: row.defaultBranch,
4929
+ autoClone: row.autoClone === 1,
4930
+ createdAt: row.createdAt,
4931
+ lastUpdatedAt: row.lastUpdatedAt,
4932
+ };
4933
+ }
4934
+
4935
+ export function getSwarmRepos(filters?: { autoClone?: boolean; name?: string }): SwarmRepo[] {
4936
+ const conditions: string[] = [];
4937
+ const params: (string | number)[] = [];
4938
+
4939
+ if (filters?.autoClone !== undefined) {
4940
+ conditions.push("autoClone = ?");
4941
+ params.push(filters.autoClone ? 1 : 0);
4942
+ }
4943
+ if (filters?.name) {
4944
+ conditions.push("name = ?");
4945
+ params.push(filters.name);
4946
+ }
4947
+
4948
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4949
+ const query = `SELECT * FROM swarm_repos ${whereClause} ORDER BY name ASC`;
4950
+
4951
+ return getDb()
4952
+ .prepare<SwarmRepoRow, (string | number)[]>(query)
4953
+ .all(...params)
4954
+ .map(rowToSwarmRepo);
4955
+ }
4956
+
4957
+ export function getSwarmRepoById(id: string): SwarmRepo | null {
4958
+ const row = getDb()
4959
+ .prepare<SwarmRepoRow, [string]>("SELECT * FROM swarm_repos WHERE id = ?")
4960
+ .get(id);
4961
+ return row ? rowToSwarmRepo(row) : null;
4962
+ }
4963
+
4964
+ export function getSwarmRepoByName(name: string): SwarmRepo | null {
4965
+ const row = getDb()
4966
+ .prepare<SwarmRepoRow, [string]>("SELECT * FROM swarm_repos WHERE name = ?")
4967
+ .get(name);
4968
+ return row ? rowToSwarmRepo(row) : null;
4969
+ }
4970
+
4971
+ export function getSwarmRepoByUrl(url: string): SwarmRepo | null {
4972
+ const row = getDb()
4973
+ .prepare<SwarmRepoRow, [string]>("SELECT * FROM swarm_repos WHERE url = ?")
4974
+ .get(url);
4975
+ return row ? rowToSwarmRepo(row) : null;
4976
+ }
4977
+
4978
+ export function createSwarmRepo(data: {
4979
+ url: string;
4980
+ name: string;
4981
+ clonePath?: string;
4982
+ defaultBranch?: string;
4983
+ autoClone?: boolean;
4984
+ }): SwarmRepo {
4985
+ const id = crypto.randomUUID();
4986
+ const now = new Date().toISOString();
4987
+ const clonePath = data.clonePath || `/workspace/repos/${data.name}`;
4988
+
4989
+ const row = getDb()
4990
+ .prepare<SwarmRepoRow, [string, string, string, string, string, number, string, string]>(
4991
+ `INSERT INTO swarm_repos (id, url, name, clonePath, defaultBranch, autoClone, createdAt, lastUpdatedAt)
4992
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
4993
+ )
4994
+ .get(
4995
+ id,
4996
+ data.url,
4997
+ data.name,
4998
+ clonePath,
4999
+ data.defaultBranch ?? "main",
5000
+ data.autoClone !== false ? 1 : 0,
5001
+ now,
5002
+ now,
5003
+ );
5004
+
5005
+ if (!row) throw new Error("Failed to create repo");
5006
+ return rowToSwarmRepo(row);
5007
+ }
5008
+
5009
+ export function updateSwarmRepo(
5010
+ id: string,
5011
+ updates: Partial<{
5012
+ url: string;
5013
+ name: string;
5014
+ clonePath: string;
5015
+ defaultBranch: string;
5016
+ autoClone: boolean;
5017
+ }>,
5018
+ ): SwarmRepo | null {
5019
+ const setClauses: string[] = [];
5020
+ const params: (string | number)[] = [];
5021
+
5022
+ const stringFields = ["url", "name", "clonePath", "defaultBranch"] as const;
5023
+ for (const field of stringFields) {
5024
+ if (updates[field] !== undefined) {
5025
+ setClauses.push(`${field} = ?`);
5026
+ params.push(updates[field]);
5027
+ }
5028
+ }
5029
+ if (updates.autoClone !== undefined) {
5030
+ setClauses.push("autoClone = ?");
5031
+ params.push(updates.autoClone ? 1 : 0);
5032
+ }
5033
+
5034
+ if (setClauses.length === 0) return getSwarmRepoById(id);
5035
+
5036
+ setClauses.push("lastUpdatedAt = ?");
5037
+ params.push(new Date().toISOString());
5038
+ params.push(id);
5039
+
5040
+ const row = getDb()
5041
+ .prepare<SwarmRepoRow, (string | number)[]>(
5042
+ `UPDATE swarm_repos SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`,
5043
+ )
5044
+ .get(...params);
5045
+
5046
+ return row ? rowToSwarmRepo(row) : null;
5047
+ }
5048
+
5049
+ export function deleteSwarmRepo(id: string): boolean {
5050
+ const result = getDb().run("DELETE FROM swarm_repos WHERE id = ?", [id]);
5051
+ return result.changes > 0;
5052
+ }
5053
+
5054
+ // ============================================================================
5055
+ // Agent Memory Functions
5056
+ // ============================================================================
5057
+
5058
+ type AgentMemoryRow = {
5059
+ id: string;
5060
+ agentId: string | null;
5061
+ scope: string;
5062
+ name: string;
5063
+ content: string;
5064
+ summary: string | null;
5065
+ embedding: Buffer | null;
5066
+ source: string;
5067
+ sourceTaskId: string | null;
5068
+ sourcePath: string | null;
5069
+ chunkIndex: number;
5070
+ totalChunks: number;
5071
+ tags: string;
5072
+ createdAt: string;
5073
+ accessedAt: string;
5074
+ };
5075
+
5076
+ function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
5077
+ return {
5078
+ id: row.id,
5079
+ agentId: row.agentId,
5080
+ scope: row.scope as AgentMemoryScope,
5081
+ name: row.name,
5082
+ content: row.content,
5083
+ summary: row.summary,
5084
+ source: row.source as AgentMemorySource,
5085
+ sourceTaskId: row.sourceTaskId,
5086
+ sourcePath: row.sourcePath,
5087
+ chunkIndex: row.chunkIndex,
5088
+ totalChunks: row.totalChunks,
5089
+ tags: JSON.parse(row.tags || "[]"),
5090
+ createdAt: row.createdAt,
5091
+ accessedAt: row.accessedAt,
5092
+ };
5093
+ }
5094
+
5095
+ export interface CreateMemoryOptions {
5096
+ agentId?: string | null;
5097
+ scope: AgentMemoryScope;
5098
+ name: string;
5099
+ content: string;
5100
+ summary?: string | null;
5101
+ embedding?: Buffer | null;
5102
+ source: AgentMemorySource;
5103
+ sourceTaskId?: string | null;
5104
+ sourcePath?: string | null;
5105
+ chunkIndex?: number;
5106
+ totalChunks?: number;
5107
+ tags?: string[];
5108
+ }
5109
+
5110
+ export function createMemory(data: CreateMemoryOptions): AgentMemory {
5111
+ const id = crypto.randomUUID();
5112
+ const now = new Date().toISOString();
5113
+ const row = getDb()
5114
+ .prepare<
5115
+ AgentMemoryRow,
5116
+ [
5117
+ string,
5118
+ string | null,
5119
+ string,
5120
+ string,
5121
+ string,
5122
+ string | null,
5123
+ Buffer | null,
5124
+ string,
5125
+ string | null,
5126
+ string | null,
5127
+ number,
5128
+ number,
5129
+ string,
5130
+ string,
5131
+ string,
5132
+ ]
5133
+ >(
5134
+ `INSERT INTO agent_memory (id, agentId, scope, name, content, summary, embedding, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt)
5135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5136
+ )
5137
+ .get(
5138
+ id,
5139
+ data.agentId ?? null,
5140
+ data.scope,
5141
+ data.name,
5142
+ data.content,
5143
+ data.summary ?? null,
5144
+ data.embedding ?? null,
5145
+ data.source,
5146
+ data.sourceTaskId ?? null,
5147
+ data.sourcePath ?? null,
5148
+ data.chunkIndex ?? 0,
5149
+ data.totalChunks ?? 1,
5150
+ JSON.stringify(data.tags ?? []),
5151
+ now,
5152
+ now,
5153
+ );
5154
+
5155
+ if (!row) throw new Error("Failed to create memory");
5156
+ return rowToAgentMemory(row);
5157
+ }
5158
+
5159
+ export function getMemoryById(id: string): AgentMemory | null {
5160
+ const row = getDb()
5161
+ .prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
5162
+ .get(id);
5163
+ if (!row) return null;
5164
+
5165
+ // Update accessedAt
5166
+ getDb()
5167
+ .prepare("UPDATE agent_memory SET accessedAt = ? WHERE id = ?")
5168
+ .run(new Date().toISOString(), id);
5169
+
5170
+ return rowToAgentMemory(row);
5171
+ }
5172
+
5173
+ export function updateMemoryEmbedding(id: string, embedding: Buffer): void {
5174
+ getDb().prepare("UPDATE agent_memory SET embedding = ? WHERE id = ?").run(embedding, id);
5175
+ }
5176
+
5177
+ export interface SearchMemoriesOptions {
5178
+ scope?: "agent" | "swarm" | "all";
5179
+ limit?: number;
5180
+ source?: AgentMemorySource;
5181
+ isLead?: boolean;
5182
+ }
5183
+
5184
+ export function searchMemoriesByVector(
5185
+ queryEmbedding: Float32Array,
5186
+ agentId: string,
5187
+ options: SearchMemoriesOptions = {},
5188
+ ): (AgentMemory & { similarity: number })[] {
5189
+ const { scope = "all", limit = 10, source, isLead = false } = options;
5190
+
5191
+ // Build WHERE clause
5192
+ const conditions: string[] = ["embedding IS NOT NULL"];
5193
+ const params: (string | null)[] = [];
5194
+
5195
+ if (!isLead) {
5196
+ // Workers see their own agent-scoped + all swarm-scoped
5197
+ if (scope === "agent") {
5198
+ conditions.push("agentId = ? AND scope = 'agent'");
5199
+ params.push(agentId);
5200
+ } else if (scope === "swarm") {
5201
+ conditions.push("scope = 'swarm'");
5202
+ } else {
5203
+ // "all" - own agent + swarm
5204
+ conditions.push("(agentId = ? OR scope = 'swarm')");
5205
+ params.push(agentId);
5206
+ }
5207
+ } else {
5208
+ // Leads see everything
5209
+ if (scope === "agent") {
5210
+ conditions.push("scope = 'agent'");
5211
+ } else if (scope === "swarm") {
5212
+ conditions.push("scope = 'swarm'");
5213
+ }
5214
+ // "all" for lead = no scope filter needed
5215
+ }
5216
+
5217
+ if (source) {
5218
+ conditions.push("source = ?");
5219
+ params.push(source);
5220
+ }
5221
+
5222
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5223
+
5224
+ const rows = getDb()
5225
+ .prepare<AgentMemoryRow, (string | null)[]>(`SELECT * FROM agent_memory ${whereClause}`)
5226
+ .all(...params);
5227
+
5228
+ // Import cosine similarity inline to avoid circular deps
5229
+ const { cosineSimilarity, deserializeEmbedding } = require("./embedding");
5230
+
5231
+ // Compute similarities and sort
5232
+ const results: (AgentMemory & { similarity: number })[] = [];
5233
+ for (const row of rows) {
5234
+ if (!row.embedding) continue;
5235
+ const embedding = deserializeEmbedding(row.embedding);
5236
+ // Skip embeddings with mismatched dimensions (can happen if embedding model changes)
5237
+ if (embedding.length !== queryEmbedding.length) continue;
5238
+ const similarity = cosineSimilarity(queryEmbedding, embedding) as number;
5239
+ results.push({ ...rowToAgentMemory(row), similarity });
5240
+ }
5241
+
5242
+ results.sort((a, b) => b.similarity - a.similarity);
5243
+ return results.slice(0, limit);
5244
+ }
5245
+
5246
+ export interface ListMemoriesOptions {
5247
+ scope?: "agent" | "swarm" | "all";
5248
+ limit?: number;
5249
+ offset?: number;
5250
+ isLead?: boolean;
5251
+ }
5252
+
5253
+ export function listMemoriesByAgent(
5254
+ agentId: string,
5255
+ options: ListMemoriesOptions = {},
5256
+ ): AgentMemory[] {
5257
+ const { scope = "all", limit = 20, offset = 0, isLead = false } = options;
5258
+
5259
+ const conditions: string[] = [];
5260
+ const params: (string | number)[] = [];
5261
+
5262
+ if (!isLead) {
5263
+ if (scope === "agent") {
5264
+ conditions.push("agentId = ? AND scope = 'agent'");
5265
+ params.push(agentId);
5266
+ } else if (scope === "swarm") {
5267
+ conditions.push("scope = 'swarm'");
5268
+ } else {
5269
+ conditions.push("(agentId = ? OR scope = 'swarm')");
5270
+ params.push(agentId);
5271
+ }
5272
+ } else {
5273
+ if (scope === "agent") {
5274
+ conditions.push("scope = 'agent'");
5275
+ } else if (scope === "swarm") {
5276
+ conditions.push("scope = 'swarm'");
5277
+ }
5278
+ }
5279
+
5280
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5281
+
5282
+ params.push(limit, offset);
5283
+
5284
+ const rows = getDb()
5285
+ .prepare<AgentMemoryRow, (string | number)[]>(
5286
+ `SELECT * FROM agent_memory ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
5287
+ )
5288
+ .all(...params);
5289
+
5290
+ return rows.map(rowToAgentMemory);
5291
+ }
5292
+
5293
+ export function deleteMemoriesBySourcePath(sourcePath: string, agentId: string): number {
5294
+ const result = getDb()
5295
+ .prepare("DELETE FROM agent_memory WHERE sourcePath = ? AND agentId = ?")
5296
+ .run(sourcePath, agentId);
5297
+ return result.changes;
5298
+ }
5299
+
5300
+ export function deleteMemory(id: string): boolean {
5301
+ const result = getDb().prepare("DELETE FROM agent_memory WHERE id = ?").run(id);
5302
+ return result.changes > 0;
5303
+ }
5304
+
5305
+ export function getMemoryStats(agentId: string): {
5306
+ total: number;
5307
+ bySource: Record<string, number>;
5308
+ byScope: Record<string, number>;
5309
+ } {
5310
+ const total = getDb()
5311
+ .prepare<{ count: number }, [string]>(
5312
+ "SELECT COUNT(*) as count FROM agent_memory WHERE agentId = ?",
5313
+ )
5314
+ .get(agentId);
5315
+
5316
+ const bySourceRows = getDb()
5317
+ .prepare<{ source: string; count: number }, [string]>(
5318
+ "SELECT source, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY source",
5319
+ )
5320
+ .all(agentId);
5321
+
5322
+ const byScopeRows = getDb()
5323
+ .prepare<{ scope: string; count: number }, [string]>(
5324
+ "SELECT scope, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY scope",
5325
+ )
5326
+ .all(agentId);
5327
+
5328
+ const bySource: Record<string, number> = {};
5329
+ for (const row of bySourceRows) {
5330
+ bySource[row.source] = row.count;
5331
+ }
5332
+
5333
+ const byScope: Record<string, number> = {};
5334
+ for (const row of byScopeRows) {
5335
+ byScope[row.scope] = row.count;
5336
+ }
5337
+
5338
+ return { total: total?.count ?? 0, bySource, byScope };
5339
+ }
5340
+
5341
+ // ============================================================================
5342
+ // AgentMail Inbox Mapping Queries
5343
+ // ============================================================================
5344
+
5345
+ export interface AgentMailInboxMapping {
5346
+ id: string;
5347
+ inboxId: string;
5348
+ agentId: string;
5349
+ inboxEmail: string | null;
5350
+ createdAt: string;
5351
+ }
5352
+
5353
+ export function getAgentMailInboxMapping(inboxId: string): AgentMailInboxMapping | null {
5354
+ return (
5355
+ getDb()
5356
+ .prepare<AgentMailInboxMapping, [string]>(
5357
+ "SELECT * FROM agentmail_inbox_mappings WHERE inboxId = ?",
5358
+ )
5359
+ .get(inboxId) ?? null
5360
+ );
5361
+ }
5362
+
5363
+ export function getAgentMailInboxMappingsByAgent(agentId: string): AgentMailInboxMapping[] {
5364
+ return getDb()
5365
+ .prepare<AgentMailInboxMapping, [string]>(
5366
+ "SELECT * FROM agentmail_inbox_mappings WHERE agentId = ? ORDER BY createdAt DESC",
5367
+ )
5368
+ .all(agentId);
5369
+ }
5370
+
5371
+ export function getAllAgentMailInboxMappings(): AgentMailInboxMapping[] {
5372
+ return getDb()
5373
+ .prepare<AgentMailInboxMapping, []>(
5374
+ "SELECT * FROM agentmail_inbox_mappings ORDER BY createdAt DESC",
5375
+ )
5376
+ .all();
5377
+ }
5378
+
5379
+ export function createAgentMailInboxMapping(
5380
+ inboxId: string,
5381
+ agentId: string,
5382
+ inboxEmail?: string,
5383
+ ): AgentMailInboxMapping {
5384
+ const id = crypto.randomUUID();
5385
+ const now = new Date().toISOString();
5386
+
5387
+ const row = getDb()
5388
+ .prepare<AgentMailInboxMapping, [string, string, string, string | null, string]>(
5389
+ `INSERT INTO agentmail_inbox_mappings (id, inboxId, agentId, inboxEmail, createdAt)
5390
+ VALUES (?, ?, ?, ?, ?)
5391
+ ON CONFLICT(inboxId) DO UPDATE SET agentId = excluded.agentId, inboxEmail = excluded.inboxEmail
5392
+ RETURNING *`,
5393
+ )
5394
+ .get(id, inboxId, agentId, inboxEmail ?? null, now);
5395
+
5396
+ if (!row) throw new Error("Failed to create AgentMail inbox mapping");
5397
+ return row;
5398
+ }
5399
+
5400
+ export function deleteAgentMailInboxMapping(inboxId: string): boolean {
5401
+ const result = getDb()
5402
+ .prepare("DELETE FROM agentmail_inbox_mappings WHERE inboxId = ?")
5403
+ .run(inboxId);
5404
+ return result.changes > 0;
5405
+ }
5406
+
5407
+ /**
5408
+ * Find the most recent task by AgentMail thread ID
5409
+ * Includes completed/failed tasks to maintain thread continuity via parentTaskId
5410
+ */
5411
+ export function findTaskByAgentMailThread(agentmailThreadId: string): AgentTask | null {
5412
+ const row = getDb()
5413
+ .prepare<AgentTaskRow, [string]>(
5414
+ `SELECT * FROM agent_tasks
5415
+ WHERE agentmailThreadId = ?
5416
+ ORDER BY createdAt DESC
5417
+ LIMIT 1`,
5418
+ )
5419
+ .get(agentmailThreadId);
5420
+ return row ? rowToAgentTask(row) : null;
5421
+ }
5422
+
5423
+ // ============================================================================
5424
+ // Active Sessions (runner session tracking for concurrency awareness)
5425
+ // ============================================================================
5426
+
5427
+ export function insertActiveSession(session: {
5428
+ agentId: string;
5429
+ taskId?: string;
5430
+ triggerType: string;
5431
+ inboxMessageId?: string;
5432
+ taskDescription?: string;
5433
+ }): ActiveSession {
5434
+ const id = crypto.randomUUID();
5435
+ const now = new Date().toISOString();
5436
+
5437
+ const row = getDb()
5438
+ .prepare<
5439
+ ActiveSession,
5440
+ [string, string, string | null, string, string | null, string | null, string, string]
5441
+ >(
5442
+ `INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, startedAt, lastHeartbeatAt)
5443
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
5444
+ RETURNING *`,
5445
+ )
5446
+ .get(
5447
+ id,
5448
+ session.agentId,
5449
+ session.taskId ?? null,
5450
+ session.triggerType,
5451
+ session.inboxMessageId ?? null,
5452
+ session.taskDescription ?? null,
5453
+ now,
5454
+ now,
5455
+ );
5456
+
5457
+ if (!row) throw new Error("Failed to insert active session");
5458
+ return row;
5459
+ }
5460
+
5461
+ export function deleteActiveSession(taskId: string): boolean {
5462
+ const result = getDb().prepare("DELETE FROM active_sessions WHERE taskId = ?").run(taskId);
5463
+ return result.changes > 0;
5464
+ }
5465
+
5466
+ export function deleteActiveSessionById(id: string): boolean {
5467
+ const result = getDb().prepare("DELETE FROM active_sessions WHERE id = ?").run(id);
5468
+ return result.changes > 0;
5469
+ }
5470
+
5471
+ export function getActiveSessions(agentId?: string): ActiveSession[] {
5472
+ if (agentId) {
5473
+ return getDb()
5474
+ .prepare<ActiveSession, [string]>(
5475
+ "SELECT * FROM active_sessions WHERE agentId = ? ORDER BY startedAt DESC",
5476
+ )
5477
+ .all(agentId);
5478
+ }
5479
+ return getDb()
5480
+ .prepare<ActiveSession, []>("SELECT * FROM active_sessions ORDER BY startedAt DESC")
5481
+ .all();
5482
+ }
5483
+
5484
+ export function heartbeatActiveSession(taskId: string): boolean {
5485
+ const now = new Date().toISOString();
5486
+ const result = getDb()
5487
+ .prepare("UPDATE active_sessions SET lastHeartbeatAt = ? WHERE taskId = ?")
5488
+ .run(now, taskId);
5489
+ return result.changes > 0;
5490
+ }
5491
+
5492
+ export function cleanupStaleSessions(maxAgeMinutes = 30): number {
5493
+ const cutoff = new Date(Date.now() - maxAgeMinutes * 60 * 1000).toISOString();
5494
+ const result = getDb()
5495
+ .prepare("DELETE FROM active_sessions WHERE lastHeartbeatAt < ?")
5496
+ .run(cutoff);
5497
+ return result.changes;
5498
+ }
5499
+
5500
+ export function cleanupAgentSessions(agentId: string): number {
5501
+ const result = getDb().prepare("DELETE FROM active_sessions WHERE agentId = ?").run(agentId);
5502
+ return result.changes;
5503
+ }
5504
+
5505
+ // ============================================================================
5506
+ // Heartbeat / Triage Query Functions
5507
+ // ============================================================================
5508
+
5509
+ /**
5510
+ * Get in_progress tasks that haven't been updated within the given threshold.
5511
+ * Used by the heartbeat to detect potentially stalled tasks.
5512
+ */
5513
+ export function getStalledInProgressTasks(thresholdMinutes: number = 30): AgentTask[] {
5514
+ const cutoff = new Date(Date.now() - thresholdMinutes * 60 * 1000).toISOString();
5515
+ return getDb()
5516
+ .prepare<AgentTaskRow, [string]>(
5517
+ `SELECT * FROM agent_tasks
5518
+ WHERE status = 'in_progress' AND lastUpdatedAt < ?
5519
+ ORDER BY lastUpdatedAt ASC`,
5520
+ )
5521
+ .all(cutoff)
5522
+ .map(rowToAgentTask);
5523
+ }
5524
+
5525
+ /**
5526
+ * Get idle, non-lead, non-offline agents that have capacity for more tasks.
5527
+ * Used by the heartbeat for auto-assignment of pool tasks.
5528
+ */
5529
+ export function getIdleWorkersWithCapacity(): Agent[] {
5530
+ const agents = getDb()
5531
+ .prepare<AgentRow, []>(
5532
+ `SELECT * FROM agents
5533
+ WHERE status = 'idle' AND isLead = 0`,
5534
+ )
5535
+ .all()
5536
+ .map(rowToAgent);
5537
+
5538
+ return agents.filter((agent) => {
5539
+ const activeCount = getActiveTaskCount(agent.id);
5540
+ return activeCount < (agent.maxTasks ?? 1);
5541
+ });
5542
+ }
5543
+
5544
+ /**
5545
+ * Get unassigned pool tasks ordered by priority (DESC) then creation time (ASC).
5546
+ * Used by the heartbeat for auto-assignment.
5547
+ */
5548
+ export function getUnassignedPoolTasks(limit: number = 10): AgentTask[] {
5549
+ return getDb()
5550
+ .prepare<AgentTaskRow, [number]>(
5551
+ `SELECT * FROM agent_tasks
5552
+ WHERE status = 'unassigned'
5553
+ ORDER BY priority DESC, createdAt ASC
5554
+ LIMIT ?`,
5555
+ )
5556
+ .all(limit)
5557
+ .map(rowToAgentTask);
5558
+ }
5559
+
5560
+ // ============================================================================
5561
+ // Workflow CRUD
5562
+ // ============================================================================
5563
+
5564
+ type WorkflowRow = {
5565
+ id: string;
5566
+ name: string;
5567
+ description: string | null;
5568
+ enabled: number;
5569
+ definition: string;
5570
+ triggers: string;
5571
+ cooldown: string | null;
5572
+ input: string | null;
5573
+ triggerSchema: string | null;
5574
+ createdByAgentId: string | null;
5575
+ createdAt: string;
5576
+ lastUpdatedAt: string;
5577
+ };
5578
+
5579
+ function rowToWorkflow(row: WorkflowRow): Workflow {
5580
+ return {
5581
+ id: row.id,
5582
+ name: row.name,
5583
+ description: row.description ?? undefined,
5584
+ enabled: row.enabled === 1,
5585
+ definition: JSON.parse(row.definition) as WorkflowDefinition,
5586
+ triggers: JSON.parse(row.triggers) as TriggerConfig[],
5587
+ cooldown: row.cooldown ? (JSON.parse(row.cooldown) as CooldownConfig) : undefined,
5588
+ input: row.input ? (JSON.parse(row.input) as Record<string, InputValue>) : undefined,
5589
+ triggerSchema: row.triggerSchema
5590
+ ? (JSON.parse(row.triggerSchema) as Record<string, unknown>)
5591
+ : undefined,
5592
+ createdByAgentId: row.createdByAgentId ?? undefined,
5593
+ createdAt: row.createdAt,
5594
+ lastUpdatedAt: row.lastUpdatedAt,
5595
+ };
5596
+ }
5597
+
5598
+ export function createWorkflow(data: {
5599
+ name: string;
5600
+ description?: string;
5601
+ definition: WorkflowDefinition;
5602
+ triggers?: TriggerConfig[];
5603
+ cooldown?: CooldownConfig;
5604
+ input?: Record<string, InputValue>;
5605
+ triggerSchema?: Record<string, unknown>;
5606
+ createdByAgentId?: string;
5607
+ }): Workflow {
5608
+ const id = crypto.randomUUID();
5609
+ const row = getDb()
5610
+ .prepare<
5611
+ WorkflowRow,
5612
+ [
5613
+ string,
5614
+ string,
5615
+ string | null,
5616
+ string,
5617
+ string,
5618
+ string | null,
5619
+ string | null,
5620
+ string | null,
5621
+ string | null,
5622
+ ]
5623
+ >(
5624
+ `INSERT INTO workflows (id, name, description, definition, triggers, cooldown, input, triggerSchema, createdByAgentId)
5625
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5626
+ )
5627
+ .get(
5628
+ id,
5629
+ data.name,
5630
+ data.description ?? null,
5631
+ JSON.stringify(data.definition),
5632
+ JSON.stringify(data.triggers ?? []),
5633
+ data.cooldown ? JSON.stringify(data.cooldown) : null,
5634
+ data.input ? JSON.stringify(data.input) : null,
5635
+ data.triggerSchema ? JSON.stringify(data.triggerSchema) : null,
5636
+ data.createdByAgentId ?? null,
5637
+ );
5638
+ if (!row) throw new Error("Failed to create workflow");
5639
+ return rowToWorkflow(row);
5640
+ }
5641
+
5642
+ export function getWorkflow(id: string): Workflow | null {
5643
+ const row = getDb()
5644
+ .prepare<WorkflowRow, [string]>("SELECT * FROM workflows WHERE id = ?")
5645
+ .get(id);
5646
+ return row ? rowToWorkflow(row) : null;
5647
+ }
5648
+
5649
+ export function listWorkflows(filters?: { enabled?: boolean }): Workflow[] {
5650
+ let query = "SELECT * FROM workflows WHERE 1=1";
5651
+ const params: (string | number)[] = [];
5652
+ if (filters?.enabled !== undefined) {
5653
+ query += " AND enabled = ?";
5654
+ params.push(filters.enabled ? 1 : 0);
5655
+ }
5656
+ query += " ORDER BY name ASC";
5657
+ return getDb()
5658
+ .prepare<WorkflowRow, (string | number)[]>(query)
5659
+ .all(...params)
5660
+ .map(rowToWorkflow);
5661
+ }
5662
+
5663
+ export function updateWorkflow(
5664
+ id: string,
5665
+ data: {
5666
+ name?: string;
5667
+ description?: string;
5668
+ enabled?: boolean;
5669
+ definition?: WorkflowDefinition;
5670
+ triggers?: TriggerConfig[];
5671
+ cooldown?: CooldownConfig | null;
5672
+ input?: Record<string, InputValue> | null;
5673
+ triggerSchema?: Record<string, unknown> | null;
5674
+ },
5675
+ ): Workflow | null {
5676
+ const updates: string[] = [];
5677
+ const params: (string | number | null)[] = [];
5678
+ if (data.name !== undefined) {
5679
+ updates.push("name = ?");
5680
+ params.push(data.name);
5681
+ }
5682
+ if (data.description !== undefined) {
5683
+ updates.push("description = ?");
5684
+ params.push(data.description);
5685
+ }
5686
+ if (data.enabled !== undefined) {
5687
+ updates.push("enabled = ?");
5688
+ params.push(data.enabled ? 1 : 0);
5689
+ }
5690
+ if (data.definition !== undefined) {
5691
+ updates.push("definition = ?");
5692
+ params.push(JSON.stringify(data.definition));
5693
+ }
5694
+ if (data.triggers !== undefined) {
5695
+ updates.push("triggers = ?");
5696
+ params.push(JSON.stringify(data.triggers));
5697
+ }
5698
+ if (data.cooldown !== undefined) {
5699
+ updates.push("cooldown = ?");
5700
+ params.push(data.cooldown ? JSON.stringify(data.cooldown) : null);
5701
+ }
5702
+ if (data.input !== undefined) {
5703
+ updates.push("input = ?");
5704
+ params.push(data.input ? JSON.stringify(data.input) : null);
5705
+ }
5706
+ if (data.triggerSchema !== undefined) {
5707
+ updates.push("triggerSchema = ?");
5708
+ params.push(data.triggerSchema ? JSON.stringify(data.triggerSchema) : null);
5709
+ }
5710
+ if (updates.length === 0) return getWorkflow(id);
5711
+ updates.push("lastUpdatedAt = ?");
5712
+ params.push(new Date().toISOString());
5713
+ params.push(id);
5714
+ const row = getDb()
5715
+ .prepare<WorkflowRow, (string | number | null)[]>(
5716
+ `UPDATE workflows SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
5717
+ )
5718
+ .get(...params);
5719
+ return row ? rowToWorkflow(row) : null;
5720
+ }
5721
+
5722
+ export function deleteWorkflow(id: string): boolean {
5723
+ const db = getDb();
5724
+ // Cascade delete in FK-safe order:
5725
+ // 1. Unlink agent_tasks (they reference steps and runs)
5726
+ db.run(
5727
+ `UPDATE agent_tasks SET workflowRunId = NULL, workflowRunStepId = NULL WHERE workflowRunId IN (SELECT id FROM workflow_runs WHERE workflowId = ?)`,
5728
+ [id],
5729
+ );
5730
+ // 2. Delete steps (they reference runs)
5731
+ db.run(
5732
+ `DELETE FROM workflow_run_steps WHERE runId IN (SELECT id FROM workflow_runs WHERE workflowId = ?)`,
5733
+ [id],
5734
+ );
5735
+ // 3. Delete runs (they reference workflow)
5736
+ db.run("DELETE FROM workflow_runs WHERE workflowId = ?", [id]);
5737
+ // 4. Delete workflow
5738
+ const result = db.run("DELETE FROM workflows WHERE id = ?", [id]);
5739
+ return result.changes > 0;
5740
+ }
5741
+
5742
+ // ============================================================================
5743
+ // Workflow Run CRUD
5744
+ // ============================================================================
5745
+
5746
+ type WorkflowRunRow = {
5747
+ id: string;
5748
+ workflowId: string;
5749
+ status: string;
5750
+ triggerData: string | null;
5751
+ context: string | null;
5752
+ error: string | null;
5753
+ startedAt: string;
5754
+ lastUpdatedAt: string;
5755
+ finishedAt: string | null;
5756
+ };
5757
+
5758
+ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
5759
+ return {
5760
+ id: row.id,
5761
+ workflowId: row.workflowId,
5762
+ status: row.status as WorkflowRunStatus,
5763
+ triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
5764
+ context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
5765
+ error: row.error ?? undefined,
5766
+ startedAt: row.startedAt,
5767
+ lastUpdatedAt: row.lastUpdatedAt,
5768
+ finishedAt: row.finishedAt ?? undefined,
5769
+ };
5770
+ }
5771
+
5772
+ export function createWorkflowRun(data: {
5773
+ id: string;
5774
+ workflowId: string;
5775
+ triggerData?: unknown;
5776
+ }): WorkflowRun {
5777
+ const now = new Date().toISOString();
5778
+ const row = getDb()
5779
+ .prepare<WorkflowRunRow, [string, string, string, string | null]>(
5780
+ `INSERT INTO workflow_runs (id, workflowId, startedAt, triggerData) VALUES (?, ?, ?, ?) RETURNING *`,
5781
+ )
5782
+ .get(data.id, data.workflowId, now, data.triggerData ? JSON.stringify(data.triggerData) : null);
5783
+ if (!row) throw new Error("Failed to create workflow run");
5784
+ return rowToWorkflowRun(row);
5785
+ }
5786
+
5787
+ export function getWorkflowRun(id: string): WorkflowRun | null {
5788
+ const row = getDb()
5789
+ .prepare<WorkflowRunRow, [string]>("SELECT * FROM workflow_runs WHERE id = ?")
5790
+ .get(id);
5791
+ return row ? rowToWorkflowRun(row) : null;
5792
+ }
5793
+
5794
+ export function updateWorkflowRun(
5795
+ id: string,
5796
+ data: {
5797
+ status?: WorkflowRunStatus;
5798
+ context?: Record<string, unknown>;
5799
+ error?: string;
5800
+ finishedAt?: string;
5801
+ },
5802
+ ): WorkflowRun | null {
5803
+ const updates: string[] = [];
5804
+ const params: (string | null)[] = [];
5805
+ if (data.status !== undefined) {
5806
+ updates.push("status = ?");
5807
+ params.push(data.status);
5808
+ }
5809
+ if (data.context !== undefined) {
5810
+ updates.push("context = ?");
5811
+ params.push(JSON.stringify(data.context));
5812
+ }
5813
+ if (data.error !== undefined) {
5814
+ updates.push("error = ?");
5815
+ params.push(data.error);
5816
+ }
5817
+ if (data.finishedAt !== undefined) {
5818
+ updates.push("finishedAt = ?");
5819
+ params.push(data.finishedAt);
5820
+ }
5821
+ if (updates.length === 0) return getWorkflowRun(id);
5822
+ updates.push("lastUpdatedAt = ?");
5823
+ params.push(new Date().toISOString());
5824
+ params.push(id);
5825
+ const row = getDb()
5826
+ .prepare<WorkflowRunRow, (string | null)[]>(
5827
+ `UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
5828
+ )
5829
+ .get(...params);
5830
+ return row ? rowToWorkflowRun(row) : null;
5831
+ }
5832
+
5833
+ export function listWorkflowRuns(workflowId: string): WorkflowRun[] {
5834
+ return getDb()
5835
+ .prepare<WorkflowRunRow, [string]>(
5836
+ "SELECT * FROM workflow_runs WHERE workflowId = ? ORDER BY startedAt DESC",
5837
+ )
5838
+ .all(workflowId)
5839
+ .map(rowToWorkflowRun);
5840
+ }
5841
+
5842
+ // ============================================================================
5843
+ // Workflow Run Step CRUD
5844
+ // ============================================================================
5845
+
5846
+ type WorkflowRunStepRow = {
5847
+ id: string;
5848
+ runId: string;
5849
+ nodeId: string;
5850
+ nodeType: string;
5851
+ status: string;
5852
+ input: string | null;
5853
+ output: string | null;
5854
+ error: string | null;
5855
+ startedAt: string;
5856
+ finishedAt: string | null;
5857
+ retryCount: number;
5858
+ maxRetries: number;
5859
+ nextRetryAt: string | null;
5860
+ idempotencyKey: string | null;
5861
+ diagnostics: string | null;
5862
+ nextPort: string | null;
5863
+ };
5864
+
5865
+ function rowToWorkflowRunStep(row: WorkflowRunStepRow): WorkflowRunStep {
5866
+ return {
5867
+ id: row.id,
5868
+ runId: row.runId,
5869
+ nodeId: row.nodeId,
5870
+ nodeType: row.nodeType,
5871
+ status: row.status as WorkflowRunStepStatus,
5872
+ input: row.input ? JSON.parse(row.input) : undefined,
5873
+ output: row.output ? JSON.parse(row.output) : undefined,
5874
+ error: row.error ?? undefined,
5875
+ startedAt: row.startedAt,
5876
+ finishedAt: row.finishedAt ?? undefined,
5877
+ retryCount: row.retryCount,
5878
+ maxRetries: row.maxRetries,
5879
+ nextRetryAt: row.nextRetryAt ?? undefined,
5880
+ idempotencyKey: row.idempotencyKey ?? undefined,
5881
+ diagnostics: row.diagnostics ?? undefined,
5882
+ nextPort: row.nextPort ?? undefined,
5883
+ };
5884
+ }
5885
+
5886
+ export function createWorkflowRunStep(data: {
5887
+ id: string;
5888
+ runId: string;
5889
+ nodeId: string;
5890
+ nodeType: string;
5891
+ input?: unknown;
5892
+ }): WorkflowRunStep {
5893
+ const now = new Date().toISOString();
5894
+ const row = getDb()
5895
+ .prepare<WorkflowRunStepRow, [string, string, string, string, string, string | null]>(
5896
+ `INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, status, startedAt, input)
5897
+ VALUES (?, ?, ?, ?, 'running', ?, ?) RETURNING *`,
5898
+ )
5899
+ .get(
5900
+ data.id,
5901
+ data.runId,
5902
+ data.nodeId,
5903
+ data.nodeType,
5904
+ now,
5905
+ data.input ? JSON.stringify(data.input) : null,
5906
+ );
5907
+ if (!row) throw new Error("Failed to create workflow run step");
5908
+ return rowToWorkflowRunStep(row);
5909
+ }
5910
+
5911
+ export function getWorkflowRunStep(id: string): WorkflowRunStep | null {
5912
+ const row = getDb()
5913
+ .prepare<WorkflowRunStepRow, [string]>("SELECT * FROM workflow_run_steps WHERE id = ?")
5914
+ .get(id);
5915
+ return row ? rowToWorkflowRunStep(row) : null;
5916
+ }
5917
+
5918
+ export function updateWorkflowRunStep(
5919
+ id: string,
5920
+ data: {
5921
+ status?: WorkflowRunStepStatus;
5922
+ output?: unknown;
5923
+ error?: string;
5924
+ finishedAt?: string;
5925
+ retryCount?: number;
5926
+ maxRetries?: number;
5927
+ nextRetryAt?: string | null;
5928
+ idempotencyKey?: string;
5929
+ diagnostics?: string;
5930
+ nextPort?: string;
5931
+ },
5932
+ ): WorkflowRunStep | null {
5933
+ const updates: string[] = [];
5934
+ const params: (string | number | null)[] = [];
5935
+ if (data.status !== undefined) {
5936
+ updates.push("status = ?");
5937
+ params.push(data.status);
5938
+ }
5939
+ if (data.output !== undefined) {
5940
+ updates.push("output = ?");
5941
+ params.push(JSON.stringify(data.output));
5942
+ }
5943
+ if (data.error !== undefined) {
5944
+ updates.push("error = ?");
5945
+ params.push(data.error);
5946
+ }
5947
+ if (data.finishedAt !== undefined) {
5948
+ updates.push("finishedAt = ?");
5949
+ params.push(data.finishedAt);
5950
+ }
5951
+ if (data.retryCount !== undefined) {
5952
+ updates.push("retryCount = ?");
5953
+ params.push(data.retryCount);
5954
+ }
5955
+ if (data.maxRetries !== undefined) {
5956
+ updates.push("maxRetries = ?");
5957
+ params.push(data.maxRetries);
5958
+ }
5959
+ if (data.nextRetryAt !== undefined) {
5960
+ updates.push("nextRetryAt = ?");
5961
+ params.push(data.nextRetryAt);
5962
+ }
5963
+ if (data.idempotencyKey !== undefined) {
5964
+ updates.push("idempotencyKey = ?");
5965
+ params.push(data.idempotencyKey);
5966
+ }
5967
+ if (data.diagnostics !== undefined) {
5968
+ updates.push("diagnostics = ?");
5969
+ params.push(data.diagnostics);
5970
+ }
5971
+ if (data.nextPort !== undefined) {
5972
+ updates.push("nextPort = ?");
5973
+ params.push(data.nextPort);
5974
+ }
5975
+ if (updates.length === 0) return getWorkflowRunStep(id);
5976
+ params.push(id);
5977
+ const row = getDb()
5978
+ .prepare<WorkflowRunStepRow, (string | number | null)[]>(
5979
+ `UPDATE workflow_run_steps SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
5980
+ )
5981
+ .get(...params);
5982
+ return row ? rowToWorkflowRunStep(row) : null;
5983
+ }
5984
+
5985
+ export function getWorkflowRunStepsByRunId(runId: string): WorkflowRunStep[] {
5986
+ return getDb()
5987
+ .prepare<WorkflowRunStepRow, [string]>(
5988
+ "SELECT * FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC",
5989
+ )
5990
+ .all(runId)
5991
+ .map(rowToWorkflowRunStep);
5992
+ }
5993
+
5994
+ // --- Stuck Workflow Run Recovery ---
5995
+
5996
+ export interface StuckWorkflowRun {
5997
+ runId: string;
5998
+ stepId: string;
5999
+ nodeId: string;
6000
+ taskStatus: string;
6001
+ taskOutput: string | null;
6002
+ workflowId: string;
6003
+ }
6004
+
6005
+ export function getStuckWorkflowRuns(): StuckWorkflowRun[] {
6006
+ return getDb()
6007
+ .prepare<StuckWorkflowRun, []>(
6008
+ `SELECT
6009
+ wr.id as runId,
6010
+ wrs.id as stepId,
6011
+ wrs.nodeId,
6012
+ at.status as taskStatus,
6013
+ at.output as taskOutput,
6014
+ wr.workflowId
6015
+ FROM workflow_runs wr
6016
+ JOIN workflow_run_steps wrs ON wrs.runId = wr.id AND wrs.status = 'waiting'
6017
+ JOIN agent_tasks at ON at.workflowRunStepId = wrs.id
6018
+ WHERE wr.status = 'waiting'
6019
+ AND at.status IN ('completed', 'failed', 'cancelled')`,
6020
+ )
6021
+ .all();
6022
+ }
6023
+
6024
+ // --- New Workflow Query Functions ---
6025
+
6026
+ export function getLastSuccessfulRun(workflowId: string): WorkflowRun | null {
6027
+ const row = getDb()
6028
+ .prepare<WorkflowRunRow, [string]>(
6029
+ `SELECT * FROM workflow_runs
6030
+ WHERE workflowId = ? AND status = 'completed'
6031
+ ORDER BY finishedAt DESC LIMIT 1`,
6032
+ )
6033
+ .get(workflowId);
6034
+ return row ? rowToWorkflowRun(row) : null;
6035
+ }
6036
+
6037
+ export function getRetryableSteps(): WorkflowRunStep[] {
6038
+ const now = new Date().toISOString();
6039
+ return getDb()
6040
+ .prepare<WorkflowRunStepRow, [string]>(
6041
+ `SELECT * FROM workflow_run_steps
6042
+ WHERE status = 'failed'
6043
+ AND nextRetryAt IS NOT NULL
6044
+ AND nextRetryAt <= ?
6045
+ ORDER BY nextRetryAt ASC`,
6046
+ )
6047
+ .all(now)
6048
+ .map(rowToWorkflowRunStep);
6049
+ }
6050
+
6051
+ export function getCompletedStepNodeIds(runId: string): string[] {
6052
+ const rows = getDb()
6053
+ .prepare<{ nodeId: string }, [string]>(
6054
+ `SELECT nodeId FROM workflow_run_steps
6055
+ WHERE runId = ? AND status = 'completed'`,
6056
+ )
6057
+ .all(runId);
6058
+ return rows.map((r) => r.nodeId);
6059
+ }
6060
+
6061
+ export function getTaskByWorkflowRunStepId(stepId: string): AgentTask | null {
6062
+ const row = getDb()
6063
+ .prepare<AgentTaskRow, [string]>(
6064
+ "SELECT * FROM agent_tasks WHERE workflowRunStepId = ? LIMIT 1",
6065
+ )
6066
+ .get(stepId);
6067
+ return row ? rowToAgentTask(row) : null;
6068
+ }
6069
+
6070
+ export function getStepByIdempotencyKey(key: string): WorkflowRunStep | null {
6071
+ const row = getDb()
6072
+ .prepare<WorkflowRunStepRow, [string]>(
6073
+ "SELECT * FROM workflow_run_steps WHERE idempotencyKey = ?",
6074
+ )
6075
+ .get(key);
6076
+ return row ? rowToWorkflowRunStep(row) : null;
6077
+ }
6078
+
6079
+ // --- Workflow Version History ---
6080
+
6081
+ type WorkflowVersionRow = {
6082
+ id: string;
6083
+ workflowId: string;
6084
+ version: number;
6085
+ snapshot: string;
6086
+ changedByAgentId: string | null;
6087
+ createdAt: string;
6088
+ };
6089
+
6090
+ function rowToWorkflowVersion(row: WorkflowVersionRow): WorkflowVersion {
6091
+ return {
6092
+ id: row.id,
6093
+ workflowId: row.workflowId,
6094
+ version: row.version,
6095
+ snapshot: JSON.parse(row.snapshot) as WorkflowSnapshot,
6096
+ changedByAgentId: row.changedByAgentId ?? undefined,
6097
+ createdAt: row.createdAt,
6098
+ };
6099
+ }
6100
+
6101
+ export function createWorkflowVersion(data: {
6102
+ workflowId: string;
6103
+ version: number;
6104
+ snapshot: WorkflowSnapshot;
6105
+ changedByAgentId?: string;
6106
+ }): WorkflowVersion {
6107
+ const id = crypto.randomUUID();
6108
+ const row = getDb()
6109
+ .prepare<WorkflowVersionRow, [string, string, number, string, string | null]>(
6110
+ `INSERT INTO workflow_versions (id, workflowId, version, snapshot, changedByAgentId)
6111
+ VALUES (?, ?, ?, ?, ?) RETURNING *`,
6112
+ )
6113
+ .get(
6114
+ id,
6115
+ data.workflowId,
6116
+ data.version,
6117
+ JSON.stringify(data.snapshot),
6118
+ data.changedByAgentId ?? null,
6119
+ );
6120
+ if (!row) throw new Error("Failed to create workflow version");
6121
+ return rowToWorkflowVersion(row);
6122
+ }
6123
+
6124
+ export function getWorkflowVersions(workflowId: string): WorkflowVersion[] {
6125
+ return getDb()
6126
+ .prepare<WorkflowVersionRow, [string]>(
6127
+ "SELECT * FROM workflow_versions WHERE workflowId = ? ORDER BY version DESC",
6128
+ )
6129
+ .all(workflowId)
6130
+ .map(rowToWorkflowVersion);
6131
+ }
6132
+
6133
+ export function getWorkflowVersion(workflowId: string, version: number): WorkflowVersion | null {
6134
+ const row = getDb()
6135
+ .prepare<WorkflowVersionRow, [string, number]>(
6136
+ "SELECT * FROM workflow_versions WHERE workflowId = ? AND version = ?",
6137
+ )
6138
+ .get(workflowId, version);
6139
+ return row ? rowToWorkflowVersion(row) : null;
6140
+ }