@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.
- package/.claude/settings.json +84 -0
- package/.claude/settings.local.json +3 -1
- package/.entire/settings.json +4 -0
- package/.env.docker.example +40 -23
- package/.env.example +39 -1
- package/.github/ISSUE_TEMPLATE/bug_report.yml +78 -0
- package/.github/ISSUE_TEMPLATE/community-template.yml +77 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +60 -0
- package/.github/PULL_REQUEST_TEMPLATE/community-template.md +29 -0
- package/.github/workflows/ci.yml +3 -27
- package/.github/workflows/docker-and-deploy.yml +20 -5
- package/.github/workflows/merge-gate.yml +233 -0
- package/.opencode/plugins/entire.ts +133 -0
- package/.superset/config.json +6 -0
- package/.wts-setup.ts +103 -34
- package/CHANGELOG.md +447 -0
- package/CLAUDE.md +422 -5
- package/CONTRIBUTING.md +46 -1
- package/DEPLOYMENT.md +26 -9
- package/Dockerfile +15 -7
- package/Dockerfile.worker +99 -67
- package/MCP.md +452 -17
- package/README.md +271 -169
- package/api-entrypoint.sh +56 -0
- package/depot.json +1 -0
- package/docker-compose.example.yml +194 -3
- package/docker-compose.local.yml +119 -0
- package/docker-entrypoint.sh +393 -113
- package/docs-site/app/api/search/route.ts +4 -0
- package/docs-site/app/docs/[[...slug]]/page.tsx +87 -0
- package/docs-site/app/docs/layout.tsx +12 -0
- package/docs-site/app/globals.css +24 -0
- package/docs-site/app/layout.config.tsx +34 -0
- package/docs-site/app/layout.tsx +119 -0
- package/docs-site/app/llms-full.txt/route.ts +11 -0
- package/docs-site/app/llms.mdx/docs/[[...slug]]/route.ts +24 -0
- package/docs-site/app/llms.txt/route.ts +8 -0
- package/docs-site/app/page.tsx +5 -0
- package/docs-site/app/robots.ts +13 -0
- package/docs-site/app/sitemap.ts +37 -0
- package/docs-site/components/api-page.client.tsx +4 -0
- package/docs-site/components/api-page.tsx +7 -0
- package/docs-site/components/mdx/mermaid.tsx +55 -0
- package/docs-site/content/docs/(documentation)/architecture/agents.mdx +117 -0
- package/docs-site/content/docs/(documentation)/architecture/hooks.mdx +77 -0
- package/docs-site/content/docs/(documentation)/architecture/memory.mdx +96 -0
- package/docs-site/content/docs/(documentation)/architecture/meta.json +4 -0
- package/docs-site/content/docs/(documentation)/architecture/overview.mdx +172 -0
- package/docs-site/content/docs/(documentation)/concepts/epics.mdx +98 -0
- package/docs-site/content/docs/(documentation)/concepts/meta.json +4 -0
- package/docs-site/content/docs/(documentation)/concepts/scheduling.mdx +136 -0
- package/docs-site/content/docs/(documentation)/concepts/services.mdx +104 -0
- package/docs-site/content/docs/(documentation)/concepts/task-lifecycle.mdx +148 -0
- package/docs-site/content/docs/(documentation)/concepts/workflows.mdx +209 -0
- package/docs-site/content/docs/(documentation)/contributing.mdx +158 -0
- package/docs-site/content/docs/(documentation)/getting-started.mdx +157 -0
- package/docs-site/content/docs/(documentation)/guides/agentmail-integration.mdx +79 -0
- package/docs-site/content/docs/(documentation)/guides/deployment.mdx +171 -0
- package/docs-site/content/docs/(documentation)/guides/github-integration.mdx +81 -0
- package/docs-site/content/docs/(documentation)/guides/gitlab-integration.mdx +93 -0
- package/docs-site/content/docs/(documentation)/guides/linear-integration.mdx +98 -0
- package/docs-site/content/docs/(documentation)/guides/meta.json +13 -0
- package/docs-site/content/docs/(documentation)/guides/sentry-integration.mdx +52 -0
- package/docs-site/content/docs/(documentation)/guides/slack-integration.mdx +179 -0
- package/docs-site/content/docs/(documentation)/guides/x402-payments.mdx +154 -0
- package/docs-site/content/docs/(documentation)/index.mdx +65 -0
- package/docs-site/content/docs/(documentation)/meta.json +19 -0
- package/docs-site/content/docs/(documentation)/reference/cli.mdx +241 -0
- package/docs-site/content/docs/(documentation)/reference/environment-variables.mdx +205 -0
- package/docs-site/content/docs/(documentation)/reference/mcp-tools.mdx +449 -0
- package/docs-site/content/docs/(documentation)/reference/meta.json +4 -0
- package/docs-site/content/docs/api-reference/active-sessions.mdx +9 -0
- package/docs-site/content/docs/api-reference/agents.mdx +9 -0
- package/docs-site/content/docs/api-reference/channels.mdx +9 -0
- package/docs-site/content/docs/api-reference/config.mdx +9 -0
- package/docs-site/content/docs/api-reference/debug.mdx +9 -0
- package/docs-site/content/docs/api-reference/ecosystem.mdx +9 -0
- package/docs-site/content/docs/api-reference/epics.mdx +9 -0
- package/docs-site/content/docs/api-reference/index.mdx +32 -0
- package/docs-site/content/docs/api-reference/memory.mdx +9 -0
- package/docs-site/content/docs/api-reference/meta.json +25 -0
- package/docs-site/content/docs/api-reference/poll.mdx +9 -0
- package/docs-site/content/docs/api-reference/repos.mdx +9 -0
- package/docs-site/content/docs/api-reference/schedules.mdx +9 -0
- package/docs-site/content/docs/api-reference/session-data.mdx +9 -0
- package/docs-site/content/docs/api-reference/stats.mdx +9 -0
- package/docs-site/content/docs/api-reference/tasks.mdx +9 -0
- package/docs-site/content/docs/api-reference/trackers.mdx +9 -0
- package/docs-site/content/docs/api-reference/webhooks.mdx +9 -0
- package/docs-site/content/docs/api-reference/workflows.mdx +9 -0
- package/docs-site/content/docs/meta.json +3 -0
- package/docs-site/lib/get-llm-text.ts +10 -0
- package/docs-site/lib/openapi.ts +23 -0
- package/docs-site/lib/source.ts +8 -0
- package/docs-site/mdx-components.tsx +13 -0
- package/docs-site/next.config.mjs +29 -0
- package/docs-site/package.json +35 -0
- package/docs-site/pnpm-lock.yaml +5407 -0
- package/docs-site/postcss.config.mjs +8 -0
- package/docs-site/scripts/generate-docs.ts +171 -0
- package/docs-site/source.config.ts +17 -0
- package/docs-site/tsconfig.json +46 -0
- package/ecosystem.config.cjs +4 -4
- package/landing/next.config.ts +14 -0
- package/landing/package.json +31 -0
- package/landing/pnpm-lock.yaml +1091 -0
- package/landing/postcss.config.mjs +8 -0
- package/landing/public/apple-touch-icon.png +0 -0
- package/landing/public/favicon.ico +0 -0
- package/landing/public/logo.png +0 -0
- package/landing/public/og-image.png +0 -0
- package/landing/public/omghost-desplega.svg +30 -0
- package/landing/public/omghost-openfort.svg +9 -0
- package/landing/src/app/actions/waitlist.ts +25 -0
- package/landing/src/app/blog/openfort-hackathon/page.tsx +863 -0
- package/landing/src/app/blog/page.tsx +162 -0
- package/landing/src/app/blog/swarm-metrics/page.tsx +685 -0
- package/landing/src/app/examples/page.tsx +174 -0
- package/landing/src/app/examples/x402/page.tsx +456 -0
- package/landing/src/app/globals.css +122 -0
- package/landing/src/app/layout.tsx +134 -0
- package/landing/src/app/page.tsx +27 -0
- package/landing/src/app/robots.ts +13 -0
- package/landing/src/app/sitemap.ts +44 -0
- package/landing/src/components/architecture.tsx +163 -0
- package/landing/src/components/cta.tsx +52 -0
- package/landing/src/components/features.tsx +160 -0
- package/landing/src/components/footer.tsx +100 -0
- package/landing/src/components/hero.tsx +217 -0
- package/landing/src/components/how-it-works.tsx +165 -0
- package/landing/src/components/navbar.tsx +147 -0
- package/landing/src/components/waitlist.tsx +110 -0
- package/landing/src/components/why-choose.tsx +149 -0
- package/landing/src/components/workshops.tsx +328 -0
- package/landing/src/lib/utils.ts +6 -0
- package/landing/tsconfig.json +41 -0
- package/misc/transcripts/2026-03-09-pi-mono-e2e-verification.md +154 -0
- package/new-ui/CLAUDE.md +92 -0
- package/new-ui/README.md +73 -0
- package/new-ui/biome.json +42 -0
- package/new-ui/components.json +21 -0
- package/new-ui/index.html +25 -0
- package/new-ui/package.json +49 -0
- package/new-ui/pnpm-lock.yaml +4845 -0
- package/new-ui/public/logo.png +0 -0
- package/new-ui/src/api/client.ts +814 -0
- package/new-ui/src/api/hooks/index.ts +64 -0
- package/new-ui/src/api/hooks/use-agents.ts +58 -0
- package/new-ui/src/api/hooks/use-channels.ts +115 -0
- package/new-ui/src/api/hooks/use-config-api.ts +46 -0
- package/new-ui/src/api/hooks/use-costs.ts +122 -0
- package/new-ui/src/api/hooks/use-db-query.ts +29 -0
- package/new-ui/src/api/hooks/use-epics.ts +75 -0
- package/new-ui/src/api/hooks/use-repos.ts +61 -0
- package/new-ui/src/api/hooks/use-schedules.ts +81 -0
- package/new-ui/src/api/hooks/use-services.ts +16 -0
- package/new-ui/src/api/hooks/use-stats.ts +27 -0
- package/new-ui/src/api/hooks/use-tasks.ts +89 -0
- package/new-ui/src/api/hooks/use-workflows.ts +109 -0
- package/{ui/src/types/api.ts → new-ui/src/api/types.ts} +236 -10
- package/new-ui/src/app/App.tsx +13 -0
- package/new-ui/src/app/providers.tsx +32 -0
- package/new-ui/src/app/router.tsx +52 -0
- package/new-ui/src/components/layout/app-header.tsx +47 -0
- package/new-ui/src/components/layout/app-sidebar.tsx +128 -0
- package/new-ui/src/components/layout/breadcrumbs.tsx +57 -0
- package/new-ui/src/components/layout/config-guard.tsx +22 -0
- package/new-ui/src/components/layout/root-layout.tsx +40 -0
- package/new-ui/src/components/layout/swarm-switcher.tsx +85 -0
- package/new-ui/src/components/shared/command-menu.tsx +131 -0
- package/new-ui/src/components/shared/data-grid.tsx +141 -0
- package/new-ui/src/components/shared/empty-state.tsx +24 -0
- package/new-ui/src/components/shared/error-boundary.tsx +72 -0
- package/new-ui/src/components/shared/json-viewer.tsx +47 -0
- package/new-ui/src/components/shared/name-connection-modal.tsx +99 -0
- package/new-ui/src/components/shared/page-skeleton.tsx +16 -0
- package/new-ui/src/components/shared/session-log-viewer.tsx +364 -0
- package/new-ui/src/components/shared/stats-bar.tsx +132 -0
- package/new-ui/src/components/shared/status-badge.tsx +131 -0
- package/new-ui/src/components/shared/usage-summary.tsx +179 -0
- package/new-ui/src/components/ui/alert-dialog.tsx +176 -0
- package/new-ui/src/components/ui/alert.tsx +60 -0
- package/new-ui/src/components/ui/avatar.tsx +96 -0
- package/new-ui/src/components/ui/badge.tsx +46 -0
- package/new-ui/src/components/ui/button.tsx +62 -0
- package/new-ui/src/components/ui/card.tsx +75 -0
- package/new-ui/src/components/ui/command.tsx +160 -0
- package/new-ui/src/components/ui/dialog.tsx +143 -0
- package/new-ui/src/components/ui/dropdown-menu.tsx +226 -0
- package/new-ui/src/components/ui/input.tsx +21 -0
- package/new-ui/src/components/ui/label.tsx +19 -0
- package/new-ui/src/components/ui/progress.tsx +26 -0
- package/new-ui/src/components/ui/scroll-area.tsx +54 -0
- package/new-ui/src/components/ui/select.tsx +175 -0
- package/new-ui/src/components/ui/separator.tsx +28 -0
- package/new-ui/src/components/ui/sheet.tsx +132 -0
- package/new-ui/src/components/ui/sidebar.tsx +691 -0
- package/new-ui/src/components/ui/skeleton.tsx +13 -0
- package/new-ui/src/components/ui/sonner.tsx +35 -0
- package/new-ui/src/components/ui/switch.tsx +33 -0
- package/new-ui/src/components/ui/table.tsx +92 -0
- package/new-ui/src/components/ui/tabs.tsx +79 -0
- package/new-ui/src/components/ui/textarea.tsx +18 -0
- package/new-ui/src/components/ui/tooltip.tsx +51 -0
- package/new-ui/src/components/workflows/action-node.tsx +53 -0
- package/new-ui/src/components/workflows/condition-node.tsx +50 -0
- package/new-ui/src/components/workflows/graph-utils.ts +124 -0
- package/new-ui/src/components/workflows/json-tree.tsx +189 -0
- package/new-ui/src/components/workflows/node-styles.ts +10 -0
- package/new-ui/src/components/workflows/step-detail-sheet.tsx +87 -0
- package/new-ui/src/components/workflows/trigger-node.tsx +41 -0
- package/new-ui/src/components/workflows/workflow-graph.tsx +65 -0
- package/{ui/src/hooks/useAutoScroll.ts → new-ui/src/hooks/use-auto-scroll.ts} +0 -1
- package/new-ui/src/hooks/use-config.ts +203 -0
- package/new-ui/src/hooks/use-keyboard-shortcuts.ts +41 -0
- package/new-ui/src/hooks/use-mobile.ts +19 -0
- package/new-ui/src/hooks/use-theme.ts +60 -0
- package/new-ui/src/lib/config.ts +188 -0
- package/new-ui/src/lib/slugs.ts +71 -0
- package/{ui → new-ui}/src/lib/utils.ts +32 -0
- package/new-ui/src/main.tsx +11 -0
- package/new-ui/src/pages/agents/[id]/page.tsx +492 -0
- package/new-ui/src/pages/agents/page.tsx +134 -0
- package/new-ui/src/pages/chat/page.tsx +674 -0
- package/new-ui/src/pages/config/page.tsx +1109 -0
- package/new-ui/src/pages/dashboard/page.tsx +454 -0
- package/new-ui/src/pages/debug/page.tsx +275 -0
- package/new-ui/src/pages/epics/[id]/page.tsx +809 -0
- package/new-ui/src/pages/epics/page.tsx +321 -0
- package/new-ui/src/pages/not-found/page.tsx +18 -0
- package/new-ui/src/pages/repos/page.tsx +369 -0
- package/new-ui/src/pages/schedules/[id]/page.tsx +664 -0
- package/new-ui/src/pages/schedules/page.tsx +477 -0
- package/new-ui/src/pages/services/page.tsx +128 -0
- package/new-ui/src/pages/tasks/[id]/page.tsx +670 -0
- package/new-ui/src/pages/tasks/page.tsx +592 -0
- package/new-ui/src/pages/usage/page.tsx +195 -0
- package/new-ui/src/pages/workflow-runs/[id]/page.tsx +363 -0
- package/new-ui/src/pages/workflows/[id]/page.tsx +417 -0
- package/new-ui/src/pages/workflows/page.tsx +266 -0
- package/new-ui/src/styles/ag-grid.css +36 -0
- package/new-ui/src/styles/globals.css +213 -0
- package/new-ui/test-results/.last-run.json +4 -0
- package/{ui/tsconfig.json → new-ui/tsconfig.app.json} +7 -4
- package/new-ui/tsconfig.json +4 -0
- package/new-ui/tsconfig.node.json +26 -0
- package/new-ui/vercel.json +4 -0
- package/new-ui/vite.config.ts +28 -0
- package/openapi.json +4436 -0
- package/package.json +31 -7
- package/plugin/build-pi-skills.ts +233 -0
- package/plugin/commands/close-issue.md +7 -3
- package/plugin/commands/create-pr.md +18 -12
- package/plugin/commands/implement-issue.md +7 -3
- package/plugin/commands/respond-github.md +8 -4
- package/plugin/commands/review-pr.md +44 -10
- package/plugin/commands/start-leader.md +1 -3
- package/plugin/commands/start-worker.md +1 -3
- package/plugin/commands/work-on-task.md +22 -3
- package/plugin/pi-skills/close-issue/SKILL.md +90 -0
- package/plugin/pi-skills/create-pr/SKILL.md +99 -0
- package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
- package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
- package/plugin/pi-skills/respond-github/SKILL.md +98 -0
- package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
- package/plugin/pi-skills/review-pr/SKILL.md +261 -0
- package/plugin/pi-skills/start-leader/SKILL.md +121 -0
- package/plugin/pi-skills/start-worker/SKILL.md +60 -0
- package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
- package/plugin/pi-skills/todos/SKILL.md +66 -0
- package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
- package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
- package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
- package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
- package/plugin/skills/artifacts/examples/static-report.sh +17 -0
- package/plugin/skills/artifacts/skill.md +71 -0
- package/prek.toml +75 -0
- package/scripts/check-db-boundary.sh +60 -0
- package/scripts/e2e-docker-provider.ts +820 -0
- package/scripts/e2e-io-schemas-test.ts +807 -0
- package/scripts/e2e-provider-test.ts +220 -0
- package/scripts/e2e-workflow-redesign.sh +229 -0
- package/scripts/e2e-workflow-test.sh +285 -0
- package/scripts/e2e-workflow-test.ts +857 -0
- package/scripts/generate-openapi.ts +26 -0
- package/scripts/measure-tool-tokens.ts +118 -0
- package/scripts/x402-e2e-test.ts +195 -0
- package/scripts/x402-test-server.ts +236 -0
- package/scripts/x402-testnet-e2e.ts +668 -0
- package/slack-manifest.json +17 -0
- package/src/agentmail/app.ts +65 -0
- package/src/agentmail/handlers.ts +207 -0
- package/src/agentmail/index.ts +9 -0
- package/src/agentmail/types.ts +51 -0
- package/src/artifact-sdk/browser-sdk.ts +30 -0
- package/src/artifact-sdk/index.ts +2 -0
- package/src/artifact-sdk/localtunnel.d.ts +20 -0
- package/src/artifact-sdk/port.ts +12 -0
- package/src/artifact-sdk/server.ts +156 -0
- package/src/artifact-sdk/tunnel.ts +19 -0
- package/src/be/chunking.ts +193 -0
- package/src/be/db-queries/oauth.ts +90 -0
- package/src/be/db-queries/tracker.ts +182 -0
- package/src/be/db.ts +2729 -770
- package/src/be/embedding.ts +80 -0
- package/src/be/migrations/001_initial.sql +409 -0
- package/src/be/migrations/002_one_time_schedules.sql +59 -0
- package/src/be/migrations/003_workflows.sql +51 -0
- package/src/be/migrations/004_workflow_source.sql +81 -0
- package/src/be/migrations/005_epic_next_steps.sql +2 -0
- package/src/be/migrations/006_vcs_provider.sql +94 -0
- package/src/be/migrations/007_task_dir.sql +2 -0
- package/src/be/migrations/008_workflow_redesign.sql +85 -0
- package/src/be/migrations/009_tracker_integration.sql +144 -0
- package/src/be/migrations/010_step_diagnostics.sql +1 -0
- package/src/be/migrations/011_step_next_port.sql +1 -0
- package/src/be/migrations/012_trigger_schema.sql +1 -0
- package/src/be/migrations/013_task_output_schema.sql +2 -0
- package/src/be/migrations/runner.ts +188 -0
- package/src/cli.tsx +231 -299
- package/src/commands/artifact.ts +241 -0
- package/src/commands/onboard/compose-generator.ts +169 -0
- package/src/commands/onboard/env-generator.ts +79 -0
- package/src/commands/onboard/manifest.ts +37 -0
- package/src/commands/onboard/presets.ts +85 -0
- package/src/commands/onboard/service-names.ts +47 -0
- package/src/commands/onboard/steps/core-credentials.tsx +111 -0
- package/src/commands/onboard/steps/custom-templates.tsx +168 -0
- package/src/commands/onboard/steps/generate.tsx +154 -0
- package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
- package/src/commands/onboard/steps/harness.tsx +21 -0
- package/src/commands/onboard/steps/health-check.tsx +171 -0
- package/src/commands/onboard/steps/integration-github.tsx +105 -0
- package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
- package/src/commands/onboard/steps/integration-menu.tsx +58 -0
- package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
- package/src/commands/onboard/steps/integration-slack.tsx +165 -0
- package/src/commands/onboard/steps/post-connect.tsx +145 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
- package/src/commands/onboard/steps/post-task.tsx +103 -0
- package/src/commands/onboard/steps/prereq-check.tsx +178 -0
- package/src/commands/onboard/steps/review.tsx +82 -0
- package/src/commands/onboard/steps/start.tsx +97 -0
- package/src/commands/onboard/templates.ts +34 -0
- package/src/commands/onboard/types.ts +259 -0
- package/src/commands/onboard.tsx +425 -0
- package/src/commands/runner.ts +1376 -559
- package/src/commands/setup.tsx +23 -38
- package/src/commands/shared/client-config.ts +41 -0
- package/src/github/app.ts +8 -0
- package/src/github/handlers.ts +93 -75
- package/src/github/index.ts +1 -0
- package/src/github/types.ts +1 -0
- package/src/gitlab/auth.ts +63 -0
- package/src/gitlab/handlers.ts +327 -0
- package/src/gitlab/index.ts +19 -0
- package/src/gitlab/reactions.ts +104 -0
- package/src/gitlab/types.ts +130 -0
- package/src/heartbeat/heartbeat.ts +425 -0
- package/src/heartbeat/index.ts +1 -0
- package/src/hooks/hook.ts +555 -4
- package/src/hooks/tool-loop-detection.test.ts +158 -0
- package/src/hooks/tool-loop-detection.ts +167 -0
- package/src/http/active-sessions.ts +172 -0
- package/src/http/agents.ts +328 -0
- package/src/http/config.ts +191 -0
- package/src/http/core.ts +309 -0
- package/src/http/db-query.ts +91 -0
- package/src/http/ecosystem.ts +63 -0
- package/src/http/epics.ts +460 -0
- package/src/http/index.ts +213 -0
- package/src/http/mcp.ts +77 -0
- package/src/http/memory.ts +168 -0
- package/src/http/openapi.ts +109 -0
- package/src/http/poll.ts +178 -0
- package/src/http/repos.ts +195 -0
- package/src/http/route-def.ts +123 -0
- package/src/http/schedules.ts +391 -0
- package/src/http/session-data.ts +241 -0
- package/src/http/stats.ts +174 -0
- package/src/http/tasks.ts +468 -0
- package/src/http/trackers/index.ts +10 -0
- package/src/http/trackers/linear.ts +187 -0
- package/src/http/types.ts +12 -0
- package/src/http/utils.ts +87 -0
- package/src/http/webhooks.ts +432 -0
- package/src/http/workflows.ts +522 -0
- package/src/http.ts +1 -1890
- package/src/linear/README.md +65 -0
- package/src/linear/app.ts +48 -0
- package/src/linear/client.ts +18 -0
- package/src/linear/index.ts +1 -0
- package/src/linear/oauth.ts +35 -0
- package/src/linear/outbound.ts +212 -0
- package/src/linear/sync.ts +543 -0
- package/src/linear/types.ts +7 -0
- package/src/linear/webhook.ts +104 -0
- package/src/oauth/README.md +66 -0
- package/src/oauth/index.ts +6 -0
- package/src/oauth/wrapper.ts +204 -0
- package/src/prompts/base-prompt.ts +366 -24
- package/src/prompts/defaults.ts +196 -0
- package/src/providers/claude-adapter.ts +429 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/pi-mono-adapter.ts +442 -0
- package/src/providers/pi-mono-extension.ts +620 -0
- package/src/providers/pi-mono-mcp-client.ts +124 -0
- package/src/providers/types.ts +75 -0
- package/src/scheduler/scheduler.test.ts +2 -0
- package/src/scheduler/scheduler.ts +186 -29
- package/src/server.ts +82 -6
- package/src/slack/HEURISTICS.md +105 -0
- package/src/slack/actions.ts +133 -0
- package/src/slack/app.ts +7 -0
- package/src/slack/assistant.ts +115 -0
- package/src/slack/blocks.ts +233 -0
- package/src/slack/commands.ts +31 -17
- package/src/slack/files.ts +1 -1
- package/src/slack/handlers.test.ts +114 -1
- package/src/slack/handlers.ts +212 -52
- package/src/slack/responses.ts +120 -67
- package/src/slack/router.ts +17 -99
- package/src/slack/thread-buffer.ts +213 -0
- package/src/slack/watcher.ts +119 -4
- package/src/tests/agent-activity.test.ts +247 -0
- package/src/tests/agentmail-filters.test.ts +97 -0
- package/src/tests/artifact-sdk.test.ts +800 -0
- package/src/tests/base-prompt.test.ts +264 -0
- package/src/tests/build-pi-skills.test.ts +127 -0
- package/src/tests/claude-adapter.test.ts +126 -0
- package/src/tests/context-versioning.test.ts +425 -0
- package/src/tests/db-queries-oauth.test.ts +197 -0
- package/src/tests/db-queries-tracker.test.ts +230 -0
- package/src/tests/epics.test.ts +3 -3
- package/src/tests/error-tracker.test.ts +368 -0
- package/src/tests/fetch-resolved-env.test.ts +167 -0
- package/src/tests/generate-default-claude-md.test.ts +9 -1
- package/src/tests/generate-identity-templates.test.ts +124 -0
- package/src/tests/gitlab-auth.test.ts +109 -0
- package/src/tests/gitlab-handlers.test.ts +691 -0
- package/src/tests/gitlab-vcs-db.test.ts +177 -0
- package/src/tests/heartbeat.test.ts +363 -0
- package/src/tests/http-api-integration.test.ts +1698 -0
- package/src/tests/linear-outbound-sync.test.ts +200 -0
- package/src/tests/linear-webhook.test.ts +402 -0
- package/src/tests/match-route.test.ts +187 -0
- package/src/tests/memory.test.ts +737 -0
- package/src/tests/migration-runner-regressions.test.ts +86 -0
- package/src/tests/model-control.test.ts +338 -0
- package/src/tests/oauth-wrapper.test.ts +147 -0
- package/src/tests/onboard-compose.test.ts +138 -0
- package/src/tests/onboard-env.test.ts +174 -0
- package/src/tests/onboard-manifest.test.ts +137 -0
- package/src/tests/pi-mono-adapter.test.ts +234 -0
- package/src/tests/progress-dedup.test.ts +98 -0
- package/src/tests/provider-adapter.test.ts +122 -0
- package/src/tests/provider-command-format.test.ts +98 -0
- package/src/tests/reload-config.test.ts +170 -0
- package/src/tests/runner-polling-api.test.ts +25 -20
- package/src/tests/scheduled-tasks.test.ts +104 -0
- package/src/tests/scheduler-backoff.test.ts +166 -0
- package/src/tests/self-improvement.test.ts +540 -0
- package/src/tests/session-attach.test.ts +536 -0
- package/src/tests/session-costs.test.ts +267 -1
- package/src/tests/slack-actions.test.ts +133 -0
- package/src/tests/slack-assistant.test.ts +136 -0
- package/src/tests/slack-blocks.test.ts +246 -0
- package/src/tests/slack-metadata-inheritance.test.ts +243 -0
- package/src/tests/slack-queue-offline.test.ts +174 -0
- package/src/tests/slack-router.test.ts +181 -0
- package/src/tests/slack-thread-buffer.test.ts +305 -0
- package/src/tests/slack-thread-followups.test.ts +298 -0
- package/src/tests/slack-watcher.test.ts +101 -0
- package/src/tests/structured-output.test.ts +311 -0
- package/src/tests/swarm-repos.test.ts +198 -0
- package/src/tests/task-cancellation.test.ts +6 -4
- package/src/tests/task-working-dir.test.ts +176 -0
- package/src/tests/template-fetch.test.ts +490 -0
- package/src/tests/tool-annotations.test.ts +371 -0
- package/src/tests/tracker-tools.test.ts +184 -0
- package/src/tests/update-profile-api.test.ts +143 -3
- package/src/tests/validation-adapters.test.ts +86 -0
- package/src/tests/vcs-provider.test.ts +27 -0
- package/src/tests/workflow-agent-task.test.ts +196 -0
- package/src/tests/workflow-async-v2.test.ts +386 -0
- package/src/tests/workflow-convergence.test.ts +541 -0
- package/src/tests/workflow-definition-validation.test.ts +290 -0
- package/src/tests/workflow-engine-v2.test.ts +691 -0
- package/src/tests/workflow-executors.test.ts +736 -0
- package/src/tests/workflow-http-v2.test.ts +599 -0
- package/src/tests/workflow-integration-io.test.ts +902 -0
- package/src/tests/workflow-io-schemas.test.ts +624 -0
- package/src/tests/workflow-registry.test.ts +592 -0
- package/src/tests/workflow-retry-v2.test.ts +401 -0
- package/src/tests/workflow-retry-validation.test.ts +282 -0
- package/src/tests/workflow-template.test.ts +288 -0
- package/src/tests/workflow-trigger-schema.test.ts +359 -0
- package/src/tests/workflow-triggers-v2.test.ts +264 -0
- package/src/tests/workflow-versions.test.ts +208 -0
- package/src/tests/x402-client.test.ts +117 -0
- package/src/tests/x402-config.test.ts +182 -0
- package/src/tests/x402-spending-tracker.test.ts +185 -0
- package/src/tools/cancel-task.ts +2 -0
- package/src/tools/context-diff.ts +171 -0
- package/src/tools/context-history.ts +138 -0
- package/src/tools/create-channel.ts +1 -0
- package/src/tools/db-query.ts +78 -0
- package/src/tools/delete-channel.ts +132 -0
- package/src/tools/epics/assign-task-to-epic.ts +1 -0
- package/src/tools/epics/create-epic.ts +3 -2
- package/src/tools/epics/delete-epic.ts +2 -0
- package/src/tools/epics/get-epic-details.ts +2 -0
- package/src/tools/epics/list-epics.ts +2 -0
- package/src/tools/epics/unassign-task-from-epic.ts +1 -0
- package/src/tools/epics/update-epic.ts +7 -4
- package/src/tools/get-swarm.ts +2 -0
- package/src/tools/get-task-details.ts +2 -0
- package/src/tools/get-tasks.ts +27 -1
- package/src/tools/inject-learning.ts +106 -0
- package/src/tools/join-swarm.ts +17 -7
- package/src/tools/list-channels.ts +2 -0
- package/src/tools/list-services.ts +2 -0
- package/src/tools/memory-get.ts +56 -0
- package/src/tools/memory-search.ts +131 -0
- package/src/tools/my-agent-info.ts +2 -0
- package/src/tools/poll-task.ts +2 -20
- package/src/tools/post-message.ts +1 -0
- package/src/tools/read-messages.ts +2 -0
- package/src/tools/register-agentmail-inbox.ts +166 -0
- package/src/tools/register-service.ts +2 -0
- package/src/tools/schedules/create-schedule.ts +134 -24
- package/src/tools/schedules/delete-schedule.ts +2 -0
- package/src/tools/schedules/list-schedules.ts +20 -4
- package/src/tools/schedules/run-schedule-now.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +49 -17
- package/src/tools/send-task.ts +132 -10
- package/src/tools/slack-download-file.ts +4 -2
- package/src/tools/slack-list-channels.ts +2 -0
- package/src/tools/slack-post.ts +2 -0
- package/src/tools/slack-read.ts +2 -0
- package/src/tools/slack-reply.ts +2 -0
- package/src/tools/slack-upload-file.ts +2 -0
- package/src/tools/store-progress.ts +187 -4
- package/src/tools/swarm-config/delete-config.ts +87 -0
- package/src/tools/swarm-config/get-config.ts +108 -0
- package/src/tools/swarm-config/index.ts +4 -0
- package/src/tools/swarm-config/list-config.ts +99 -0
- package/src/tools/swarm-config/set-config.ts +118 -0
- package/src/tools/task-action.ts +30 -5
- package/src/tools/task-dedup.ts +97 -0
- package/src/tools/tool-config.ts +117 -0
- package/src/tools/tracker/index.ts +6 -0
- package/src/tools/tracker/tracker-link-epic.ts +64 -0
- package/src/tools/tracker/tracker-link-task.ts +64 -0
- package/src/tools/tracker/tracker-map-agent.ts +57 -0
- package/src/tools/tracker/tracker-status.ts +56 -0
- package/src/tools/tracker/tracker-sync-status.ts +42 -0
- package/src/tools/tracker/tracker-unlink.ts +41 -0
- package/src/tools/unregister-service.ts +2 -0
- package/src/tools/update-profile.ts +93 -10
- package/src/tools/update-service-status.ts +2 -0
- package/src/tools/utils.ts +10 -1
- package/src/tools/workflows/create-workflow.ts +111 -0
- package/src/tools/workflows/delete-workflow.ts +42 -0
- package/src/tools/workflows/get-workflow-run.ts +59 -0
- package/src/tools/workflows/get-workflow.ts +53 -0
- package/src/tools/workflows/index.ts +9 -0
- package/src/tools/workflows/list-workflow-runs.ts +48 -0
- package/src/tools/workflows/list-workflows.ts +42 -0
- package/src/tools/workflows/retry-workflow-run.ts +40 -0
- package/src/tools/workflows/trigger-workflow.ts +96 -0
- package/src/tools/workflows/update-workflow.ts +118 -0
- package/src/tracker/types.ts +51 -0
- package/src/types.ts +477 -14
- package/src/utils/credentials.test.ts +156 -0
- package/src/utils/credentials.ts +50 -0
- package/src/utils/error-tracker.ts +190 -0
- package/src/vcs/index.ts +15 -0
- package/src/vcs/types.ts +5 -0
- package/src/workflows/checkpoint.ts +121 -0
- package/src/workflows/cooldown.ts +28 -0
- package/src/workflows/definition.ts +217 -0
- package/src/workflows/engine.ts +554 -0
- package/src/workflows/event-bus.ts +29 -0
- package/src/workflows/executors/agent-task.ts +92 -0
- package/src/workflows/executors/base.ts +86 -0
- package/src/workflows/executors/code-match.ts +88 -0
- package/src/workflows/executors/index.ts +16 -0
- package/src/workflows/executors/notify.ts +93 -0
- package/src/workflows/executors/property-match.ts +104 -0
- package/src/workflows/executors/raw-llm.ts +83 -0
- package/src/workflows/executors/registry.ts +76 -0
- package/src/workflows/executors/script.ts +103 -0
- package/src/workflows/executors/validate.ts +215 -0
- package/src/workflows/executors/vcs.ts +58 -0
- package/src/workflows/index.ts +61 -0
- package/src/workflows/input.ts +46 -0
- package/src/workflows/json-schema-validator.ts +118 -0
- package/src/workflows/recovery.ts +139 -0
- package/src/workflows/resume.ts +143 -0
- package/src/workflows/retry-poller.ts +216 -0
- package/src/workflows/template.ts +74 -0
- package/src/workflows/templates.ts +86 -0
- package/src/workflows/triggers.ts +95 -0
- package/src/workflows/validation.ts +104 -0
- package/src/workflows/version.ts +42 -0
- package/src/x402/cli.ts +140 -0
- package/src/x402/client.ts +192 -0
- package/src/x402/config.ts +131 -0
- package/src/x402/index.ts +37 -0
- package/src/x402/openfort-signer.ts +83 -0
- package/src/x402/spending-tracker.ts +109 -0
- package/templates/community/.gitkeep +0 -0
- package/templates/official/coder/CLAUDE.md +49 -0
- package/templates/official/coder/IDENTITY.md +28 -0
- package/templates/official/coder/SOUL.md +43 -0
- package/templates/official/coder/TOOLS.md +40 -0
- package/templates/official/coder/config.json +23 -0
- package/templates/official/coder/start-up.sh +23 -0
- package/templates/official/content-reviewer/CLAUDE.md +68 -0
- package/templates/official/content-reviewer/IDENTITY.md +28 -0
- package/templates/official/content-reviewer/SOUL.md +44 -0
- package/templates/official/content-reviewer/TOOLS.md +37 -0
- package/templates/official/content-reviewer/config.json +23 -0
- package/templates/official/content-reviewer/start-up.sh +23 -0
- package/templates/official/content-strategist/CLAUDE.md +63 -0
- package/templates/official/content-strategist/IDENTITY.md +33 -0
- package/templates/official/content-strategist/SOUL.md +48 -0
- package/templates/official/content-strategist/TOOLS.md +47 -0
- package/templates/official/content-strategist/config.json +23 -0
- package/templates/official/content-strategist/start-up.sh +23 -0
- package/templates/official/content-writer/CLAUDE.md +72 -0
- package/templates/official/content-writer/IDENTITY.md +30 -0
- package/templates/official/content-writer/SOUL.md +46 -0
- package/templates/official/content-writer/TOOLS.md +44 -0
- package/templates/official/content-writer/config.json +23 -0
- package/templates/official/content-writer/start-up.sh +23 -0
- package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
- package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
- package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
- package/templates/official/forward-deployed-engineer/config.json +21 -0
- package/templates/official/lead/CLAUDE.md +33 -0
- package/templates/official/lead/IDENTITY.md +36 -0
- package/templates/official/lead/SOUL.md +51 -0
- package/templates/official/lead/config.json +22 -0
- package/templates/official/researcher/CLAUDE.md +46 -0
- package/templates/official/researcher/IDENTITY.md +28 -0
- package/templates/official/researcher/SOUL.md +43 -0
- package/templates/official/researcher/config.json +21 -0
- package/templates/official/reviewer/CLAUDE.md +63 -0
- package/templates/official/reviewer/IDENTITY.md +28 -0
- package/templates/official/reviewer/SOUL.md +45 -0
- package/templates/official/reviewer/config.json +21 -0
- package/templates/official/tester/CLAUDE.md +53 -0
- package/templates/official/tester/IDENTITY.md +28 -0
- package/templates/official/tester/SOUL.md +55 -0
- package/templates/official/tester/config.json +21 -0
- package/templates/schema.ts +35 -0
- package/templates-ui/README.md +46 -0
- package/templates-ui/components.json +17 -0
- package/templates-ui/eslint.config.mjs +18 -0
- package/templates-ui/next.config.ts +7 -0
- package/templates-ui/package.json +35 -0
- package/templates-ui/pnpm-lock.yaml +4571 -0
- package/templates-ui/postcss.config.mjs +7 -0
- package/templates-ui/public/file.svg +1 -0
- package/templates-ui/public/globe.svg +1 -0
- package/templates-ui/public/logo.png +0 -0
- package/templates-ui/public/next.svg +1 -0
- package/templates-ui/public/vercel.svg +1 -0
- package/templates-ui/public/window.svg +1 -0
- package/templates-ui/src/app/[category]/[name]/page.tsx +89 -0
- package/templates-ui/src/app/api/templates/[...slug]/route.ts +52 -0
- package/templates-ui/src/app/api/templates/route.ts +18 -0
- package/templates-ui/src/app/builder/page.tsx +37 -0
- package/templates-ui/src/app/globals.css +94 -0
- package/templates-ui/src/app/layout.tsx +79 -0
- package/templates-ui/src/app/page.tsx +38 -0
- package/templates-ui/src/app/robots.ts +11 -0
- package/templates-ui/src/app/sitemap.ts +31 -0
- package/templates-ui/src/components/compose-builder.tsx +442 -0
- package/templates-ui/src/components/compose-preview.tsx +117 -0
- package/templates-ui/src/components/file-preview.tsx +77 -0
- package/templates-ui/src/components/footer.tsx +40 -0
- package/templates-ui/src/components/header.tsx +41 -0
- package/templates-ui/src/components/template-card.tsx +87 -0
- package/templates-ui/src/components/template-detail.tsx +125 -0
- package/templates-ui/src/components/template-gallery.tsx +263 -0
- package/templates-ui/src/components/ui/badge.tsx +36 -0
- package/templates-ui/src/components/ui/button.tsx +57 -0
- package/templates-ui/src/components/ui/card.tsx +76 -0
- package/templates-ui/src/components/ui/separator.tsx +31 -0
- package/templates-ui/src/components/ui/tooltip.tsx +32 -0
- package/templates-ui/src/lib/compose-generator.ts +241 -0
- package/templates-ui/src/lib/templates.ts +137 -0
- package/templates-ui/src/lib/utils.ts +6 -0
- package/templates-ui/tsconfig.json +34 -0
- package/thoughts/research/2026-02-28-openfort-viem-x402-research.md +679 -0
- package/thoughts/research/2026-02-28-x402-payments-research.md +686 -0
- package/thoughts/researcher/plans/2026-02-20-agent-self-improvement-plan.md +282 -0
- package/thoughts/researcher/research/2026-02-20-agent-self-improvement.md +492 -0
- package/thoughts/shared/plans/2026-02-26-mcp-tool-context-reduction.md +282 -0
- package/thoughts/shared/plans/2026-03-02-claude-context-mode-integration.md +328 -0
- package/thoughts/shared/plans/2026-03-02-code-level-heartbeat.md +224 -0
- package/thoughts/shared/research/2026-02-26-cliffy-mcp-tools.md +159 -0
- package/thoughts/shared/research/2026-03-03-database-migration-system-refactor.md +337 -0
- package/thoughts/swarm-researcher/plans/2026-02-23-openclaw-improvements-plan.md +778 -0
- package/thoughts/swarm-researcher/plans/2026-02-26-artifacts-localtunnel-plan.md +1269 -0
- package/thoughts/swarm-researcher/research/2026-02-23-openclaw-vs-agent-swarm-comparison.md +411 -0
- package/thoughts/swarm-researcher/research/2026-02-26-artifacts-localtunnel.md +724 -0
- package/thoughts/taras/brainstorms/2026-03-20-prompt-template-registry.md +443 -0
- package/thoughts/taras/brainstorms/2026-03-20-setup-cli-onboarding.md +307 -0
- package/thoughts/taras/plans/2026-02-20-auto-improvement.md +803 -0
- package/thoughts/taras/plans/2026-02-20-env-management.md +538 -0
- package/thoughts/taras/plans/2026-02-20-memory-system.md +882 -0
- package/thoughts/taras/plans/2026-02-20-repos-knowledge.md +806 -0
- package/thoughts/taras/plans/2026-02-20-session-attach.md +647 -0
- package/thoughts/taras/plans/2026-02-20-worker-identity.md +820 -0
- package/thoughts/taras/plans/2026-02-25-feat-new-ui-visual-redesign-plan.md +768 -0
- package/thoughts/taras/plans/2026-03-04-fix-buildSystemPrompt-missing-fields.md +77 -0
- package/thoughts/taras/plans/2026-03-04-new-ui-missing-actions.md +543 -0
- package/thoughts/taras/plans/2026-03-06-one-time-scheduled-tasks.md +373 -0
- package/thoughts/taras/plans/2026-03-08-memory-self-improvement-enhancements.md +512 -0
- package/thoughts/taras/plans/2026-03-08-pi-mono-provider-implementation.md +919 -0
- package/thoughts/taras/plans/2026-03-09-templates-registry.md +723 -0
- package/thoughts/taras/plans/2026-03-10-task-working-directory.md +371 -0
- package/thoughts/taras/plans/2026-03-11-archil-per-agent-write-strategy.md +621 -0
- package/thoughts/taras/plans/2026-03-12-eliminate-inbox-route-to-tasks.md +61 -0
- package/thoughts/taras/plans/2026-03-12-slack-thread-followup-additive.md +488 -0
- package/thoughts/taras/plans/2026-03-13-slack-ai-improvements.md +644 -0
- package/thoughts/taras/plans/2026-03-16-route-wrapper-openapi.md +636 -0
- package/thoughts/taras/plans/2026-03-17-multi-api-config.md +444 -0
- package/thoughts/taras/plans/2026-03-18-agent-fs-integration.md +591 -0
- package/thoughts/taras/plans/2026-03-18-debug-db-explorer.md +446 -0
- package/thoughts/taras/plans/2026-03-18-workflow-redesign.md +987 -0
- package/thoughts/taras/plans/2026-03-19-compound-learnings.md +403 -0
- package/thoughts/taras/plans/2026-03-19-ticket-tracker-linear-integration.md +860 -0
- package/thoughts/taras/plans/2026-03-19-workflow-io-schemas-and-bugs.md +899 -0
- package/thoughts/taras/plans/2026-03-20-setup-cli-onboarding.md +874 -0
- package/thoughts/taras/plans/2026-03-20-workflow-structured-output-validation-workspace.md +723 -0
- package/thoughts/taras/research/2026-02-19-agent-native-swarm-architecture.md +390 -0
- package/thoughts/taras/research/2026-02-19-swarm-gaps-implementation.md +594 -0
- package/thoughts/taras/research/2026-02-25-dashboard-ui-design-best-practices.md +825 -0
- package/thoughts/taras/research/2026-02-26-task-detail-page-redesign.md +393 -0
- package/thoughts/taras/research/2026-03-03-new-ui-missing-actions.md +168 -0
- package/thoughts/taras/research/2026-03-05-pi-mono-provider-research.md +230 -0
- package/thoughts/taras/research/2026-03-06-workflow-engine-design.md +445 -0
- package/thoughts/taras/research/2026-03-08-drive-loop-concept.md +375 -0
- package/thoughts/taras/research/2026-03-08-pi-mono-deep-dive.md +869 -0
- package/thoughts/taras/research/2026-03-09-templates-registry.md +373 -0
- package/thoughts/taras/research/2026-03-10-agent-working-directory.md +223 -0
- package/thoughts/taras/research/2026-03-10-configurable-event-prompts.md +339 -0
- package/thoughts/taras/research/2026-03-11-archil-production-setup.md +181 -0
- package/thoughts/taras/research/2026-03-11-archil-shared-disk-write-strategies.md +437 -0
- package/thoughts/taras/research/2026-03-13-slack-ai-features.md +258 -0
- package/thoughts/taras/research/2026-03-16-openapi-docs-generation.md +335 -0
- package/thoughts/taras/research/2026-03-16-route-wrapper-openapi.md +670 -0
- package/thoughts/taras/research/2026-03-16-slack-thread-followups-e2e.md +54 -0
- package/thoughts/taras/research/2026-03-18-agent-fs-integration.md +558 -0
- package/thoughts/taras/research/2026-03-18-linear-integration-finalization.md +526 -0
- package/thoughts/taras/research/2026-03-18-workflow-redesign.md +797 -0
- package/thoughts/taras/research/2026-03-19-workflow-node-io-schemas-and-bugs.md +563 -0
- package/thoughts/taras/research/2026-03-19-workflow-structured-output-validation-workspace.md +486 -0
- package/thoughts/taras/research/2026-03-20-prompt-template-registry.md +469 -0
- package/tsconfig.json +1 -1
- package/FAQ.md +0 -19
- package/src/tests/get-inbox-message.test.ts +0 -145
- package/src/tools/get-inbox-message.ts +0 -89
- package/src/tools/inbox-delegate.ts +0 -113
- package/ui/CLAUDE.md +0 -49
- package/ui/bun.lock +0 -771
- package/ui/index.html +0 -22
- package/ui/package-lock.json +0 -5290
- package/ui/package.json +0 -33
- package/ui/pnpm-lock.yaml +0 -3341
- package/ui/postcss.config.js +0 -6
- package/ui/src/App.tsx +0 -63
- package/ui/src/components/ActivityFeed.tsx +0 -440
- package/ui/src/components/AgentDetailPanel.tsx +0 -733
- package/ui/src/components/AgentsPanel.tsx +0 -815
- package/ui/src/components/ChatPanel.tsx +0 -1920
- package/ui/src/components/ConfigModal.tsx +0 -253
- package/ui/src/components/Dashboard.tsx +0 -832
- package/ui/src/components/EditAgentProfileModal.tsx +0 -433
- package/ui/src/components/EpicDetailPage.tsx +0 -741
- package/ui/src/components/EpicsPanel.tsx +0 -566
- package/ui/src/components/Header.tsx +0 -160
- package/ui/src/components/JsonViewer.tsx +0 -171
- package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
- package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
- package/ui/src/components/ServicesPanel.tsx +0 -622
- package/ui/src/components/SessionLogPanel.tsx +0 -1219
- package/ui/src/components/StatsBar.tsx +0 -321
- package/ui/src/components/StatusBadge.tsx +0 -168
- package/ui/src/components/TaskDetailPanel.tsx +0 -903
- package/ui/src/components/TasksPanel.tsx +0 -614
- package/ui/src/components/UsageCharts.tsx +0 -216
- package/ui/src/components/UsageTab.tsx +0 -394
- package/ui/src/hooks/queries.ts +0 -353
- package/ui/src/index.css +0 -257
- package/ui/src/lib/api.ts +0 -268
- package/ui/src/lib/config.ts +0 -35
- package/ui/src/lib/contentPreview.ts +0 -208
- package/ui/src/lib/theme.ts +0 -214
- package/ui/src/main.tsx +0 -28
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -37
- package/ui/vite.config.ts +0 -35
- /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
|
-
//
|
|
43
|
-
|
|
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
|
-
//
|
|
532
|
-
|
|
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:
|
|
546
|
-
//
|
|
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
|
-
|
|
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 = '
|
|
76
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'agent_tasks'",
|
|
552
77
|
)
|
|
553
78
|
.get();
|
|
554
79
|
|
|
555
|
-
const
|
|
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 (
|
|
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
|
|
90
|
+
CREATE TABLE agent_tasks_new (
|
|
565
91
|
id TEXT PRIMARY KEY,
|
|
566
|
-
agentId TEXT
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
status TEXT NOT NULL DEFAULT '
|
|
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
|
-
|
|
579
|
-
|
|
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
|
-
|
|
584
|
-
db.run(
|
|
585
|
-
|
|
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
|
-
|
|
588
|
-
db.run("
|
|
589
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
603
|
-
|
|
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
|
-
//
|
|
208
|
+
// Context Versioning
|
|
663
209
|
// ============================================================================
|
|
664
210
|
|
|
665
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
259
|
+
function rowToContextVersion(row: ContextVersionRow): ContextVersion {
|
|
681
260
|
return {
|
|
682
261
|
id: row.id,
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
if (!row) throw new Error("Failed to create context version");
|
|
324
|
+
return rowToContextVersion(row);
|
|
325
|
+
}
|
|
706
326
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
713
|
-
}
|
|
337
|
+
return row ? rowToContextVersion(row) : null;
|
|
338
|
+
}
|
|
714
339
|
|
|
715
|
-
export function
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
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
|
|
1109
|
-
* Returns the most recent non-completed/failed task for this
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
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
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1303
|
+
return row ? rowToAgentTask(row) : null;
|
|
1304
|
+
}
|
|
1413
1305
|
|
|
1414
|
-
|
|
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
|
|
1466
|
-
|
|
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
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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?.
|
|
1822
|
-
options?.
|
|
1823
|
-
options?.
|
|
1824
|
-
options?.
|
|
1825
|
-
options?.
|
|
1826
|
-
options?.
|
|
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
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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 = '
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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
|
|
2181
|
-
if (!agent) return null;
|
|
2207
|
+
const database = getDb();
|
|
2182
2208
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
AgentRow,
|
|
2187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3714
|
-
|
|
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
|
-
|
|
3741
|
-
|
|
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
|
-
|
|
3818
|
-
|
|
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,
|
|
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.
|
|
3862
|
-
data.
|
|
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
|
-
|
|
3902
|
-
|
|
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.
|
|
3977
|
-
updates.push("
|
|
3978
|
-
params.push(data.
|
|
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.
|
|
3981
|
-
updates.push("
|
|
3982
|
-
params.push(data.
|
|
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
|
+
}
|