@desplega.ai/agent-swarm 1.20.0 → 1.51.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +271 -169
- package/openapi.json +5015 -0
- package/package.json +40 -7
- 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/src/agentmail/app.ts +65 -0
- package/src/agentmail/handlers.ts +262 -0
- package/src/agentmail/index.ts +9 -0
- package/src/agentmail/templates.ts +111 -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 +3327 -784
- 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/014_prompt_templates.sql +33 -0
- package/src/be/migrations/015_workflow_workspace.sql +3 -0
- package/src/be/migrations/016_active_session_runner_session.sql +4 -0
- package/src/be/migrations/017_channel_activity_cursors.sql +6 -0
- package/src/be/migrations/018_fix_seed_double_version.sql +30 -0
- package/src/be/migrations/runner.ts +188 -0
- package/src/be/seed.ts +62 -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 +1540 -630
- package/src/commands/setup.tsx +23 -38
- package/src/commands/shared/client-config.ts +41 -0
- package/src/commands/templates.ts +172 -0
- package/src/github/app.ts +8 -0
- package/src/github/handlers.ts +384 -151
- package/src/github/index.ts +1 -0
- package/src/github/mentions-aliases.test.ts +73 -0
- package/src/github/mentions.test.ts +3 -3
- package/src/github/mentions.ts +32 -6
- package/src/github/templates.ts +398 -0
- package/src/github/types.ts +1 -0
- package/src/gitlab/auth.ts +63 -0
- package/src/gitlab/handlers.ts +368 -0
- package/src/gitlab/index.ts +19 -0
- package/src/gitlab/reactions.ts +104 -0
- package/src/gitlab/templates.ts +140 -0
- package/src/gitlab/types.ts +130 -0
- package/src/heartbeat/heartbeat.ts +434 -0
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/templates.ts +30 -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 +199 -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 +216 -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 +299 -0
- package/src/http/prompt-templates.ts +412 -0
- package/src/http/repos.ts +195 -0
- package/src/http/route-def.ts +123 -0
- package/src/http/schedules.ts +426 -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 +530 -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 +567 -0
- package/src/linear/templates.ts +47 -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 +150 -265
- package/src/prompts/defaults.ts +196 -0
- package/src/prompts/registry.ts +57 -0
- package/src/prompts/resolver.ts +296 -0
- package/src/prompts/session-templates.ts +604 -0
- package/src/providers/claude-adapter.ts +442 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/pi-mono-adapter.ts +442 -0
- package/src/providers/pi-mono-extension.ts +624 -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 +231 -40
- package/src/server.ts +97 -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 +118 -0
- package/src/slack/blocks.ts +233 -0
- package/src/slack/channel-activity.ts +177 -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 +230 -55
- package/src/slack/responses.ts +120 -67
- package/src/slack/router.ts +17 -99
- package/src/slack/templates.ts +55 -0
- 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/channel-activity.test.ts +363 -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 +364 -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 +406 -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/pool-session-logs.test.ts +199 -0
- package/src/tests/progress-dedup.test.ts +98 -0
- package/src/tests/prompt-template-github.test.ts +682 -0
- package/src/tests/prompt-template-remaining.test.ts +504 -0
- package/src/tests/prompt-template-resolver.test.ts +621 -0
- package/src/tests/prompt-template-session.test.ts +363 -0
- package/src/tests/prompt-templates-db.test.ts +616 -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 +541 -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 +307 -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-agentid.test.ts +248 -0
- package/src/tests/update-profile-api.test.ts +143 -3
- package/src/tests/update-profile-auth.test.ts +195 -0
- 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 +508 -0
- package/src/tests/workflow-convergence.test.ts +541 -0
- package/src/tests/workflow-definition-validation.test.ts +366 -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-schedule-trigger.test.ts +104 -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/workflow-workspace.test.ts +272 -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/prompt-templates/delete.ts +86 -0
- package/src/tools/prompt-templates/get.ts +89 -0
- package/src/tools/prompt-templates/index.ts +5 -0
- package/src/tools/prompt-templates/list.ts +95 -0
- package/src/tools/prompt-templates/preview.ts +84 -0
- package/src/tools/prompt-templates/set.ts +117 -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 +205 -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 +50 -5
- package/src/tools/task-dedup.ts +97 -0
- package/src/tools/templates.ts +53 -0
- package/src/tools/tool-config.ts +124 -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 +172 -17
- package/src/tools/update-service-status.ts +2 -0
- package/src/tools/utils.ts +10 -1
- package/src/tools/workflows/create-workflow.ts +129 -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 +133 -0
- package/src/tracker/types.ts +51 -0
- package/src/types.ts +530 -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 +235 -0
- package/src/workflows/engine.ts +580 -0
- package/src/workflows/event-bus.ts +29 -0
- package/src/workflows/executors/agent-task.ts +103 -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 +229 -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 +124 -0
- package/src/workflows/validation.ts +104 -0
- package/src/workflows/version.ts +44 -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/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/.claude/settings.local.json +0 -115
- package/.dockerignore +0 -61
- package/.editorconfig +0 -15
- package/.env.docker.example +0 -39
- package/.env.example +0 -40
- package/.github/workflows/ci.yml +0 -76
- package/.github/workflows/docker-and-deploy.yml +0 -117
- package/.wts-config.json +0 -4
- package/.wts-setup.ts +0 -102
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -270
- package/DEPLOYMENT.md +0 -605
- package/Dockerfile +0 -57
- package/Dockerfile.worker +0 -157
- package/FAQ.md +0 -19
- package/MCP.md +0 -406
- package/UI.md +0 -40
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/assets/agent-swarm.mp4 +0 -0
- package/assets/agent-swarm.png +0 -0
- package/biome.json +0 -39
- package/deploy/DEPLOY.md +0 -60
- package/deploy/agent-swarm.service +0 -17
- package/deploy/docker-push.ts +0 -30
- package/deploy/install.ts +0 -85
- package/deploy/prod-db.ts +0 -42
- package/deploy/uninstall.ts +0 -12
- package/deploy/update.ts +0 -21
- package/docker-compose.example.yml +0 -159
- package/docker-entrypoint.sh +0 -352
- package/ecosystem.config.cjs +0 -66
- package/plugin/README.md +0 -1
- package/plugin/hooks/hooks.json +0 -71
- package/pyproject.toml +0 -9
- package/scripts/generate-mcp-docs.ts +0 -415
- package/slack-manifest.json +0 -71
- 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/thoughts/shared/plans/2025-12-18-slack-integration.md +0 -1195
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +0 -732
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +0 -361
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +0 -501
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +0 -560
- package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +0 -934
- package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +0 -1000
- package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +0 -568
- package/thoughts/shared/plans/2026-01-09-inverse-teleport.md +0 -1516
- package/thoughts/shared/plans/2026-01-12-agent-rename-pm2-control.md +0 -1133
- package/thoughts/shared/plans/2026-01-12-github-app-integration.md +0 -380
- package/thoughts/shared/plans/2026-01-12-lead-inbox-model.md +0 -876
- package/thoughts/shared/plans/2026-01-12-ralph-wiggum-integration.md +0 -463
- package/thoughts/shared/plans/2026-01-13-agent-concurrency.md +0 -691
- package/thoughts/shared/plans/2026-01-13-github-assignment-handling.md +0 -690
- package/thoughts/shared/plans/2026-01-13-prevent-duplicate-trigger-processing.md +0 -1071
- package/thoughts/shared/plans/2026-01-14-fix-slack-thread-context.md +0 -507
- package/thoughts/shared/plans/2026-01-15-scheduled-tasks-implementation.md +0 -565
- package/thoughts/shared/plans/2026-01-15-usage-cost-tracking-ui.md +0 -1479
- package/thoughts/shared/plans/2026-01-16-epics-feature-implementation.md +0 -1230
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-01-09-inverse-teleport-plan-review.md +0 -420
- package/thoughts/shared/research/2025-12-18-slack-integration.md +0 -442
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +0 -339
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +0 -390
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +0 -376
- package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +0 -582
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +0 -264
- package/thoughts/shared/research/2026-01-13-lead-duplicate-trigger-processing.md +0 -223
- package/thoughts/shared/research/2026-01-14-lead-slack-thread-context.md +0 -277
- package/thoughts/shared/research/2026-01-15-ai-tracker-agent-swarm-integration.md +0 -376
- package/thoughts/shared/research/2026-01-15-auto-starting-processes-in-worker-containers.md +0 -787
- package/thoughts/shared/research/2026-01-15-scheduled-tasks.md +0 -390
- package/thoughts/shared/research/2026-01-16-epics-feature-research.md +0 -437
- package/thoughts/taras/plans/2026-01-22-agent-swarm-schemas.md +0 -98
- package/thoughts/taras/plans/2026-01-28-per-worker-claude-md.md +0 -617
- package/thoughts/taras/plans/2026-01-28-sentry-cli-integration.md +0 -214
- package/thoughts/taras/research/2026-01-22-vercel-cli-integration.md +0 -287
- package/thoughts/taras/research/2026-01-27-excessive-polling-issue.md +0 -311
- package/thoughts/taras/research/2026-01-28-per-worker-claude-md.md +0 -383
- package/thoughts/taras/research/2026-01-28-sentry-cli-integration.md +0 -240
- package/tsconfig.json +0 -37
- 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/public/logo.png +0 -0
- 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/hooks/useAutoScroll.ts +0 -83
- 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/lib/utils.ts +0 -88
- package/ui/src/main.tsx +0 -28
- package/ui/src/types/api.ts +0 -323
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -37
- package/ui/tsconfig.json +0 -31
- package/ui/vite.config.ts +0 -35
- /package/{thoughts/shared/plans → templates/community}/.gitkeep +0 -0
package/src/be/db.ts
CHANGED
|
@@ -1,27 +1,53 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { configureDbResolver } from "../prompts/resolver";
|
|
2
3
|
import type {
|
|
4
|
+
ActiveSession,
|
|
3
5
|
Agent,
|
|
4
6
|
AgentLog,
|
|
5
7
|
AgentLogEventType,
|
|
8
|
+
AgentMemory,
|
|
9
|
+
AgentMemoryScope,
|
|
10
|
+
AgentMemorySource,
|
|
6
11
|
AgentStatus,
|
|
7
12
|
AgentTask,
|
|
8
13
|
AgentTaskSource,
|
|
9
14
|
AgentTaskStatus,
|
|
10
15
|
AgentWithTasks,
|
|
16
|
+
ChangeSource,
|
|
11
17
|
Channel,
|
|
12
18
|
ChannelMessage,
|
|
13
19
|
ChannelType,
|
|
20
|
+
ContextVersion,
|
|
21
|
+
CooldownConfig,
|
|
14
22
|
Epic,
|
|
15
23
|
EpicStatus,
|
|
16
24
|
EpicWithProgress,
|
|
17
25
|
InboxMessage,
|
|
18
26
|
InboxMessageStatus,
|
|
27
|
+
InputValue,
|
|
28
|
+
PromptTemplate,
|
|
29
|
+
PromptTemplateHistory,
|
|
19
30
|
ScheduledTask,
|
|
20
31
|
Service,
|
|
21
32
|
ServiceStatus,
|
|
22
33
|
SessionCost,
|
|
23
34
|
SessionLog,
|
|
35
|
+
SwarmConfig,
|
|
36
|
+
SwarmRepo,
|
|
37
|
+
TriggerConfig,
|
|
38
|
+
VersionableField,
|
|
39
|
+
VersionMeta,
|
|
40
|
+
Workflow,
|
|
41
|
+
WorkflowDefinition,
|
|
42
|
+
WorkflowRun,
|
|
43
|
+
WorkflowRunStatus,
|
|
44
|
+
WorkflowRunStep,
|
|
45
|
+
WorkflowRunStepStatus,
|
|
46
|
+
WorkflowSnapshot,
|
|
47
|
+
WorkflowVersion,
|
|
24
48
|
} from "../types";
|
|
49
|
+
import { runMigrations } from "./migrations/runner";
|
|
50
|
+
import { seedDefaultTemplates } from "./seed";
|
|
25
51
|
|
|
26
52
|
let db: Database | null = null;
|
|
27
53
|
|
|
@@ -33,613 +59,141 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
33
59
|
db = new Database(dbPath, { create: true });
|
|
34
60
|
console.log(`Database initialized at ${dbPath}`);
|
|
35
61
|
|
|
36
|
-
// Capture in local const for TypeScript (db is guaranteed non-null here)
|
|
37
62
|
const database = db;
|
|
38
|
-
|
|
39
63
|
database.run("PRAGMA journal_mode = WAL;");
|
|
40
64
|
database.run("PRAGMA foreign_keys = ON;");
|
|
41
65
|
|
|
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
|
-
}
|
|
530
|
-
|
|
531
|
-
// Migration: Add processing_since column to channel_read_state for Phase 3
|
|
532
|
-
try {
|
|
533
|
-
db.run(`ALTER TABLE channel_read_state ADD COLUMN processing_since TEXT`);
|
|
534
|
-
} catch {
|
|
535
|
-
/* exists */
|
|
536
|
-
}
|
|
66
|
+
// Run database migrations (schema creation + incremental changes)
|
|
67
|
+
runMigrations(database);
|
|
537
68
|
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
db.run(`ALTER TABLE agent_tasks ADD COLUMN notifiedAt TEXT`);
|
|
541
|
-
} catch {
|
|
542
|
-
/* exists */
|
|
543
|
-
}
|
|
69
|
+
// Compatibility migration for legacy databases that predate profile fields
|
|
70
|
+
ensureAgentProfileColumns(database);
|
|
544
71
|
|
|
545
|
-
// Migration:
|
|
546
|
-
//
|
|
72
|
+
// Migration: Remove restrictive CHECK constraint on agent_tasks.status
|
|
73
|
+
// Old databases have CHECK(status IN ('pending','in_progress','completed','failed'))
|
|
74
|
+
// which blocks 'cancelled', 'paused', 'offered', 'unassigned' statuses
|
|
547
75
|
try {
|
|
548
|
-
|
|
549
|
-
const schemaInfo = db
|
|
76
|
+
const taskSchemaInfo = db
|
|
550
77
|
.prepare<{ sql: string | null }, []>(
|
|
551
|
-
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = '
|
|
78
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'agent_tasks'",
|
|
552
79
|
)
|
|
553
80
|
.get();
|
|
554
81
|
|
|
555
|
-
const
|
|
82
|
+
const schemaSql = taskSchemaInfo?.sql ?? "";
|
|
83
|
+
const hasStatusCheck = /status\s+TEXT\b[^,]*\bCHECK\s*\(\s*status\s+IN\s*\(/i.test(schemaSql);
|
|
84
|
+
const statusAllowsCancelled = /status\s+IN\s*\([^)]*'cancelled'/i.test(schemaSql);
|
|
85
|
+
const needsStatusMigration = hasStatusCheck && !statusAllowsCancelled;
|
|
556
86
|
|
|
557
|
-
if (
|
|
558
|
-
console.log(
|
|
559
|
-
"[Migration] Updating inbox_messages CHECK constraint to include 'processing' status",
|
|
560
|
-
);
|
|
87
|
+
if (needsStatusMigration) {
|
|
88
|
+
console.log("[Migration] Removing restrictive CHECK constraint on agent_tasks.status");
|
|
561
89
|
db.run("PRAGMA foreign_keys=off");
|
|
562
90
|
|
|
563
91
|
db.run(`
|
|
564
|
-
CREATE TABLE
|
|
92
|
+
CREATE TABLE agent_tasks_new (
|
|
565
93
|
id TEXT PRIMARY KEY,
|
|
566
|
-
agentId TEXT
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
status TEXT NOT NULL DEFAULT '
|
|
94
|
+
agentId TEXT,
|
|
95
|
+
creatorAgentId TEXT,
|
|
96
|
+
task TEXT NOT NULL,
|
|
97
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
98
|
+
source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api', 'github', 'agentmail', 'system', 'schedule')),
|
|
99
|
+
taskType TEXT,
|
|
100
|
+
tags TEXT DEFAULT '[]',
|
|
101
|
+
priority INTEGER DEFAULT 50,
|
|
102
|
+
dependsOn TEXT DEFAULT '[]',
|
|
103
|
+
offeredTo TEXT,
|
|
104
|
+
offeredAt TEXT,
|
|
105
|
+
acceptedAt TEXT,
|
|
106
|
+
rejectionReason TEXT,
|
|
570
107
|
slackChannelId TEXT,
|
|
571
108
|
slackThreadTs TEXT,
|
|
572
109
|
slackUserId TEXT,
|
|
573
|
-
matchedText TEXT,
|
|
574
|
-
delegatedToTaskId TEXT,
|
|
575
|
-
responseText TEXT,
|
|
576
110
|
createdAt TEXT NOT NULL,
|
|
577
111
|
lastUpdatedAt TEXT NOT NULL,
|
|
578
|
-
|
|
579
|
-
|
|
112
|
+
finishedAt TEXT,
|
|
113
|
+
failureReason TEXT,
|
|
114
|
+
output TEXT,
|
|
115
|
+
progress TEXT,
|
|
116
|
+
notifiedAt TEXT,
|
|
117
|
+
mentionMessageId TEXT,
|
|
118
|
+
mentionChannelId TEXT,
|
|
119
|
+
githubRepo TEXT,
|
|
120
|
+
githubEventType TEXT,
|
|
121
|
+
githubNumber INTEGER,
|
|
122
|
+
githubCommentId INTEGER,
|
|
123
|
+
githubAuthor TEXT,
|
|
124
|
+
githubUrl TEXT,
|
|
125
|
+
epicId TEXT REFERENCES epics(id) ON DELETE SET NULL,
|
|
126
|
+
parentTaskId TEXT,
|
|
127
|
+
claudeSessionId TEXT,
|
|
128
|
+
agentmailInboxId TEXT,
|
|
129
|
+
agentmailMessageId TEXT,
|
|
130
|
+
agentmailThreadId TEXT,
|
|
131
|
+
model TEXT,
|
|
132
|
+
scheduleId TEXT
|
|
580
133
|
)
|
|
581
134
|
`);
|
|
582
135
|
|
|
583
|
-
|
|
584
|
-
db.run(
|
|
585
|
-
|
|
136
|
+
// Copy all data — use column list to handle any column ordering differences
|
|
137
|
+
db.run(`
|
|
138
|
+
INSERT INTO agent_tasks_new (
|
|
139
|
+
id, agentId, creatorAgentId, task, status, source, taskType, tags,
|
|
140
|
+
priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
|
|
141
|
+
slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt,
|
|
142
|
+
finishedAt, failureReason, output, progress, notifiedAt,
|
|
143
|
+
mentionMessageId, mentionChannelId, githubRepo, githubEventType,
|
|
144
|
+
githubNumber, githubCommentId, githubAuthor, githubUrl,
|
|
145
|
+
epicId, parentTaskId, claudeSessionId,
|
|
146
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
147
|
+
model, scheduleId
|
|
148
|
+
)
|
|
149
|
+
SELECT
|
|
150
|
+
id, agentId, creatorAgentId, task, status, source, taskType, tags,
|
|
151
|
+
priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
|
|
152
|
+
slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt,
|
|
153
|
+
finishedAt, failureReason, output, progress, notifiedAt,
|
|
154
|
+
mentionMessageId, mentionChannelId, githubRepo, githubEventType,
|
|
155
|
+
githubNumber, githubCommentId, githubAuthor, githubUrl,
|
|
156
|
+
epicId, parentTaskId, claudeSessionId,
|
|
157
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
158
|
+
model, scheduleId
|
|
159
|
+
FROM agent_tasks
|
|
160
|
+
`);
|
|
586
161
|
|
|
587
|
-
|
|
588
|
-
db.run("
|
|
589
|
-
|
|
162
|
+
db.run("DROP TABLE agent_tasks");
|
|
163
|
+
db.run("ALTER TABLE agent_tasks_new RENAME TO agent_tasks");
|
|
164
|
+
|
|
165
|
+
// Recreate all indexes
|
|
166
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId)");
|
|
167
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status)");
|
|
168
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_offeredTo ON agent_tasks(offeredTo)");
|
|
169
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_taskType ON agent_tasks(taskType)");
|
|
170
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_epicId ON agent_tasks(epicId)");
|
|
171
|
+
db.run(
|
|
172
|
+
"CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentmailThreadId ON agent_tasks(agentmailThreadId)",
|
|
173
|
+
);
|
|
174
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_agent_tasks_schedule_id ON agent_tasks(scheduleId)");
|
|
590
175
|
|
|
591
176
|
db.run("PRAGMA foreign_keys=on");
|
|
592
|
-
console.log("[Migration] Successfully
|
|
177
|
+
console.log("[Migration] Successfully removed CHECK constraint on agent_tasks.status");
|
|
593
178
|
}
|
|
594
179
|
} catch (e) {
|
|
595
|
-
console.error("[Migration] Failed to update
|
|
180
|
+
console.error("[Migration] Failed to update agent_tasks CHECK constraint:", e);
|
|
596
181
|
try {
|
|
597
182
|
db.run("PRAGMA foreign_keys=on");
|
|
598
|
-
} catch {
|
|
183
|
+
} catch (cleanupError) {
|
|
184
|
+
console.error("[Migration] Failed to re-enable SQLite foreign_keys pragma:", cleanupError);
|
|
185
|
+
}
|
|
599
186
|
throw e;
|
|
600
187
|
}
|
|
601
188
|
|
|
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
|
-
}
|
|
189
|
+
// Backfill: Seed v1 for existing agents that don't have any context versions yet
|
|
190
|
+
seedContextVersions();
|
|
627
191
|
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
db.run(`ALTER TABLE epics ADD COLUMN progressNotifiedAt TEXT`);
|
|
631
|
-
} catch {
|
|
632
|
-
/* exists */
|
|
633
|
-
}
|
|
192
|
+
// Inject DB resolver into the prompt template resolver (DI to avoid worker/API boundary violation)
|
|
193
|
+
configureDbResolver(resolvePromptTemplate);
|
|
634
194
|
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
db.run(
|
|
638
|
-
`ALTER TABLE epics ADD COLUMN channelId TEXT REFERENCES channels(id) ON DELETE SET NULL`,
|
|
639
|
-
);
|
|
640
|
-
} catch {
|
|
641
|
-
/* exists */
|
|
642
|
-
}
|
|
195
|
+
// Seed default prompt templates from the in-memory code registry
|
|
196
|
+
seedDefaultTemplates();
|
|
643
197
|
|
|
644
198
|
return db;
|
|
645
199
|
}
|
|
@@ -659,64 +213,286 @@ export function closeDb(): void {
|
|
|
659
213
|
}
|
|
660
214
|
|
|
661
215
|
// ============================================================================
|
|
662
|
-
//
|
|
216
|
+
// Context Versioning
|
|
663
217
|
// ============================================================================
|
|
664
218
|
|
|
665
|
-
|
|
219
|
+
const VERSIONABLE_FIELDS: VersionableField[] = [
|
|
220
|
+
"soulMd",
|
|
221
|
+
"identityMd",
|
|
222
|
+
"toolsMd",
|
|
223
|
+
"claudeMd",
|
|
224
|
+
"setupScript",
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
function ensureAgentProfileColumns(database: Database): void {
|
|
228
|
+
const existingColumns = new Set(
|
|
229
|
+
database
|
|
230
|
+
.prepare<{ name: string }, []>("PRAGMA table_info(agents)")
|
|
231
|
+
.all()
|
|
232
|
+
.map((row) => row.name),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
for (const column of VERSIONABLE_FIELDS) {
|
|
236
|
+
if (!existingColumns.has(column)) {
|
|
237
|
+
try {
|
|
238
|
+
database.run(`ALTER TABLE agents ADD COLUMN ${column} TEXT`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`[Migration] Failed to add missing agents.${column} column`, error);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function computeContentHash(content: string): string {
|
|
248
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
249
|
+
hasher.update(content);
|
|
250
|
+
return hasher.digest("hex");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type ContextVersionRow = {
|
|
666
254
|
id: string;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
255
|
+
agentId: string;
|
|
256
|
+
field: string;
|
|
257
|
+
content: string;
|
|
258
|
+
version: number;
|
|
259
|
+
changeSource: string;
|
|
260
|
+
changedByAgentId: string | null;
|
|
261
|
+
changeReason: string | null;
|
|
262
|
+
contentHash: string;
|
|
263
|
+
previousVersionId: string | null;
|
|
676
264
|
createdAt: string;
|
|
677
|
-
lastUpdatedAt: string;
|
|
678
265
|
};
|
|
679
266
|
|
|
680
|
-
function
|
|
267
|
+
function rowToContextVersion(row: ContextVersionRow): ContextVersion {
|
|
681
268
|
return {
|
|
682
269
|
id: row.id,
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
270
|
+
agentId: row.agentId,
|
|
271
|
+
field: row.field as VersionableField,
|
|
272
|
+
content: row.content,
|
|
273
|
+
version: row.version,
|
|
274
|
+
changeSource: row.changeSource as ChangeSource,
|
|
275
|
+
changedByAgentId: row.changedByAgentId,
|
|
276
|
+
changeReason: row.changeReason,
|
|
277
|
+
contentHash: row.contentHash,
|
|
278
|
+
previousVersionId: row.previousVersionId,
|
|
692
279
|
createdAt: row.createdAt,
|
|
693
|
-
lastUpdatedAt: row.lastUpdatedAt,
|
|
694
280
|
};
|
|
695
281
|
}
|
|
696
282
|
|
|
697
|
-
export
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
283
|
+
export function createContextVersion(params: {
|
|
284
|
+
agentId: string;
|
|
285
|
+
field: VersionableField;
|
|
286
|
+
content: string;
|
|
287
|
+
version: number;
|
|
288
|
+
changeSource: ChangeSource;
|
|
289
|
+
changedByAgentId?: string | null;
|
|
290
|
+
changeReason?: string | null;
|
|
291
|
+
contentHash: string;
|
|
292
|
+
previousVersionId?: string | null;
|
|
293
|
+
}): ContextVersion {
|
|
294
|
+
const id = crypto.randomUUID();
|
|
295
|
+
const now = new Date().toISOString();
|
|
702
296
|
|
|
703
|
-
|
|
297
|
+
const row = getDb()
|
|
298
|
+
.prepare<
|
|
299
|
+
ContextVersionRow,
|
|
300
|
+
[
|
|
301
|
+
string,
|
|
302
|
+
string,
|
|
303
|
+
string,
|
|
304
|
+
string,
|
|
305
|
+
number,
|
|
306
|
+
string,
|
|
307
|
+
string | null,
|
|
308
|
+
string | null,
|
|
309
|
+
string,
|
|
310
|
+
string | null,
|
|
311
|
+
string,
|
|
312
|
+
]
|
|
313
|
+
>(
|
|
314
|
+
`INSERT INTO context_versions (id, agentId, field, content, version, changeSource, changedByAgentId, changeReason, contentHash, previousVersionId, createdAt)
|
|
315
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
316
|
+
)
|
|
317
|
+
.get(
|
|
318
|
+
id,
|
|
319
|
+
params.agentId,
|
|
320
|
+
params.field,
|
|
321
|
+
params.content,
|
|
322
|
+
params.version,
|
|
323
|
+
params.changeSource,
|
|
324
|
+
params.changedByAgentId ?? null,
|
|
325
|
+
params.changeReason ?? null,
|
|
326
|
+
params.contentHash,
|
|
327
|
+
params.previousVersionId ?? null,
|
|
328
|
+
now,
|
|
329
|
+
);
|
|
704
330
|
|
|
705
|
-
|
|
331
|
+
if (!row) throw new Error("Failed to create context version");
|
|
332
|
+
return rowToContextVersion(row);
|
|
333
|
+
}
|
|
706
334
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
335
|
+
export function getLatestContextVersion(
|
|
336
|
+
agentId: string,
|
|
337
|
+
field: VersionableField,
|
|
338
|
+
): ContextVersion | null {
|
|
339
|
+
const row = getDb()
|
|
340
|
+
.prepare<ContextVersionRow, [string, string]>(
|
|
341
|
+
`SELECT * FROM context_versions WHERE agentId = ? AND field = ? ORDER BY version DESC LIMIT 1`,
|
|
342
|
+
)
|
|
343
|
+
.get(agentId, field);
|
|
711
344
|
|
|
712
|
-
|
|
713
|
-
}
|
|
345
|
+
return row ? rowToContextVersion(row) : null;
|
|
346
|
+
}
|
|
714
347
|
|
|
715
|
-
export function
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
348
|
+
export function getContextVersion(id: string): ContextVersion | null {
|
|
349
|
+
const row = getDb()
|
|
350
|
+
.prepare<ContextVersionRow, [string]>(`SELECT * FROM context_versions WHERE id = ?`)
|
|
351
|
+
.get(id);
|
|
352
|
+
|
|
353
|
+
return row ? rowToContextVersion(row) : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function getContextVersionHistory(params: {
|
|
357
|
+
agentId: string;
|
|
358
|
+
field?: VersionableField;
|
|
359
|
+
limit?: number;
|
|
360
|
+
}): ContextVersion[] {
|
|
361
|
+
const limit = params.limit ?? 10;
|
|
362
|
+
|
|
363
|
+
if (params.field) {
|
|
364
|
+
const rows = getDb()
|
|
365
|
+
.prepare<ContextVersionRow, [string, string, number]>(
|
|
366
|
+
`SELECT * FROM context_versions WHERE agentId = ? AND field = ? ORDER BY version DESC LIMIT ?`,
|
|
367
|
+
)
|
|
368
|
+
.all(params.agentId, params.field, limit);
|
|
369
|
+
return rows.map(rowToContextVersion);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const rows = getDb()
|
|
373
|
+
.prepare<ContextVersionRow, [string, number]>(
|
|
374
|
+
`SELECT * FROM context_versions WHERE agentId = ? ORDER BY createdAt DESC LIMIT ?`,
|
|
375
|
+
)
|
|
376
|
+
.all(params.agentId, limit);
|
|
377
|
+
return rows.map(rowToContextVersion);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Seed v1 context versions for existing agents that don't have any versions yet.
|
|
382
|
+
* Called during migration.
|
|
383
|
+
*/
|
|
384
|
+
function seedContextVersions(): void {
|
|
385
|
+
const database = getDb();
|
|
386
|
+
const agents = database
|
|
387
|
+
.prepare<
|
|
388
|
+
{
|
|
389
|
+
id: string;
|
|
390
|
+
soulMd: string | null;
|
|
391
|
+
identityMd: string | null;
|
|
392
|
+
toolsMd: string | null;
|
|
393
|
+
claudeMd: string | null;
|
|
394
|
+
setupScript: string | null;
|
|
395
|
+
},
|
|
396
|
+
[]
|
|
397
|
+
>(`SELECT id, soulMd, identityMd, toolsMd, claudeMd, setupScript FROM agents`)
|
|
398
|
+
.all();
|
|
399
|
+
|
|
400
|
+
for (const agent of agents) {
|
|
401
|
+
for (const field of VERSIONABLE_FIELDS) {
|
|
402
|
+
const content = agent[field];
|
|
403
|
+
if (!content) continue;
|
|
404
|
+
|
|
405
|
+
// Check if a version already exists for this agent+field
|
|
406
|
+
const existing = database
|
|
407
|
+
.prepare<{ id: string }, [string, string]>(
|
|
408
|
+
`SELECT id FROM context_versions WHERE agentId = ? AND field = ? LIMIT 1`,
|
|
409
|
+
)
|
|
410
|
+
.get(agent.id, field);
|
|
411
|
+
if (existing) continue;
|
|
412
|
+
|
|
413
|
+
const id = crypto.randomUUID();
|
|
414
|
+
const hash = computeContentHash(content);
|
|
415
|
+
const now = new Date().toISOString();
|
|
416
|
+
|
|
417
|
+
database
|
|
418
|
+
.prepare(
|
|
419
|
+
`INSERT INTO context_versions (id, agentId, field, content, version, changeSource, contentHash, createdAt)
|
|
420
|
+
VALUES (?, ?, ?, ?, 1, 'system', ?, ?)`,
|
|
421
|
+
)
|
|
422
|
+
.run(id, agent.id, field, content, hash, now);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Agent Queries
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
type AgentRow = {
|
|
432
|
+
id: string;
|
|
433
|
+
name: string;
|
|
434
|
+
isLead: number;
|
|
435
|
+
status: AgentStatus;
|
|
436
|
+
description: string | null;
|
|
437
|
+
role: string | null;
|
|
438
|
+
capabilities: string | null;
|
|
439
|
+
maxTasks: number | null;
|
|
440
|
+
emptyPollCount: number | null;
|
|
441
|
+
claudeMd: string | null;
|
|
442
|
+
soulMd: string | null;
|
|
443
|
+
identityMd: string | null;
|
|
444
|
+
setupScript: string | null;
|
|
445
|
+
toolsMd: string | null;
|
|
446
|
+
lastActivityAt: string | null;
|
|
447
|
+
createdAt: string;
|
|
448
|
+
lastUpdatedAt: string;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
function rowToAgent(row: AgentRow): Agent {
|
|
452
|
+
return {
|
|
453
|
+
id: row.id,
|
|
454
|
+
name: row.name,
|
|
455
|
+
isLead: row.isLead === 1,
|
|
456
|
+
status: row.status,
|
|
457
|
+
description: row.description ?? undefined,
|
|
458
|
+
role: row.role ?? undefined,
|
|
459
|
+
capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
|
|
460
|
+
maxTasks: row.maxTasks ?? 1,
|
|
461
|
+
emptyPollCount: row.emptyPollCount ?? 0,
|
|
462
|
+
claudeMd: row.claudeMd ?? undefined,
|
|
463
|
+
soulMd: row.soulMd ?? undefined,
|
|
464
|
+
identityMd: row.identityMd ?? undefined,
|
|
465
|
+
setupScript: row.setupScript ?? undefined,
|
|
466
|
+
toolsMd: row.toolsMd ?? undefined,
|
|
467
|
+
lastActivityAt: row.lastActivityAt ?? undefined,
|
|
468
|
+
createdAt: row.createdAt,
|
|
469
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export const agentQueries = {
|
|
474
|
+
insert: () =>
|
|
475
|
+
getDb().prepare<AgentRow, [string, string, number, AgentStatus, number]>(
|
|
476
|
+
"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 *",
|
|
477
|
+
),
|
|
478
|
+
|
|
479
|
+
getById: () => getDb().prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?"),
|
|
480
|
+
|
|
481
|
+
getAll: () => getDb().prepare<AgentRow, []>("SELECT * FROM agents ORDER BY name"),
|
|
482
|
+
|
|
483
|
+
updateStatus: () =>
|
|
484
|
+
getDb().prepare<AgentRow, [AgentStatus, string]>(
|
|
485
|
+
"UPDATE agents SET status = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ? RETURNING *",
|
|
486
|
+
),
|
|
487
|
+
|
|
488
|
+
delete: () => getDb().prepare<null, [string]>("DELETE FROM agents WHERE id = ?"),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export function createAgent(
|
|
492
|
+
agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
|
|
493
|
+
): Agent {
|
|
494
|
+
const id = agent.id ?? crypto.randomUUID();
|
|
495
|
+
const maxTasks = agent.maxTasks ?? 1;
|
|
720
496
|
const row = agentQueries
|
|
721
497
|
.insert()
|
|
722
498
|
.get(id, agent.name, agent.isLead ? 1 : 0, agent.status, maxTasks);
|
|
@@ -736,6 +512,11 @@ export function getAllAgents(): Agent[] {
|
|
|
736
512
|
return agentQueries.getAll().all().map(rowToAgent);
|
|
737
513
|
}
|
|
738
514
|
|
|
515
|
+
export function getLeadAgent(): Agent | null {
|
|
516
|
+
const agents = getAllAgents();
|
|
517
|
+
return agents.find((a) => a.isLead) ?? null;
|
|
518
|
+
}
|
|
519
|
+
|
|
739
520
|
export function updateAgentStatus(id: string, status: AgentStatus): Agent | null {
|
|
740
521
|
const oldAgent = getAgentById(id);
|
|
741
522
|
const row = agentQueries.updateStatus().get(status, id);
|
|
@@ -762,6 +543,14 @@ export function updateAgentMaxTasks(id: string, maxTasks: number): Agent | null
|
|
|
762
543
|
return row ? rowToAgent(row) : null;
|
|
763
544
|
}
|
|
764
545
|
|
|
546
|
+
export function updateAgentActivity(id: string): void {
|
|
547
|
+
getDb()
|
|
548
|
+
.prepare<null, [string]>(
|
|
549
|
+
`UPDATE agents SET lastActivityAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
|
550
|
+
)
|
|
551
|
+
.run(id);
|
|
552
|
+
}
|
|
553
|
+
|
|
765
554
|
// ============================================================================
|
|
766
555
|
// Agent Poll Tracking Functions
|
|
767
556
|
// ============================================================================
|
|
@@ -895,15 +684,27 @@ type AgentTaskRow = {
|
|
|
895
684
|
slackChannelId: string | null;
|
|
896
685
|
slackThreadTs: string | null;
|
|
897
686
|
slackUserId: string | null;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
687
|
+
vcsProvider: string | null;
|
|
688
|
+
vcsRepo: string | null;
|
|
689
|
+
vcsEventType: string | null;
|
|
690
|
+
vcsNumber: number | null;
|
|
691
|
+
vcsCommentId: number | null;
|
|
692
|
+
vcsAuthor: string | null;
|
|
693
|
+
vcsUrl: string | null;
|
|
694
|
+
agentmailInboxId: string | null;
|
|
695
|
+
agentmailMessageId: string | null;
|
|
696
|
+
agentmailThreadId: string | null;
|
|
904
697
|
mentionMessageId: string | null;
|
|
905
698
|
mentionChannelId: string | null;
|
|
906
699
|
epicId: string | null;
|
|
700
|
+
dir: string | null;
|
|
701
|
+
parentTaskId: string | null;
|
|
702
|
+
claudeSessionId: string | null;
|
|
703
|
+
model: string | null;
|
|
704
|
+
scheduleId: string | null;
|
|
705
|
+
workflowRunId: string | null;
|
|
706
|
+
workflowRunStepId: string | null;
|
|
707
|
+
outputSchema: string | null;
|
|
907
708
|
createdAt: string;
|
|
908
709
|
lastUpdatedAt: string;
|
|
909
710
|
finishedAt: string | null;
|
|
@@ -932,15 +733,27 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
932
733
|
slackChannelId: row.slackChannelId ?? undefined,
|
|
933
734
|
slackThreadTs: row.slackThreadTs ?? undefined,
|
|
934
735
|
slackUserId: row.slackUserId ?? undefined,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
736
|
+
vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
|
|
737
|
+
vcsRepo: row.vcsRepo ?? undefined,
|
|
738
|
+
vcsEventType: row.vcsEventType ?? undefined,
|
|
739
|
+
vcsNumber: row.vcsNumber ?? undefined,
|
|
740
|
+
vcsCommentId: row.vcsCommentId ?? undefined,
|
|
741
|
+
vcsAuthor: row.vcsAuthor ?? undefined,
|
|
742
|
+
vcsUrl: row.vcsUrl ?? undefined,
|
|
743
|
+
agentmailInboxId: row.agentmailInboxId ?? undefined,
|
|
744
|
+
agentmailMessageId: row.agentmailMessageId ?? undefined,
|
|
745
|
+
agentmailThreadId: row.agentmailThreadId ?? undefined,
|
|
941
746
|
mentionMessageId: row.mentionMessageId ?? undefined,
|
|
942
747
|
mentionChannelId: row.mentionChannelId ?? undefined,
|
|
943
748
|
epicId: row.epicId ?? undefined,
|
|
749
|
+
dir: row.dir ?? undefined,
|
|
750
|
+
parentTaskId: row.parentTaskId ?? undefined,
|
|
751
|
+
claudeSessionId: row.claudeSessionId ?? undefined,
|
|
752
|
+
model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
|
|
753
|
+
scheduleId: row.scheduleId ?? undefined,
|
|
754
|
+
workflowRunId: row.workflowRunId ?? undefined,
|
|
755
|
+
workflowRunStepId: row.workflowRunStepId ?? undefined,
|
|
756
|
+
outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
|
|
944
757
|
createdAt: row.createdAt,
|
|
945
758
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
946
759
|
finishedAt: row.finishedAt ?? undefined,
|
|
@@ -1006,7 +819,10 @@ export const taskQueries = {
|
|
|
1006
819
|
|
|
1007
820
|
setProgress: () =>
|
|
1008
821
|
getDb().prepare<AgentTaskRow, [string, string]>(
|
|
1009
|
-
|
|
822
|
+
`UPDATE agent_tasks SET progress = ?,
|
|
823
|
+
status = CASE WHEN status IN ('completed', 'failed', 'cancelled') THEN status ELSE 'in_progress' END,
|
|
824
|
+
lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
825
|
+
WHERE id = ? RETURNING *`,
|
|
1010
826
|
),
|
|
1011
827
|
|
|
1012
828
|
delete: () => getDb().prepare<null, [string]>("DELETE FROM agent_tasks WHERE id = ?"),
|
|
@@ -1071,10 +887,17 @@ export function getPendingTaskForAgent(agentId: string): AgentTask | null {
|
|
|
1071
887
|
|
|
1072
888
|
export function startTask(taskId: string): AgentTask | null {
|
|
1073
889
|
const oldTask = getTaskById(taskId);
|
|
890
|
+
if (!oldTask) return null;
|
|
891
|
+
|
|
892
|
+
// Guard: never revive tasks that are already in a terminal state
|
|
893
|
+
if (["completed", "failed", "cancelled"].includes(oldTask.status)) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
1074
897
|
const row = getDb()
|
|
1075
898
|
.prepare<AgentTaskRow, [string]>(
|
|
1076
899
|
`UPDATE agent_tasks SET status = 'in_progress', lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
1077
|
-
WHERE id = ? RETURNING *`,
|
|
900
|
+
WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled') RETURNING *`,
|
|
1078
901
|
)
|
|
1079
902
|
.get(taskId);
|
|
1080
903
|
if (row && oldTask) {
|
|
@@ -1096,6 +919,18 @@ export function getTaskById(id: string): AgentTask | null {
|
|
|
1096
919
|
return row ? rowToAgentTask(row) : null;
|
|
1097
920
|
}
|
|
1098
921
|
|
|
922
|
+
export function updateTaskClaudeSessionId(
|
|
923
|
+
taskId: string,
|
|
924
|
+
claudeSessionId: string,
|
|
925
|
+
): AgentTask | null {
|
|
926
|
+
const row = getDb()
|
|
927
|
+
.prepare<AgentTaskRow, [string, string, string]>(
|
|
928
|
+
`UPDATE agent_tasks SET claudeSessionId = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *`,
|
|
929
|
+
)
|
|
930
|
+
.get(claudeSessionId, new Date().toISOString(), taskId);
|
|
931
|
+
return row ? rowToAgentTask(row) : null;
|
|
932
|
+
}
|
|
933
|
+
|
|
1099
934
|
export function getTasksByAgentId(agentId: string): AgentTask[] {
|
|
1100
935
|
return taskQueries.getByAgentId().all(agentId).map(rowToAgentTask);
|
|
1101
936
|
}
|
|
@@ -1105,25 +940,29 @@ export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
|
|
|
1105
940
|
}
|
|
1106
941
|
|
|
1107
942
|
/**
|
|
1108
|
-
* Find a task by
|
|
1109
|
-
* Returns the most recent non-completed/failed task for this
|
|
943
|
+
* Find a task by VCS repo and issue/PR/MR number.
|
|
944
|
+
* Returns the most recent non-completed/failed task for this VCS entity.
|
|
1110
945
|
*/
|
|
1111
|
-
export function
|
|
946
|
+
export function findTaskByVcs(vcsRepo: string, vcsNumber: number): AgentTask | null {
|
|
1112
947
|
const row = getDb()
|
|
1113
948
|
.prepare<AgentTaskRow, [string, number]>(
|
|
1114
949
|
`SELECT * FROM agent_tasks
|
|
1115
|
-
WHERE
|
|
950
|
+
WHERE vcsRepo = ? AND vcsNumber = ?
|
|
1116
951
|
AND status NOT IN ('completed', 'failed')
|
|
1117
952
|
ORDER BY createdAt DESC
|
|
1118
953
|
LIMIT 1`,
|
|
1119
954
|
)
|
|
1120
|
-
.get(
|
|
955
|
+
.get(vcsRepo, vcsNumber);
|
|
1121
956
|
return row ? rowToAgentTask(row) : null;
|
|
1122
957
|
}
|
|
1123
958
|
|
|
959
|
+
/** @deprecated Use findTaskByVcs instead */
|
|
960
|
+
export const findTaskByGitHub = findTaskByVcs;
|
|
961
|
+
|
|
1124
962
|
export interface TaskFilters {
|
|
1125
963
|
status?: AgentTaskStatus;
|
|
1126
964
|
agentId?: string;
|
|
965
|
+
epicId?: string;
|
|
1127
966
|
search?: string;
|
|
1128
967
|
// New filters
|
|
1129
968
|
unassigned?: boolean;
|
|
@@ -1131,7 +970,10 @@ export interface TaskFilters {
|
|
|
1131
970
|
readyOnly?: boolean;
|
|
1132
971
|
taskType?: string;
|
|
1133
972
|
tags?: string[];
|
|
973
|
+
scheduleId?: string;
|
|
1134
974
|
limit?: number;
|
|
975
|
+
offset?: number;
|
|
976
|
+
includeHeartbeat?: boolean;
|
|
1135
977
|
}
|
|
1136
978
|
|
|
1137
979
|
export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
@@ -1148,6 +990,11 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1148
990
|
params.push(filters.agentId);
|
|
1149
991
|
}
|
|
1150
992
|
|
|
993
|
+
if (filters?.epicId) {
|
|
994
|
+
conditions.push("epicId = ?");
|
|
995
|
+
params.push(filters.epicId);
|
|
996
|
+
}
|
|
997
|
+
|
|
1151
998
|
if (filters?.search) {
|
|
1152
999
|
conditions.push("task LIKE ?");
|
|
1153
1000
|
params.push(`%${filters.search}%`);
|
|
@@ -1177,9 +1024,20 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1177
1024
|
}
|
|
1178
1025
|
}
|
|
1179
1026
|
|
|
1027
|
+
if (filters?.scheduleId) {
|
|
1028
|
+
conditions.push("scheduleId = ?");
|
|
1029
|
+
params.push(filters.scheduleId);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Exclude heartbeat tasks by default
|
|
1033
|
+
if (!filters?.includeHeartbeat) {
|
|
1034
|
+
conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1180
1037
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1181
1038
|
const limit = filters?.limit ?? 25;
|
|
1182
|
-
const
|
|
1039
|
+
const offset = filters?.offset ?? 0;
|
|
1040
|
+
const query = `SELECT * FROM agent_tasks ${whereClause} ORDER BY lastUpdatedAt DESC, priority DESC LIMIT ${limit} OFFSET ${offset}`;
|
|
1183
1041
|
|
|
1184
1042
|
let tasks = getDb()
|
|
1185
1043
|
.prepare<AgentTaskRow, (string | AgentTaskStatus)[]>(query)
|
|
@@ -1215,6 +1073,11 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1215
1073
|
params.push(filters.agentId);
|
|
1216
1074
|
}
|
|
1217
1075
|
|
|
1076
|
+
if (filters?.epicId) {
|
|
1077
|
+
conditions.push("epicId = ?");
|
|
1078
|
+
params.push(filters.epicId);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1218
1081
|
if (filters?.search) {
|
|
1219
1082
|
conditions.push("task LIKE ?");
|
|
1220
1083
|
params.push(`%${filters.search}%`);
|
|
@@ -1242,6 +1105,16 @@ export function getTasksCount(filters?: Omit<TaskFilters, "limit" | "readyOnly">
|
|
|
1242
1105
|
}
|
|
1243
1106
|
}
|
|
1244
1107
|
|
|
1108
|
+
if (filters?.scheduleId) {
|
|
1109
|
+
conditions.push("scheduleId = ?");
|
|
1110
|
+
params.push(filters.scheduleId);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Exclude heartbeat tasks by default
|
|
1114
|
+
if (!filters?.includeHeartbeat) {
|
|
1115
|
+
conditions.push("(IFNULL(taskType, '') != 'heartbeat' AND tags NOT LIKE '%\"heartbeat\"%')");
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1245
1118
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1246
1119
|
const query = `SELECT COUNT(*) as count FROM agent_tasks ${whereClause}`;
|
|
1247
1120
|
|
|
@@ -1315,10 +1188,10 @@ export function getCompletedSlackTasks(): AgentTask[] {
|
|
|
1315
1188
|
return getDb()
|
|
1316
1189
|
.prepare<AgentTaskRow, []>(
|
|
1317
1190
|
`SELECT * FROM agent_tasks
|
|
1318
|
-
WHERE
|
|
1319
|
-
AND slackChannelId IS NOT NULL
|
|
1191
|
+
WHERE slackChannelId IS NOT NULL
|
|
1320
1192
|
AND status IN ('completed', 'failed')
|
|
1321
|
-
ORDER BY lastUpdatedAt DESC
|
|
1193
|
+
ORDER BY lastUpdatedAt DESC
|
|
1194
|
+
LIMIT 200`,
|
|
1322
1195
|
)
|
|
1323
1196
|
.all()
|
|
1324
1197
|
.map(rowToAgentTask);
|
|
@@ -1363,33 +1236,51 @@ export function markTasksNotified(taskIds: string[]): number {
|
|
|
1363
1236
|
return result.changes;
|
|
1364
1237
|
}
|
|
1365
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Reset notifiedAt for tasks, allowing them to be re-delivered on next poll.
|
|
1241
|
+
* Used when a trigger was consumed but the session that should process it failed.
|
|
1242
|
+
* This prevents permanent notification loss from the mark-before-process race.
|
|
1243
|
+
*/
|
|
1244
|
+
export function resetTasksNotified(taskIds: string[]): number {
|
|
1245
|
+
if (taskIds.length === 0) return 0;
|
|
1246
|
+
|
|
1247
|
+
const placeholders = taskIds.map(() => "?").join(",");
|
|
1248
|
+
|
|
1249
|
+
const result = getDb().run(
|
|
1250
|
+
`UPDATE agent_tasks SET notifiedAt = NULL
|
|
1251
|
+
WHERE id IN (${placeholders}) AND notifiedAt IS NOT NULL`,
|
|
1252
|
+
taskIds,
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
return result.changes;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1366
1258
|
export function getInProgressSlackTasks(): AgentTask[] {
|
|
1367
1259
|
return getDb()
|
|
1368
1260
|
.prepare<AgentTaskRow, []>(
|
|
1369
1261
|
`SELECT * FROM agent_tasks
|
|
1370
|
-
WHERE
|
|
1371
|
-
AND slackChannelId IS NOT NULL
|
|
1262
|
+
WHERE slackChannelId IS NOT NULL
|
|
1372
1263
|
AND status = 'in_progress'
|
|
1373
|
-
ORDER BY lastUpdatedAt DESC
|
|
1264
|
+
ORDER BY lastUpdatedAt DESC
|
|
1265
|
+
LIMIT 200`,
|
|
1374
1266
|
)
|
|
1375
1267
|
.all()
|
|
1376
1268
|
.map(rowToAgentTask);
|
|
1377
1269
|
}
|
|
1378
1270
|
|
|
1379
1271
|
/**
|
|
1380
|
-
* Find
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1272
|
+
* Find the most recent agent associated with a specific Slack thread.
|
|
1273
|
+
* No status filter — returns the last agent that touched this thread regardless of task state.
|
|
1274
|
+
* This is intentional: follow-up messages should route to the same agent even after task completion.
|
|
1275
|
+
* Callers (e.g. assistant.ts) apply their own status checks (e.g. agent.status !== "offline").
|
|
1383
1276
|
*/
|
|
1384
1277
|
export function getAgentWorkingOnThread(channelId: string, threadTs: string): Agent | null {
|
|
1385
|
-
// First check tasks (for workers)
|
|
1386
1278
|
const taskRow = getDb()
|
|
1387
1279
|
.prepare<AgentTaskRow, [string, string]>(
|
|
1388
1280
|
`SELECT * FROM agent_tasks
|
|
1389
1281
|
WHERE source = 'slack'
|
|
1390
1282
|
AND slackChannelId = ?
|
|
1391
1283
|
AND slackThreadTs = ?
|
|
1392
|
-
AND status IN ('in_progress', 'pending')
|
|
1393
1284
|
ORDER BY createdAt DESC
|
|
1394
1285
|
LIMIT 1`,
|
|
1395
1286
|
)
|
|
@@ -1397,21 +1288,45 @@ export function getAgentWorkingOnThread(channelId: string, threadTs: string): Ag
|
|
|
1397
1288
|
|
|
1398
1289
|
if (taskRow?.agentId) return getAgentById(taskRow.agentId);
|
|
1399
1290
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Find the latest active (in_progress or pending) task in a specific Slack thread.
|
|
1296
|
+
* Used for dependency chaining in additive Slack buffer.
|
|
1297
|
+
*/
|
|
1298
|
+
export function getLatestActiveTaskInThread(channelId: string, threadTs: string): AgentTask | null {
|
|
1299
|
+
const row = getDb()
|
|
1300
|
+
.prepare<AgentTaskRow, [string, string]>(
|
|
1301
|
+
`SELECT * FROM agent_tasks
|
|
1404
1302
|
WHERE source = 'slack'
|
|
1405
1303
|
AND slackChannelId = ?
|
|
1406
1304
|
AND slackThreadTs = ?
|
|
1407
|
-
|
|
1305
|
+
AND status IN ('in_progress', 'pending')
|
|
1306
|
+
ORDER BY createdAt DESC, rowid DESC
|
|
1408
1307
|
LIMIT 1`,
|
|
1409
1308
|
)
|
|
1410
1309
|
.get(channelId, threadTs);
|
|
1411
1310
|
|
|
1412
|
-
|
|
1311
|
+
return row ? rowToAgentTask(row) : null;
|
|
1312
|
+
}
|
|
1413
1313
|
|
|
1414
|
-
|
|
1314
|
+
/**
|
|
1315
|
+
* Find the most recent task in a Slack thread, regardless of source or status.
|
|
1316
|
+
* Unlike getAgentWorkingOnThread (which filters source='slack'), this finds ALL tasks
|
|
1317
|
+
* including worker tasks that inherited Slack metadata via parentTaskId.
|
|
1318
|
+
*/
|
|
1319
|
+
export function getMostRecentTaskInThread(channelId: string, threadTs: string): AgentTask | null {
|
|
1320
|
+
const row = getDb()
|
|
1321
|
+
.prepare<AgentTaskRow, [string, string]>(
|
|
1322
|
+
`SELECT * FROM agent_tasks
|
|
1323
|
+
WHERE slackChannelId = ?
|
|
1324
|
+
AND slackThreadTs = ?
|
|
1325
|
+
ORDER BY createdAt DESC
|
|
1326
|
+
LIMIT 1`,
|
|
1327
|
+
)
|
|
1328
|
+
.get(channelId, threadTs);
|
|
1329
|
+
return row ? rowToAgentTask(row) : null;
|
|
1415
1330
|
}
|
|
1416
1331
|
|
|
1417
1332
|
export function completeTask(id: string, output?: string): AgentTask | null {
|
|
@@ -1434,6 +1349,17 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
1434
1349
|
newValue: "completed",
|
|
1435
1350
|
});
|
|
1436
1351
|
} catch {}
|
|
1352
|
+
try {
|
|
1353
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
1354
|
+
workflowEventBus.emit("task.completed", {
|
|
1355
|
+
taskId: id,
|
|
1356
|
+
output,
|
|
1357
|
+
agentId: row.agentId,
|
|
1358
|
+
workflowRunId: row.workflowRunId,
|
|
1359
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
} catch {}
|
|
1437
1363
|
}
|
|
1438
1364
|
|
|
1439
1365
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -1454,6 +1380,17 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
1454
1380
|
metadata: { reason },
|
|
1455
1381
|
});
|
|
1456
1382
|
} catch {}
|
|
1383
|
+
try {
|
|
1384
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
1385
|
+
workflowEventBus.emit("task.failed", {
|
|
1386
|
+
taskId: id,
|
|
1387
|
+
failureReason: reason,
|
|
1388
|
+
agentId: row.agentId,
|
|
1389
|
+
workflowRunId: row.workflowRunId,
|
|
1390
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
} catch {}
|
|
1457
1394
|
}
|
|
1458
1395
|
return row ? rowToAgentTask(row) : null;
|
|
1459
1396
|
}
|
|
@@ -1462,8 +1399,9 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
1462
1399
|
const oldTask = getTaskById(id);
|
|
1463
1400
|
if (!oldTask) return null;
|
|
1464
1401
|
|
|
1465
|
-
// Only cancel tasks that are in
|
|
1466
|
-
|
|
1402
|
+
// Only cancel tasks that are not already in a terminal state
|
|
1403
|
+
const terminalStatuses = ["completed", "failed", "cancelled"];
|
|
1404
|
+
if (terminalStatuses.includes(oldTask.status)) {
|
|
1467
1405
|
return null;
|
|
1468
1406
|
}
|
|
1469
1407
|
|
|
@@ -1482,6 +1420,16 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
1482
1420
|
metadata: reason ? { reason } : undefined,
|
|
1483
1421
|
});
|
|
1484
1422
|
} catch {}
|
|
1423
|
+
try {
|
|
1424
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
1425
|
+
workflowEventBus.emit("task.cancelled", {
|
|
1426
|
+
taskId: id,
|
|
1427
|
+
agentId: row.agentId,
|
|
1428
|
+
workflowRunId: row.workflowRunId,
|
|
1429
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
} catch {}
|
|
1485
1433
|
}
|
|
1486
1434
|
|
|
1487
1435
|
return row ? rowToAgentTask(row) : null;
|
|
@@ -1612,6 +1560,15 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
|
|
|
1612
1560
|
newValue: progress,
|
|
1613
1561
|
});
|
|
1614
1562
|
} catch {}
|
|
1563
|
+
try {
|
|
1564
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
1565
|
+
workflowEventBus.emit("task.progress", {
|
|
1566
|
+
taskId: id,
|
|
1567
|
+
progress,
|
|
1568
|
+
agentId: row.agentId,
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
} catch {}
|
|
1615
1572
|
}
|
|
1616
1573
|
return row ? rowToAgentTask(row) : null;
|
|
1617
1574
|
}
|
|
@@ -1770,15 +1727,62 @@ export interface CreateTaskOptions {
|
|
|
1770
1727
|
slackChannelId?: string;
|
|
1771
1728
|
slackThreadTs?: string;
|
|
1772
1729
|
slackUserId?: string;
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1730
|
+
vcsProvider?: "github" | "gitlab";
|
|
1731
|
+
vcsRepo?: string;
|
|
1732
|
+
vcsEventType?: string;
|
|
1733
|
+
vcsNumber?: number;
|
|
1734
|
+
vcsCommentId?: number;
|
|
1735
|
+
vcsAuthor?: string;
|
|
1736
|
+
vcsUrl?: string;
|
|
1737
|
+
agentmailInboxId?: string;
|
|
1738
|
+
agentmailMessageId?: string;
|
|
1739
|
+
agentmailThreadId?: string;
|
|
1779
1740
|
mentionMessageId?: string;
|
|
1780
1741
|
mentionChannelId?: string;
|
|
1781
1742
|
epicId?: string;
|
|
1743
|
+
dir?: string;
|
|
1744
|
+
parentTaskId?: string;
|
|
1745
|
+
model?: string;
|
|
1746
|
+
scheduleId?: string;
|
|
1747
|
+
workflowRunId?: string;
|
|
1748
|
+
workflowRunStepId?: string;
|
|
1749
|
+
sourceTaskId?: string;
|
|
1750
|
+
outputSchema?: Record<string, unknown>;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Find recent tasks within a time window for deduplication checks.
|
|
1755
|
+
* Returns tasks created in the last N minutes, optionally filtered by creator or target agent.
|
|
1756
|
+
*/
|
|
1757
|
+
export function findRecentSimilarTasks(opts: {
|
|
1758
|
+
windowMinutes?: number;
|
|
1759
|
+
creatorAgentId?: string;
|
|
1760
|
+
agentId?: string;
|
|
1761
|
+
limit?: number;
|
|
1762
|
+
}): AgentTask[] {
|
|
1763
|
+
const since = new Date(Date.now() - (opts.windowMinutes ?? 10) * 60 * 1000).toISOString();
|
|
1764
|
+
const conditions: string[] = ["createdAt > ?"];
|
|
1765
|
+
const params: (string | number)[] = [since];
|
|
1766
|
+
|
|
1767
|
+
// Exclude completed/failed/cancelled tasks — only active or recently created
|
|
1768
|
+
conditions.push("status NOT IN ('completed', 'failed', 'cancelled')");
|
|
1769
|
+
|
|
1770
|
+
if (opts.creatorAgentId) {
|
|
1771
|
+
conditions.push("creatorAgentId = ?");
|
|
1772
|
+
params.push(opts.creatorAgentId);
|
|
1773
|
+
}
|
|
1774
|
+
if (opts.agentId) {
|
|
1775
|
+
conditions.push("agentId = ?");
|
|
1776
|
+
params.push(opts.agentId);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const limit = opts.limit ?? 50;
|
|
1780
|
+
const query = `SELECT * FROM agent_tasks WHERE ${conditions.join(" AND ")} ORDER BY createdAt DESC LIMIT ${limit}`;
|
|
1781
|
+
|
|
1782
|
+
return getDb()
|
|
1783
|
+
.prepare<AgentTaskRow, (string | number)[]>(query)
|
|
1784
|
+
.all(...params)
|
|
1785
|
+
.map(rowToAgentTask);
|
|
1782
1786
|
}
|
|
1783
1787
|
|
|
1784
1788
|
export function createTaskExtended(task: string, options?: CreateTaskOptions): AgentTask {
|
|
@@ -1792,15 +1796,51 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1792
1796
|
? "backlog"
|
|
1793
1797
|
: "unassigned";
|
|
1794
1798
|
|
|
1799
|
+
// Inherit Slack/AgentMail metadata from parent task (unless explicitly overridden)
|
|
1800
|
+
if (options?.parentTaskId) {
|
|
1801
|
+
const parent = getTaskById(options.parentTaskId);
|
|
1802
|
+
if (parent) {
|
|
1803
|
+
if (parent.slackChannelId && !options.slackChannelId) {
|
|
1804
|
+
options.slackChannelId = parent.slackChannelId;
|
|
1805
|
+
}
|
|
1806
|
+
if (parent.slackThreadTs && !options.slackThreadTs) {
|
|
1807
|
+
options.slackThreadTs = parent.slackThreadTs;
|
|
1808
|
+
}
|
|
1809
|
+
if (parent.slackUserId && !options.slackUserId) {
|
|
1810
|
+
options.slackUserId = parent.slackUserId;
|
|
1811
|
+
}
|
|
1812
|
+
if (parent.agentmailInboxId && !options.agentmailInboxId) {
|
|
1813
|
+
options.agentmailInboxId = parent.agentmailInboxId;
|
|
1814
|
+
}
|
|
1815
|
+
if (parent.agentmailThreadId && !options.agentmailThreadId) {
|
|
1816
|
+
options.agentmailThreadId = parent.agentmailThreadId;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Auto-inherit Slack metadata from the creator's source task (deterministic via sourceTaskId)
|
|
1822
|
+
// Priority: explicit params > parentTaskId inheritance > sourceTaskId lookup
|
|
1823
|
+
// sourceTaskId is set by the adapter's X-Source-Task-Id header — each adapter carries its taskId natively
|
|
1824
|
+
if (options?.creatorAgentId && !options.slackChannelId && options.sourceTaskId) {
|
|
1825
|
+
const sourceTask = getTaskById(options.sourceTaskId);
|
|
1826
|
+
if (sourceTask?.slackChannelId) {
|
|
1827
|
+
options.slackChannelId = sourceTask.slackChannelId;
|
|
1828
|
+
options.slackThreadTs = sourceTask.slackThreadTs;
|
|
1829
|
+
options.slackUserId = sourceTask.slackUserId;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1795
1833
|
const row = getDb()
|
|
1796
1834
|
.prepare<AgentTaskRow, (string | number | null)[]>(
|
|
1797
1835
|
`INSERT INTO agent_tasks (
|
|
1798
1836
|
id, agentId, creatorAgentId, task, status, source,
|
|
1799
1837
|
taskType, tags, priority, dependsOn, offeredTo, offeredAt,
|
|
1800
1838
|
slackChannelId, slackThreadTs, slackUserId,
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1839
|
+
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
1840
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
1841
|
+
mentionMessageId, mentionChannelId, epicId, dir, parentTaskId, model, scheduleId,
|
|
1842
|
+
workflowRunId, workflowRunStepId, outputSchema, createdAt, lastUpdatedAt
|
|
1843
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1804
1844
|
)
|
|
1805
1845
|
.get(
|
|
1806
1846
|
id,
|
|
@@ -1818,15 +1858,26 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1818
1858
|
options?.slackChannelId ?? null,
|
|
1819
1859
|
options?.slackThreadTs ?? null,
|
|
1820
1860
|
options?.slackUserId ?? null,
|
|
1821
|
-
options?.
|
|
1822
|
-
options?.
|
|
1823
|
-
options?.
|
|
1824
|
-
options?.
|
|
1825
|
-
options?.
|
|
1826
|
-
options?.
|
|
1861
|
+
options?.vcsProvider ?? null,
|
|
1862
|
+
options?.vcsRepo ?? null,
|
|
1863
|
+
options?.vcsEventType ?? null,
|
|
1864
|
+
options?.vcsNumber ?? null,
|
|
1865
|
+
options?.vcsCommentId ?? null,
|
|
1866
|
+
options?.vcsAuthor ?? null,
|
|
1867
|
+
options?.vcsUrl ?? null,
|
|
1868
|
+
options?.agentmailInboxId ?? null,
|
|
1869
|
+
options?.agentmailMessageId ?? null,
|
|
1870
|
+
options?.agentmailThreadId ?? null,
|
|
1827
1871
|
options?.mentionMessageId ?? null,
|
|
1828
1872
|
options?.mentionChannelId ?? null,
|
|
1829
1873
|
options?.epicId ?? null,
|
|
1874
|
+
options?.dir ?? null,
|
|
1875
|
+
options?.parentTaskId ?? null,
|
|
1876
|
+
options?.model ?? null,
|
|
1877
|
+
options?.scheduleId ?? null,
|
|
1878
|
+
options?.workflowRunId ?? null,
|
|
1879
|
+
options?.workflowRunStepId ?? null,
|
|
1880
|
+
options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
|
|
1830
1881
|
now,
|
|
1831
1882
|
now,
|
|
1832
1883
|
);
|
|
@@ -1843,18 +1894,32 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
1843
1894
|
});
|
|
1844
1895
|
} catch {}
|
|
1845
1896
|
|
|
1897
|
+
try {
|
|
1898
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
1899
|
+
workflowEventBus.emit("task.created", {
|
|
1900
|
+
taskId: row.id,
|
|
1901
|
+
task: row.task,
|
|
1902
|
+
source: row.source,
|
|
1903
|
+
tags: options?.tags ?? [],
|
|
1904
|
+
agentId: row.agentId,
|
|
1905
|
+
workflowRunId: row.workflowRunId,
|
|
1906
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
1907
|
+
});
|
|
1908
|
+
});
|
|
1909
|
+
} catch {}
|
|
1910
|
+
|
|
1846
1911
|
return rowToAgentTask(row);
|
|
1847
1912
|
}
|
|
1848
1913
|
|
|
1849
1914
|
export function claimTask(taskId: string, agentId: string): AgentTask | null {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1915
|
+
// Atomic claim: single UPDATE with WHERE guard ensures exactly-once claiming.
|
|
1916
|
+
// No pre-read needed — the WHERE clause handles the race condition.
|
|
1917
|
+
// Status goes directly to 'in_progress' because the claiming session is
|
|
1918
|
+
// already working on the task (prevents duplicate task_assigned triggers).
|
|
1854
1919
|
const now = new Date().toISOString();
|
|
1855
1920
|
const row = getDb()
|
|
1856
1921
|
.prepare<AgentTaskRow, [string, string, string]>(
|
|
1857
|
-
`UPDATE agent_tasks SET agentId = ?, status = '
|
|
1922
|
+
`UPDATE agent_tasks SET agentId = ?, status = 'in_progress', lastUpdatedAt = ?
|
|
1858
1923
|
WHERE id = ? AND status = 'unassigned' RETURNING *`,
|
|
1859
1924
|
)
|
|
1860
1925
|
.get(agentId, now, taskId);
|
|
@@ -1866,7 +1931,7 @@ export function claimTask(taskId: string, agentId: string): AgentTask | null {
|
|
|
1866
1931
|
agentId,
|
|
1867
1932
|
taskId,
|
|
1868
1933
|
oldValue: "unassigned",
|
|
1869
|
-
newValue: "
|
|
1934
|
+
newValue: "in_progress",
|
|
1870
1935
|
});
|
|
1871
1936
|
} catch {}
|
|
1872
1937
|
}
|
|
@@ -1877,13 +1942,14 @@ export function claimTask(taskId: string, agentId: string): AgentTask | null {
|
|
|
1877
1942
|
export function releaseTask(taskId: string): AgentTask | null {
|
|
1878
1943
|
const task = getTaskById(taskId);
|
|
1879
1944
|
if (!task) return null;
|
|
1880
|
-
|
|
1945
|
+
// Allow releasing both 'pending' (directly assigned) and 'in_progress' (pool-claimed) tasks
|
|
1946
|
+
if (task.status !== "pending" && task.status !== "in_progress") return null;
|
|
1881
1947
|
|
|
1882
1948
|
const now = new Date().toISOString();
|
|
1883
1949
|
const row = getDb()
|
|
1884
1950
|
.prepare<AgentTaskRow, [string, string]>(
|
|
1885
1951
|
`UPDATE agent_tasks SET agentId = NULL, status = 'unassigned', lastUpdatedAt = ?
|
|
1886
|
-
WHERE id = ? AND status
|
|
1952
|
+
WHERE id = ? AND status IN ('pending', 'in_progress') RETURNING *`,
|
|
1887
1953
|
)
|
|
1888
1954
|
.get(now, taskId);
|
|
1889
1955
|
|
|
@@ -1893,7 +1959,7 @@ export function releaseTask(taskId: string): AgentTask | null {
|
|
|
1893
1959
|
eventType: "task_released",
|
|
1894
1960
|
agentId: task.agentId ?? undefined,
|
|
1895
1961
|
taskId,
|
|
1896
|
-
oldValue:
|
|
1962
|
+
oldValue: task.status,
|
|
1897
1963
|
newValue: "unassigned",
|
|
1898
1964
|
});
|
|
1899
1965
|
} catch {}
|
|
@@ -2095,6 +2161,16 @@ export function getUnassignedTasksCount(): number {
|
|
|
2095
2161
|
return result?.count ?? 0;
|
|
2096
2162
|
}
|
|
2097
2163
|
|
|
2164
|
+
/** Get unassigned task IDs, ordered by priority (highest first) then creation time */
|
|
2165
|
+
export function getUnassignedTaskIds(limit = 10): string[] {
|
|
2166
|
+
const rows = getDb()
|
|
2167
|
+
.prepare<{ id: string }, [number]>(
|
|
2168
|
+
"SELECT id FROM agent_tasks WHERE status = 'unassigned' ORDER BY priority DESC, createdAt ASC LIMIT ?",
|
|
2169
|
+
)
|
|
2170
|
+
.all(limit);
|
|
2171
|
+
return rows.map((r) => r.id);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2098
2174
|
// ============================================================================
|
|
2099
2175
|
// Dependency Checking
|
|
2100
2176
|
// ============================================================================
|
|
@@ -2123,50 +2199,14 @@ export function checkDependencies(taskId: string): {
|
|
|
2123
2199
|
// Agent Profile Operations
|
|
2124
2200
|
// ============================================================================
|
|
2125
2201
|
|
|
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
|
-
}
|
|
2202
|
+
// Default markdown template generators moved to src/prompts/defaults.ts
|
|
2203
|
+
// Re-export for backwards compatibility with any external consumers
|
|
2204
|
+
export {
|
|
2205
|
+
generateDefaultClaudeMd,
|
|
2206
|
+
generateDefaultIdentityMd,
|
|
2207
|
+
generateDefaultSoulMd,
|
|
2208
|
+
generateDefaultToolsMd,
|
|
2209
|
+
} from "../prompts/defaults.ts";
|
|
2170
2210
|
|
|
2171
2211
|
export function updateAgentProfile(
|
|
2172
2212
|
id: string,
|
|
@@ -2175,51 +2215,110 @@ export function updateAgentProfile(
|
|
|
2175
2215
|
role?: string;
|
|
2176
2216
|
capabilities?: string[];
|
|
2177
2217
|
claudeMd?: string;
|
|
2218
|
+
soulMd?: string;
|
|
2219
|
+
identityMd?: string;
|
|
2220
|
+
setupScript?: string;
|
|
2221
|
+
toolsMd?: string;
|
|
2178
2222
|
},
|
|
2223
|
+
meta?: VersionMeta,
|
|
2179
2224
|
): Agent | null {
|
|
2180
|
-
const
|
|
2181
|
-
if (!agent) return null;
|
|
2225
|
+
const database = getDb();
|
|
2182
2226
|
|
|
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
|
-
);
|
|
2227
|
+
return database.transaction(() => {
|
|
2228
|
+
// Get current agent state for version comparison
|
|
2229
|
+
const current = database
|
|
2230
|
+
.prepare<AgentRow, [string]>("SELECT * FROM agents WHERE id = ?")
|
|
2231
|
+
.get(id);
|
|
2232
|
+
if (!current) return null;
|
|
2205
2233
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2234
|
+
// Create context versions for changed fields
|
|
2235
|
+
for (const field of VERSIONABLE_FIELDS) {
|
|
2236
|
+
const newValue = updates[field];
|
|
2237
|
+
if (newValue === undefined || newValue === null) continue;
|
|
2208
2238
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
.prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
|
|
2213
|
-
.get(newName, id);
|
|
2239
|
+
const currentValue = current[field] ?? "";
|
|
2240
|
+
const newHash = computeContentHash(newValue);
|
|
2241
|
+
const currentHash = computeContentHash(currentValue);
|
|
2214
2242
|
|
|
2215
|
-
|
|
2216
|
-
throw new Error("Agent name already exists");
|
|
2217
|
-
}
|
|
2243
|
+
if (newHash === currentHash) continue; // No actual change
|
|
2218
2244
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2245
|
+
const latestVersion = getLatestContextVersion(id, field);
|
|
2246
|
+
const version = (latestVersion?.version ?? 0) + 1;
|
|
2247
|
+
|
|
2248
|
+
createContextVersion({
|
|
2249
|
+
agentId: id,
|
|
2250
|
+
field,
|
|
2251
|
+
content: newValue,
|
|
2252
|
+
version,
|
|
2253
|
+
changeSource: meta?.changeSource ?? "api",
|
|
2254
|
+
changedByAgentId: meta?.changedByAgentId ?? null,
|
|
2255
|
+
changeReason: meta?.changeReason ?? null,
|
|
2256
|
+
contentHash: newHash,
|
|
2257
|
+
previousVersionId: latestVersion?.id ?? null,
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Proceed with existing UPDATE logic
|
|
2262
|
+
const now = new Date().toISOString();
|
|
2263
|
+
const row = database
|
|
2264
|
+
.prepare<
|
|
2265
|
+
AgentRow,
|
|
2266
|
+
[
|
|
2267
|
+
string | null,
|
|
2268
|
+
string | null,
|
|
2269
|
+
string | null,
|
|
2270
|
+
string | null,
|
|
2271
|
+
string | null,
|
|
2272
|
+
string | null,
|
|
2273
|
+
string | null,
|
|
2274
|
+
string | null,
|
|
2275
|
+
string,
|
|
2276
|
+
string,
|
|
2277
|
+
]
|
|
2278
|
+
>(
|
|
2279
|
+
`UPDATE agents SET
|
|
2280
|
+
description = COALESCE(?, description),
|
|
2281
|
+
role = COALESCE(?, role),
|
|
2282
|
+
capabilities = COALESCE(?, capabilities),
|
|
2283
|
+
claudeMd = COALESCE(?, claudeMd),
|
|
2284
|
+
soulMd = COALESCE(?, soulMd),
|
|
2285
|
+
identityMd = COALESCE(?, identityMd),
|
|
2286
|
+
setupScript = COALESCE(?, setupScript),
|
|
2287
|
+
toolsMd = COALESCE(?, toolsMd),
|
|
2288
|
+
lastUpdatedAt = ?
|
|
2289
|
+
WHERE id = ? RETURNING *`,
|
|
2290
|
+
)
|
|
2291
|
+
.get(
|
|
2292
|
+
updates.description ?? null,
|
|
2293
|
+
updates.role ?? null,
|
|
2294
|
+
updates.capabilities ? JSON.stringify(updates.capabilities) : null,
|
|
2295
|
+
updates.claudeMd ?? null,
|
|
2296
|
+
updates.soulMd ?? null,
|
|
2297
|
+
updates.identityMd ?? null,
|
|
2298
|
+
updates.setupScript ?? null,
|
|
2299
|
+
updates.toolsMd ?? null,
|
|
2300
|
+
now,
|
|
2301
|
+
id,
|
|
2302
|
+
);
|
|
2303
|
+
|
|
2304
|
+
return row ? rowToAgent(row) : null;
|
|
2305
|
+
})();
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
export function updateAgentName(id: string, newName: string): Agent | null {
|
|
2309
|
+
// Check if another agent already has this name
|
|
2310
|
+
const existingAgent = getDb()
|
|
2311
|
+
.prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
|
|
2312
|
+
.get(newName, id);
|
|
2313
|
+
|
|
2314
|
+
if (existingAgent) {
|
|
2315
|
+
throw new Error("Agent name already exists");
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const now = new Date().toISOString();
|
|
2319
|
+
const row = getDb()
|
|
2320
|
+
.prepare<AgentRow, [string, string, string]>(
|
|
2321
|
+
"UPDATE agents SET name = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *",
|
|
2223
2322
|
)
|
|
2224
2323
|
.get(newName, now, id);
|
|
2225
2324
|
|
|
@@ -2337,6 +2436,11 @@ export function getAllChannels(): Channel[] {
|
|
|
2337
2436
|
.map(rowToChannel);
|
|
2338
2437
|
}
|
|
2339
2438
|
|
|
2439
|
+
export function deleteChannel(id: string): boolean {
|
|
2440
|
+
const result = getDb().prepare("DELETE FROM channels WHERE id = ?").run(id);
|
|
2441
|
+
return result.changes > 0;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2340
2444
|
export function postMessage(
|
|
2341
2445
|
channelId: string,
|
|
2342
2446
|
agentId: string | null,
|
|
@@ -3183,8 +3287,8 @@ const sessionCostQueries = {
|
|
|
3183
3287
|
),
|
|
3184
3288
|
|
|
3185
3289
|
getByTaskId: () =>
|
|
3186
|
-
getDb().prepare<SessionCostRow, [string]>(
|
|
3187
|
-
"SELECT * FROM session_costs WHERE taskId = ? ORDER BY createdAt DESC",
|
|
3290
|
+
getDb().prepare<SessionCostRow, [string, number]>(
|
|
3291
|
+
"SELECT * FROM session_costs WHERE taskId = ? ORDER BY createdAt DESC LIMIT ?",
|
|
3188
3292
|
),
|
|
3189
3293
|
|
|
3190
3294
|
getByAgentId: () =>
|
|
@@ -3251,8 +3355,8 @@ export function createSessionCost(input: CreateSessionCostInput): SessionCost {
|
|
|
3251
3355
|
};
|
|
3252
3356
|
}
|
|
3253
3357
|
|
|
3254
|
-
export function getSessionCostsByTaskId(taskId: string): SessionCost[] {
|
|
3255
|
-
return sessionCostQueries.getByTaskId().all(taskId).map(rowToSessionCost);
|
|
3358
|
+
export function getSessionCostsByTaskId(taskId: string, limit = 500): SessionCost[] {
|
|
3359
|
+
return sessionCostQueries.getByTaskId().all(taskId, limit).map(rowToSessionCost);
|
|
3256
3360
|
}
|
|
3257
3361
|
|
|
3258
3362
|
export function getSessionCostsByAgentId(agentId: string, limit = 100): SessionCost[] {
|
|
@@ -3263,6 +3367,224 @@ export function getAllSessionCosts(limit = 100): SessionCost[] {
|
|
|
3263
3367
|
return sessionCostQueries.getAll().all(limit).map(rowToSessionCost);
|
|
3264
3368
|
}
|
|
3265
3369
|
|
|
3370
|
+
// --- Date-filtered session costs (P1) ---
|
|
3371
|
+
|
|
3372
|
+
export function getSessionCostsFiltered(opts: {
|
|
3373
|
+
agentId?: string;
|
|
3374
|
+
startDate?: string;
|
|
3375
|
+
endDate?: string;
|
|
3376
|
+
limit?: number;
|
|
3377
|
+
}): SessionCost[] {
|
|
3378
|
+
const conditions: string[] = [];
|
|
3379
|
+
const params: (string | number)[] = [];
|
|
3380
|
+
|
|
3381
|
+
if (opts.agentId) {
|
|
3382
|
+
conditions.push("agentId = ?");
|
|
3383
|
+
params.push(opts.agentId);
|
|
3384
|
+
}
|
|
3385
|
+
if (opts.startDate) {
|
|
3386
|
+
conditions.push("createdAt >= ?");
|
|
3387
|
+
params.push(opts.startDate);
|
|
3388
|
+
}
|
|
3389
|
+
if (opts.endDate) {
|
|
3390
|
+
conditions.push("createdAt <= ?");
|
|
3391
|
+
params.push(opts.endDate);
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3395
|
+
const limit = opts.limit ?? 100;
|
|
3396
|
+
params.push(limit);
|
|
3397
|
+
|
|
3398
|
+
return getDb()
|
|
3399
|
+
.prepare<SessionCostRow, (string | number)[]>(
|
|
3400
|
+
`SELECT * FROM session_costs ${where} ORDER BY createdAt DESC LIMIT ?`,
|
|
3401
|
+
)
|
|
3402
|
+
.all(...params)
|
|
3403
|
+
.map(rowToSessionCost);
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
// --- Aggregation queries (P0) ---
|
|
3407
|
+
|
|
3408
|
+
export interface SessionCostSummaryTotals {
|
|
3409
|
+
totalCostUsd: number;
|
|
3410
|
+
totalInputTokens: number;
|
|
3411
|
+
totalOutputTokens: number;
|
|
3412
|
+
totalCacheReadTokens: number;
|
|
3413
|
+
totalCacheWriteTokens: number;
|
|
3414
|
+
totalDurationMs: number;
|
|
3415
|
+
totalSessions: number;
|
|
3416
|
+
avgCostPerSession: number;
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
export interface SessionCostDailyRow {
|
|
3420
|
+
date: string;
|
|
3421
|
+
costUsd: number;
|
|
3422
|
+
inputTokens: number;
|
|
3423
|
+
outputTokens: number;
|
|
3424
|
+
sessions: number;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
export interface SessionCostByAgentRow {
|
|
3428
|
+
agentId: string;
|
|
3429
|
+
costUsd: number;
|
|
3430
|
+
inputTokens: number;
|
|
3431
|
+
outputTokens: number;
|
|
3432
|
+
sessions: number;
|
|
3433
|
+
durationMs: number;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
export function getSessionCostSummary(opts: {
|
|
3437
|
+
startDate?: string;
|
|
3438
|
+
endDate?: string;
|
|
3439
|
+
agentId?: string;
|
|
3440
|
+
groupBy?: "day" | "agent" | "both";
|
|
3441
|
+
}): {
|
|
3442
|
+
totals: SessionCostSummaryTotals;
|
|
3443
|
+
daily: SessionCostDailyRow[];
|
|
3444
|
+
byAgent: SessionCostByAgentRow[];
|
|
3445
|
+
} {
|
|
3446
|
+
const conditions: string[] = [];
|
|
3447
|
+
const params: string[] = [];
|
|
3448
|
+
|
|
3449
|
+
if (opts.startDate) {
|
|
3450
|
+
conditions.push("createdAt >= ?");
|
|
3451
|
+
params.push(opts.startDate);
|
|
3452
|
+
}
|
|
3453
|
+
if (opts.endDate) {
|
|
3454
|
+
conditions.push("createdAt <= ?");
|
|
3455
|
+
params.push(opts.endDate);
|
|
3456
|
+
}
|
|
3457
|
+
if (opts.agentId) {
|
|
3458
|
+
conditions.push("agentId = ?");
|
|
3459
|
+
params.push(opts.agentId);
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3463
|
+
|
|
3464
|
+
// Totals
|
|
3465
|
+
type TotalsRow = {
|
|
3466
|
+
totalCostUsd: number;
|
|
3467
|
+
totalInputTokens: number;
|
|
3468
|
+
totalOutputTokens: number;
|
|
3469
|
+
totalCacheReadTokens: number;
|
|
3470
|
+
totalCacheWriteTokens: number;
|
|
3471
|
+
totalDurationMs: number;
|
|
3472
|
+
totalSessions: number;
|
|
3473
|
+
};
|
|
3474
|
+
|
|
3475
|
+
const totalsRow = getDb()
|
|
3476
|
+
.prepare<TotalsRow, string[]>(
|
|
3477
|
+
`SELECT
|
|
3478
|
+
COALESCE(SUM(totalCostUsd), 0) as totalCostUsd,
|
|
3479
|
+
COALESCE(SUM(inputTokens), 0) as totalInputTokens,
|
|
3480
|
+
COALESCE(SUM(outputTokens), 0) as totalOutputTokens,
|
|
3481
|
+
COALESCE(SUM(cacheReadTokens), 0) as totalCacheReadTokens,
|
|
3482
|
+
COALESCE(SUM(cacheWriteTokens), 0) as totalCacheWriteTokens,
|
|
3483
|
+
COALESCE(SUM(durationMs), 0) as totalDurationMs,
|
|
3484
|
+
COUNT(*) as totalSessions
|
|
3485
|
+
FROM session_costs ${where}`,
|
|
3486
|
+
)
|
|
3487
|
+
.get(...params);
|
|
3488
|
+
|
|
3489
|
+
const totals: SessionCostSummaryTotals = totalsRow
|
|
3490
|
+
? {
|
|
3491
|
+
...totalsRow,
|
|
3492
|
+
avgCostPerSession:
|
|
3493
|
+
totalsRow.totalSessions > 0 ? totalsRow.totalCostUsd / totalsRow.totalSessions : 0,
|
|
3494
|
+
}
|
|
3495
|
+
: {
|
|
3496
|
+
totalCostUsd: 0,
|
|
3497
|
+
totalInputTokens: 0,
|
|
3498
|
+
totalOutputTokens: 0,
|
|
3499
|
+
totalCacheReadTokens: 0,
|
|
3500
|
+
totalCacheWriteTokens: 0,
|
|
3501
|
+
totalDurationMs: 0,
|
|
3502
|
+
totalSessions: 0,
|
|
3503
|
+
avgCostPerSession: 0,
|
|
3504
|
+
};
|
|
3505
|
+
|
|
3506
|
+
// Daily breakdown
|
|
3507
|
+
const groupBy = opts.groupBy ?? "both";
|
|
3508
|
+
let daily: SessionCostDailyRow[] = [];
|
|
3509
|
+
if (groupBy === "day" || groupBy === "both") {
|
|
3510
|
+
daily = getDb()
|
|
3511
|
+
.prepare<
|
|
3512
|
+
{
|
|
3513
|
+
date: string;
|
|
3514
|
+
costUsd: number;
|
|
3515
|
+
inputTokens: number;
|
|
3516
|
+
outputTokens: number;
|
|
3517
|
+
sessions: number;
|
|
3518
|
+
},
|
|
3519
|
+
string[]
|
|
3520
|
+
>(
|
|
3521
|
+
`SELECT
|
|
3522
|
+
DATE(createdAt) as date,
|
|
3523
|
+
COALESCE(SUM(totalCostUsd), 0) as costUsd,
|
|
3524
|
+
COALESCE(SUM(inputTokens), 0) as inputTokens,
|
|
3525
|
+
COALESCE(SUM(outputTokens), 0) as outputTokens,
|
|
3526
|
+
COUNT(*) as sessions
|
|
3527
|
+
FROM session_costs ${where}
|
|
3528
|
+
GROUP BY DATE(createdAt)
|
|
3529
|
+
ORDER BY date ASC`,
|
|
3530
|
+
)
|
|
3531
|
+
.all(...params);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// Per-agent breakdown
|
|
3535
|
+
let byAgent: SessionCostByAgentRow[] = [];
|
|
3536
|
+
if (groupBy === "agent" || groupBy === "both") {
|
|
3537
|
+
byAgent = getDb()
|
|
3538
|
+
.prepare<
|
|
3539
|
+
{
|
|
3540
|
+
agentId: string;
|
|
3541
|
+
costUsd: number;
|
|
3542
|
+
inputTokens: number;
|
|
3543
|
+
outputTokens: number;
|
|
3544
|
+
sessions: number;
|
|
3545
|
+
durationMs: number;
|
|
3546
|
+
},
|
|
3547
|
+
string[]
|
|
3548
|
+
>(
|
|
3549
|
+
`SELECT
|
|
3550
|
+
agentId,
|
|
3551
|
+
COALESCE(SUM(totalCostUsd), 0) as costUsd,
|
|
3552
|
+
COALESCE(SUM(inputTokens), 0) as inputTokens,
|
|
3553
|
+
COALESCE(SUM(outputTokens), 0) as outputTokens,
|
|
3554
|
+
COUNT(*) as sessions,
|
|
3555
|
+
COALESCE(SUM(durationMs), 0) as durationMs
|
|
3556
|
+
FROM session_costs ${where}
|
|
3557
|
+
GROUP BY agentId
|
|
3558
|
+
ORDER BY costUsd DESC`,
|
|
3559
|
+
)
|
|
3560
|
+
.all(...params);
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
return { totals, daily, byAgent };
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// --- Dashboard cost summary (P4) ---
|
|
3567
|
+
|
|
3568
|
+
export interface DashboardCostSummary {
|
|
3569
|
+
costToday: number;
|
|
3570
|
+
costMtd: number;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
export function getDashboardCostSummary(): DashboardCostSummary {
|
|
3574
|
+
type CostRow = { costToday: number; costMtd: number };
|
|
3575
|
+
const row = getDb()
|
|
3576
|
+
.prepare<CostRow, []>(
|
|
3577
|
+
`SELECT
|
|
3578
|
+
COALESCE(SUM(CASE WHEN createdAt >= date('now') THEN totalCostUsd ELSE 0 END), 0) as costToday,
|
|
3579
|
+
COALESCE(SUM(totalCostUsd), 0) as costMtd
|
|
3580
|
+
FROM session_costs
|
|
3581
|
+
WHERE createdAt >= date('now', 'start of month')`,
|
|
3582
|
+
)
|
|
3583
|
+
.get();
|
|
3584
|
+
|
|
3585
|
+
return row ?? { costToday: 0, costMtd: 0 };
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3266
3588
|
// ============================================================================
|
|
3267
3589
|
// Inbox Message Operations
|
|
3268
3590
|
// ============================================================================
|
|
@@ -3302,7 +3624,7 @@ function rowToInboxMessage(row: InboxMessageRow): InboxMessage {
|
|
|
3302
3624
|
}
|
|
3303
3625
|
|
|
3304
3626
|
export interface CreateInboxMessageOptions {
|
|
3305
|
-
source?: "slack";
|
|
3627
|
+
source?: "slack" | "agentmail";
|
|
3306
3628
|
slackChannelId?: string;
|
|
3307
3629
|
slackThreadTs?: string;
|
|
3308
3630
|
slackUserId?: string;
|
|
@@ -3435,6 +3757,115 @@ export function releaseStaleProcessingInbox(timeoutMinutes: number = 30): number
|
|
|
3435
3757
|
return result.changes;
|
|
3436
3758
|
}
|
|
3437
3759
|
|
|
3760
|
+
// ============================================================================
|
|
3761
|
+
// Concurrent Context (for lead session awareness)
|
|
3762
|
+
// ============================================================================
|
|
3763
|
+
|
|
3764
|
+
export interface ConcurrentContext {
|
|
3765
|
+
processingInboxMessages: Array<{
|
|
3766
|
+
id: string;
|
|
3767
|
+
content: string;
|
|
3768
|
+
source: string;
|
|
3769
|
+
slackChannelId: string | null;
|
|
3770
|
+
slackThreadTs: string | null;
|
|
3771
|
+
createdAt: string;
|
|
3772
|
+
}>;
|
|
3773
|
+
recentTaskDelegations: Array<{
|
|
3774
|
+
id: string;
|
|
3775
|
+
task: string;
|
|
3776
|
+
agentId: string | null;
|
|
3777
|
+
agentName: string | null;
|
|
3778
|
+
creatorAgentId: string | null;
|
|
3779
|
+
status: string;
|
|
3780
|
+
createdAt: string;
|
|
3781
|
+
}>;
|
|
3782
|
+
activeSwarmTasks: Array<{
|
|
3783
|
+
id: string;
|
|
3784
|
+
task: string;
|
|
3785
|
+
agentId: string | null;
|
|
3786
|
+
agentName: string | null;
|
|
3787
|
+
status: string;
|
|
3788
|
+
createdAt: string;
|
|
3789
|
+
progress: string | null;
|
|
3790
|
+
}>;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Get concurrent context for lead session awareness.
|
|
3795
|
+
* Returns processing inbox messages, recent task delegations by leads,
|
|
3796
|
+
* and currently active (in-progress) tasks across the swarm.
|
|
3797
|
+
*/
|
|
3798
|
+
export function getConcurrentContext(): ConcurrentContext {
|
|
3799
|
+
// 1. Inbox messages currently being processed (status = 'processing')
|
|
3800
|
+
const processingInboxMessages = getDb()
|
|
3801
|
+
.prepare<
|
|
3802
|
+
{
|
|
3803
|
+
id: string;
|
|
3804
|
+
content: string;
|
|
3805
|
+
source: string;
|
|
3806
|
+
slackChannelId: string | null;
|
|
3807
|
+
slackThreadTs: string | null;
|
|
3808
|
+
createdAt: string;
|
|
3809
|
+
},
|
|
3810
|
+
[]
|
|
3811
|
+
>(
|
|
3812
|
+
"SELECT id, content, source, slackChannelId, slackThreadTs, createdAt FROM inbox_messages WHERE status = 'processing' ORDER BY createdAt DESC",
|
|
3813
|
+
)
|
|
3814
|
+
.all();
|
|
3815
|
+
|
|
3816
|
+
// 2. Tasks created in the last 5 minutes by lead agents
|
|
3817
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
3818
|
+
const recentTaskDelegations = getDb()
|
|
3819
|
+
.prepare<
|
|
3820
|
+
{
|
|
3821
|
+
id: string;
|
|
3822
|
+
task: string;
|
|
3823
|
+
agentId: string | null;
|
|
3824
|
+
agentName: string | null;
|
|
3825
|
+
creatorAgentId: string | null;
|
|
3826
|
+
status: string;
|
|
3827
|
+
createdAt: string;
|
|
3828
|
+
},
|
|
3829
|
+
[string]
|
|
3830
|
+
>(
|
|
3831
|
+
`SELECT t.id, t.task, t.agentId, a.name as agentName, t.creatorAgentId, t.status, t.createdAt
|
|
3832
|
+
FROM agent_tasks t
|
|
3833
|
+
LEFT JOIN agents a ON t.agentId = a.id
|
|
3834
|
+
WHERE t.createdAt > ?
|
|
3835
|
+
AND t.creatorAgentId IN (SELECT id FROM agents WHERE isLead = 1)
|
|
3836
|
+
ORDER BY t.createdAt DESC`,
|
|
3837
|
+
)
|
|
3838
|
+
.all(fiveMinutesAgo);
|
|
3839
|
+
|
|
3840
|
+
// 3. Currently in-progress tasks across the swarm
|
|
3841
|
+
const activeSwarmTasks = getDb()
|
|
3842
|
+
.prepare<
|
|
3843
|
+
{
|
|
3844
|
+
id: string;
|
|
3845
|
+
task: string;
|
|
3846
|
+
agentId: string | null;
|
|
3847
|
+
agentName: string | null;
|
|
3848
|
+
status: string;
|
|
3849
|
+
createdAt: string;
|
|
3850
|
+
progress: string | null;
|
|
3851
|
+
},
|
|
3852
|
+
[]
|
|
3853
|
+
>(
|
|
3854
|
+
`SELECT t.id, t.task, t.agentId, a.name as agentName, t.status, t.createdAt, t.progress
|
|
3855
|
+
FROM agent_tasks t
|
|
3856
|
+
LEFT JOIN agents a ON t.agentId = a.id
|
|
3857
|
+
WHERE t.status = 'in_progress'
|
|
3858
|
+
ORDER BY t.createdAt DESC`,
|
|
3859
|
+
)
|
|
3860
|
+
.all();
|
|
3861
|
+
|
|
3862
|
+
return {
|
|
3863
|
+
processingInboxMessages,
|
|
3864
|
+
recentTaskDelegations,
|
|
3865
|
+
activeSwarmTasks,
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3438
3869
|
// ============================================================================
|
|
3439
3870
|
// Scheduled Task Queries
|
|
3440
3871
|
// ============================================================================
|
|
@@ -3455,6 +3886,11 @@ type ScheduledTaskRow = {
|
|
|
3455
3886
|
nextRunAt: string | null;
|
|
3456
3887
|
createdByAgentId: string | null;
|
|
3457
3888
|
timezone: string;
|
|
3889
|
+
consecutiveErrors: number | null;
|
|
3890
|
+
lastErrorAt: string | null;
|
|
3891
|
+
lastErrorMessage: string | null;
|
|
3892
|
+
model: string | null;
|
|
3893
|
+
scheduleType: string;
|
|
3458
3894
|
createdAt: string;
|
|
3459
3895
|
lastUpdatedAt: string;
|
|
3460
3896
|
};
|
|
@@ -3476,6 +3912,11 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
|
|
|
3476
3912
|
nextRunAt: row.nextRunAt ?? undefined,
|
|
3477
3913
|
createdByAgentId: row.createdByAgentId ?? undefined,
|
|
3478
3914
|
timezone: row.timezone,
|
|
3915
|
+
consecutiveErrors: row.consecutiveErrors ?? 0,
|
|
3916
|
+
lastErrorAt: row.lastErrorAt ?? undefined,
|
|
3917
|
+
lastErrorMessage: row.lastErrorMessage ?? undefined,
|
|
3918
|
+
model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
|
|
3919
|
+
scheduleType: row.scheduleType as "recurring" | "one_time",
|
|
3479
3920
|
createdAt: row.createdAt,
|
|
3480
3921
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
3481
3922
|
};
|
|
@@ -3484,6 +3925,8 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
|
|
|
3484
3925
|
export interface ScheduledTaskFilters {
|
|
3485
3926
|
enabled?: boolean;
|
|
3486
3927
|
name?: string;
|
|
3928
|
+
scheduleType?: "recurring" | "one_time";
|
|
3929
|
+
hideCompleted?: boolean;
|
|
3487
3930
|
}
|
|
3488
3931
|
|
|
3489
3932
|
export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask[] {
|
|
@@ -3500,6 +3943,15 @@ export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask
|
|
|
3500
3943
|
params.push(`%${filters.name}%`);
|
|
3501
3944
|
}
|
|
3502
3945
|
|
|
3946
|
+
if (filters?.scheduleType) {
|
|
3947
|
+
query += " AND scheduleType = ?";
|
|
3948
|
+
params.push(filters.scheduleType);
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
if (filters?.hideCompleted !== false) {
|
|
3952
|
+
query += " AND NOT (scheduleType = 'one_time' AND enabled = 0)";
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3503
3955
|
query += " ORDER BY name ASC";
|
|
3504
3956
|
|
|
3505
3957
|
return getDb()
|
|
@@ -3536,6 +3988,8 @@ export interface CreateScheduledTaskData {
|
|
|
3536
3988
|
nextRunAt?: string;
|
|
3537
3989
|
createdByAgentId?: string;
|
|
3538
3990
|
timezone?: string;
|
|
3991
|
+
model?: string;
|
|
3992
|
+
scheduleType?: "recurring" | "one_time";
|
|
3539
3993
|
}
|
|
3540
3994
|
|
|
3541
3995
|
export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTask {
|
|
@@ -3547,8 +4001,8 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
|
|
|
3547
4001
|
`INSERT INTO scheduled_tasks (
|
|
3548
4002
|
id, name, description, cronExpression, intervalMs, taskTemplate,
|
|
3549
4003
|
taskType, tags, priority, targetAgentId, enabled, nextRunAt,
|
|
3550
|
-
createdByAgentId, timezone, createdAt, lastUpdatedAt
|
|
3551
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
4004
|
+
createdByAgentId, timezone, model, scheduleType, createdAt, lastUpdatedAt
|
|
4005
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
3552
4006
|
)
|
|
3553
4007
|
.get(
|
|
3554
4008
|
id,
|
|
@@ -3565,6 +4019,8 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
|
|
|
3565
4019
|
data.nextRunAt ?? null,
|
|
3566
4020
|
data.createdByAgentId ?? null,
|
|
3567
4021
|
data.timezone ?? "UTC",
|
|
4022
|
+
data.model ?? null,
|
|
4023
|
+
data.scheduleType ?? "recurring",
|
|
3568
4024
|
now,
|
|
3569
4025
|
now,
|
|
3570
4026
|
);
|
|
@@ -3585,8 +4041,13 @@ export interface UpdateScheduledTaskData {
|
|
|
3585
4041
|
targetAgentId?: string | null;
|
|
3586
4042
|
enabled?: boolean;
|
|
3587
4043
|
lastRunAt?: string;
|
|
3588
|
-
nextRunAt?: string;
|
|
4044
|
+
nextRunAt?: string | null;
|
|
3589
4045
|
timezone?: string;
|
|
4046
|
+
consecutiveErrors?: number;
|
|
4047
|
+
lastErrorAt?: string | null;
|
|
4048
|
+
lastErrorMessage?: string | null;
|
|
4049
|
+
model?: string | null;
|
|
4050
|
+
scheduleType?: "recurring" | "one_time";
|
|
3590
4051
|
lastUpdatedAt?: string;
|
|
3591
4052
|
}
|
|
3592
4053
|
|
|
@@ -3649,6 +4110,26 @@ export function updateScheduledTask(
|
|
|
3649
4110
|
updates.push("timezone = ?");
|
|
3650
4111
|
params.push(data.timezone);
|
|
3651
4112
|
}
|
|
4113
|
+
if (data.consecutiveErrors !== undefined) {
|
|
4114
|
+
updates.push("consecutiveErrors = ?");
|
|
4115
|
+
params.push(data.consecutiveErrors);
|
|
4116
|
+
}
|
|
4117
|
+
if (data.lastErrorAt !== undefined) {
|
|
4118
|
+
updates.push("lastErrorAt = ?");
|
|
4119
|
+
params.push(data.lastErrorAt);
|
|
4120
|
+
}
|
|
4121
|
+
if (data.lastErrorMessage !== undefined) {
|
|
4122
|
+
updates.push("lastErrorMessage = ?");
|
|
4123
|
+
params.push(data.lastErrorMessage);
|
|
4124
|
+
}
|
|
4125
|
+
if (data.model !== undefined) {
|
|
4126
|
+
updates.push("model = ?");
|
|
4127
|
+
params.push(data.model);
|
|
4128
|
+
}
|
|
4129
|
+
if (data.scheduleType !== undefined) {
|
|
4130
|
+
updates.push("scheduleType = ?");
|
|
4131
|
+
params.push(data.scheduleType);
|
|
4132
|
+
}
|
|
3652
4133
|
|
|
3653
4134
|
if (updates.length === 0) {
|
|
3654
4135
|
return getScheduledTaskById(id);
|
|
@@ -3710,8 +4191,10 @@ type EpicRow = {
|
|
|
3710
4191
|
planDocPath: string | null;
|
|
3711
4192
|
slackChannelId: string | null;
|
|
3712
4193
|
slackThreadTs: string | null;
|
|
3713
|
-
|
|
3714
|
-
|
|
4194
|
+
vcsProvider: string | null;
|
|
4195
|
+
vcsRepo: string | null;
|
|
4196
|
+
vcsMilestone: string | null;
|
|
4197
|
+
nextSteps: string | null;
|
|
3715
4198
|
createdAt: string;
|
|
3716
4199
|
lastUpdatedAt: string;
|
|
3717
4200
|
startedAt: string | null;
|
|
@@ -3737,8 +4220,10 @@ function rowToEpic(row: EpicRow): Epic {
|
|
|
3737
4220
|
planDocPath: row.planDocPath ?? undefined,
|
|
3738
4221
|
slackChannelId: row.slackChannelId ?? undefined,
|
|
3739
4222
|
slackThreadTs: row.slackThreadTs ?? undefined,
|
|
3740
|
-
|
|
3741
|
-
|
|
4223
|
+
vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
|
|
4224
|
+
vcsRepo: row.vcsRepo ?? undefined,
|
|
4225
|
+
vcsMilestone: row.vcsMilestone ?? undefined,
|
|
4226
|
+
nextSteps: row.nextSteps ?? undefined,
|
|
3742
4227
|
createdAt: row.createdAt,
|
|
3743
4228
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
3744
4229
|
startedAt: row.startedAt ?? undefined,
|
|
@@ -3814,8 +4299,9 @@ export interface CreateEpicData {
|
|
|
3814
4299
|
planDocPath?: string;
|
|
3815
4300
|
slackChannelId?: string;
|
|
3816
4301
|
slackThreadTs?: string;
|
|
3817
|
-
|
|
3818
|
-
|
|
4302
|
+
vcsProvider?: "github" | "gitlab";
|
|
4303
|
+
vcsRepo?: string;
|
|
4304
|
+
vcsMilestone?: string;
|
|
3819
4305
|
}
|
|
3820
4306
|
|
|
3821
4307
|
export function createEpic(data: CreateEpicData): Epic {
|
|
@@ -3838,9 +4324,9 @@ export function createEpic(data: CreateEpicData): Epic {
|
|
|
3838
4324
|
`INSERT INTO epics (
|
|
3839
4325
|
id, name, description, goal, prd, plan, status, priority, tags,
|
|
3840
4326
|
createdByAgentId, leadAgentId, channelId, researchDocPath, planDocPath,
|
|
3841
|
-
slackChannelId, slackThreadTs,
|
|
4327
|
+
slackChannelId, slackThreadTs, vcsProvider, vcsRepo, vcsMilestone,
|
|
3842
4328
|
createdAt, lastUpdatedAt
|
|
3843
|
-
) VALUES (?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
4329
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
3844
4330
|
)
|
|
3845
4331
|
.get(
|
|
3846
4332
|
id,
|
|
@@ -3858,8 +4344,9 @@ export function createEpic(data: CreateEpicData): Epic {
|
|
|
3858
4344
|
data.planDocPath ?? null,
|
|
3859
4345
|
data.slackChannelId ?? null,
|
|
3860
4346
|
data.slackThreadTs ?? null,
|
|
3861
|
-
data.
|
|
3862
|
-
data.
|
|
4347
|
+
data.vcsProvider ?? null,
|
|
4348
|
+
data.vcsRepo ?? null,
|
|
4349
|
+
data.vcsMilestone ?? null,
|
|
3863
4350
|
now,
|
|
3864
4351
|
now,
|
|
3865
4352
|
);
|
|
@@ -3898,8 +4385,10 @@ export interface UpdateEpicData {
|
|
|
3898
4385
|
planDocPath?: string;
|
|
3899
4386
|
slackChannelId?: string;
|
|
3900
4387
|
slackThreadTs?: string;
|
|
3901
|
-
|
|
3902
|
-
|
|
4388
|
+
vcsProvider?: "github" | "gitlab";
|
|
4389
|
+
vcsRepo?: string;
|
|
4390
|
+
vcsMilestone?: string;
|
|
4391
|
+
nextSteps?: string;
|
|
3903
4392
|
}
|
|
3904
4393
|
|
|
3905
4394
|
export function updateEpic(id: string, data: UpdateEpicData): Epic | null {
|
|
@@ -3973,13 +4462,21 @@ export function updateEpic(id: string, data: UpdateEpicData): Epic | null {
|
|
|
3973
4462
|
updates.push("slackThreadTs = ?");
|
|
3974
4463
|
params.push(data.slackThreadTs);
|
|
3975
4464
|
}
|
|
3976
|
-
if (data.
|
|
3977
|
-
updates.push("
|
|
3978
|
-
params.push(data.
|
|
4465
|
+
if (data.vcsProvider !== undefined) {
|
|
4466
|
+
updates.push("vcsProvider = ?");
|
|
4467
|
+
params.push(data.vcsProvider);
|
|
4468
|
+
}
|
|
4469
|
+
if (data.vcsRepo !== undefined) {
|
|
4470
|
+
updates.push("vcsRepo = ?");
|
|
4471
|
+
params.push(data.vcsRepo);
|
|
4472
|
+
}
|
|
4473
|
+
if (data.vcsMilestone !== undefined) {
|
|
4474
|
+
updates.push("vcsMilestone = ?");
|
|
4475
|
+
params.push(data.vcsMilestone);
|
|
3979
4476
|
}
|
|
3980
|
-
if (data.
|
|
3981
|
-
updates.push("
|
|
3982
|
-
params.push(data.
|
|
4477
|
+
if (data.nextSteps !== undefined) {
|
|
4478
|
+
updates.push("nextSteps = ?");
|
|
4479
|
+
params.push(data.nextSteps);
|
|
3983
4480
|
}
|
|
3984
4481
|
|
|
3985
4482
|
params.push(id);
|
|
@@ -4179,3 +4676,2049 @@ export function markEpicsProgressNotified(epicIds: string[]): number {
|
|
|
4179
4676
|
|
|
4180
4677
|
return result.changes;
|
|
4181
4678
|
}
|
|
4679
|
+
|
|
4680
|
+
// ============================================================================
|
|
4681
|
+
// Swarm Config Operations (Centralized Environment/Config Management)
|
|
4682
|
+
// ============================================================================
|
|
4683
|
+
|
|
4684
|
+
type SwarmConfigRow = {
|
|
4685
|
+
id: string;
|
|
4686
|
+
scope: string;
|
|
4687
|
+
scopeId: string | null;
|
|
4688
|
+
key: string;
|
|
4689
|
+
value: string;
|
|
4690
|
+
isSecret: number; // SQLite boolean
|
|
4691
|
+
envPath: string | null;
|
|
4692
|
+
description: string | null;
|
|
4693
|
+
createdAt: string;
|
|
4694
|
+
lastUpdatedAt: string;
|
|
4695
|
+
};
|
|
4696
|
+
|
|
4697
|
+
function rowToSwarmConfig(row: SwarmConfigRow): SwarmConfig {
|
|
4698
|
+
return {
|
|
4699
|
+
id: row.id,
|
|
4700
|
+
scope: row.scope as "global" | "agent" | "repo",
|
|
4701
|
+
scopeId: row.scopeId ?? null,
|
|
4702
|
+
key: row.key,
|
|
4703
|
+
value: row.value,
|
|
4704
|
+
isSecret: row.isSecret === 1,
|
|
4705
|
+
envPath: row.envPath ?? null,
|
|
4706
|
+
description: row.description ?? null,
|
|
4707
|
+
createdAt: row.createdAt,
|
|
4708
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
/**
|
|
4713
|
+
* Mask secret values in config entries for API responses.
|
|
4714
|
+
*/
|
|
4715
|
+
export function maskSecrets(configs: SwarmConfig[]): SwarmConfig[] {
|
|
4716
|
+
return configs.map((c) => (c.isSecret ? { ...c, value: "********" } : c));
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
/**
|
|
4720
|
+
* Write config values to .env files on disk when `envPath` is set.
|
|
4721
|
+
* Groups configs by envPath, reads existing file, updates/adds matching keys, writes back.
|
|
4722
|
+
*/
|
|
4723
|
+
function writeEnvFile(configs: SwarmConfig[]): void {
|
|
4724
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
4725
|
+
|
|
4726
|
+
const byPath = new Map<string, SwarmConfig[]>();
|
|
4727
|
+
for (const config of configs) {
|
|
4728
|
+
if (!config.envPath) continue;
|
|
4729
|
+
const existing = byPath.get(config.envPath) ?? [];
|
|
4730
|
+
existing.push(config);
|
|
4731
|
+
byPath.set(config.envPath, existing);
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
for (const [envPath, entries] of byPath) {
|
|
4735
|
+
let lines: string[] = [];
|
|
4736
|
+
try {
|
|
4737
|
+
const content = readFileSync(envPath, "utf-8") as string;
|
|
4738
|
+
lines = content.split("\n");
|
|
4739
|
+
} catch {
|
|
4740
|
+
// File doesn't exist yet, start empty
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
for (const entry of entries) {
|
|
4744
|
+
const prefix = `${entry.key}=`;
|
|
4745
|
+
const lineIndex = lines.findIndex((l) => l.startsWith(prefix));
|
|
4746
|
+
const newLine = `${entry.key}=${entry.value}`;
|
|
4747
|
+
if (lineIndex >= 0) {
|
|
4748
|
+
lines[lineIndex] = newLine;
|
|
4749
|
+
} else {
|
|
4750
|
+
lines.push(newLine);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
const output = `${lines.filter((l) => l !== "").join("\n")}\n`;
|
|
4755
|
+
writeFileSync(envPath, output, "utf-8");
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
/**
|
|
4760
|
+
* List config entries with optional filters.
|
|
4761
|
+
*/
|
|
4762
|
+
export function getSwarmConfigs(filters?: {
|
|
4763
|
+
scope?: string;
|
|
4764
|
+
scopeId?: string;
|
|
4765
|
+
key?: string;
|
|
4766
|
+
}): SwarmConfig[] {
|
|
4767
|
+
const conditions: string[] = [];
|
|
4768
|
+
const params: string[] = [];
|
|
4769
|
+
|
|
4770
|
+
if (filters?.scope) {
|
|
4771
|
+
conditions.push("scope = ?");
|
|
4772
|
+
params.push(filters.scope);
|
|
4773
|
+
}
|
|
4774
|
+
if (filters?.scopeId) {
|
|
4775
|
+
conditions.push("scopeId = ?");
|
|
4776
|
+
params.push(filters.scopeId);
|
|
4777
|
+
}
|
|
4778
|
+
if (filters?.key) {
|
|
4779
|
+
conditions.push("key = ?");
|
|
4780
|
+
params.push(filters.key);
|
|
4781
|
+
}
|
|
4782
|
+
|
|
4783
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4784
|
+
const query = `SELECT * FROM swarm_config ${whereClause} ORDER BY key ASC`;
|
|
4785
|
+
|
|
4786
|
+
return getDb()
|
|
4787
|
+
.prepare<SwarmConfigRow, string[]>(query)
|
|
4788
|
+
.all(...params)
|
|
4789
|
+
.map(rowToSwarmConfig);
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4792
|
+
/**
|
|
4793
|
+
* Get a single config entry by ID.
|
|
4794
|
+
*/
|
|
4795
|
+
export function getSwarmConfigById(id: string): SwarmConfig | null {
|
|
4796
|
+
const row = getDb()
|
|
4797
|
+
.prepare<SwarmConfigRow, [string]>("SELECT * FROM swarm_config WHERE id = ?")
|
|
4798
|
+
.get(id);
|
|
4799
|
+
return row ? rowToSwarmConfig(row) : null;
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
/**
|
|
4803
|
+
* Upsert a config entry. Inserts or updates by (scope, scopeId, key) unique constraint.
|
|
4804
|
+
*/
|
|
4805
|
+
export function upsertSwarmConfig(data: {
|
|
4806
|
+
scope: "global" | "agent" | "repo";
|
|
4807
|
+
scopeId?: string | null;
|
|
4808
|
+
key: string;
|
|
4809
|
+
value: string;
|
|
4810
|
+
isSecret?: boolean;
|
|
4811
|
+
envPath?: string | null;
|
|
4812
|
+
description?: string | null;
|
|
4813
|
+
}): SwarmConfig {
|
|
4814
|
+
const now = new Date().toISOString();
|
|
4815
|
+
const scopeId = data.scope === "global" ? null : (data.scopeId ?? null);
|
|
4816
|
+
const isSecret = data.isSecret ? 1 : 0;
|
|
4817
|
+
const envPath = data.envPath ?? null;
|
|
4818
|
+
const description = data.description ?? null;
|
|
4819
|
+
|
|
4820
|
+
// Manual check for existing entry because SQLite's UNIQUE constraint
|
|
4821
|
+
// treats NULL != NULL, so ON CONFLICT never fires when scopeId is NULL (global scope).
|
|
4822
|
+
const existing =
|
|
4823
|
+
scopeId === null
|
|
4824
|
+
? getDb()
|
|
4825
|
+
.prepare<{ id: string }, [string, string]>(
|
|
4826
|
+
"SELECT id FROM swarm_config WHERE scope = ? AND scopeId IS NULL AND key = ?",
|
|
4827
|
+
)
|
|
4828
|
+
.get(data.scope, data.key)
|
|
4829
|
+
: getDb()
|
|
4830
|
+
.prepare<{ id: string }, [string, string, string]>(
|
|
4831
|
+
"SELECT id FROM swarm_config WHERE scope = ? AND scopeId = ? AND key = ?",
|
|
4832
|
+
)
|
|
4833
|
+
.get(data.scope, scopeId, data.key);
|
|
4834
|
+
|
|
4835
|
+
let row: SwarmConfigRow | null;
|
|
4836
|
+
|
|
4837
|
+
if (existing) {
|
|
4838
|
+
row = getDb()
|
|
4839
|
+
.prepare<SwarmConfigRow, [string, number, string | null, string | null, string, string]>(
|
|
4840
|
+
`UPDATE swarm_config SET value = ?, isSecret = ?, envPath = ?, description = ?, lastUpdatedAt = ?
|
|
4841
|
+
WHERE id = ? RETURNING *`,
|
|
4842
|
+
)
|
|
4843
|
+
.get(data.value, isSecret, envPath, description, now, existing.id);
|
|
4844
|
+
} else {
|
|
4845
|
+
const id = crypto.randomUUID();
|
|
4846
|
+
row = getDb()
|
|
4847
|
+
.prepare<
|
|
4848
|
+
SwarmConfigRow,
|
|
4849
|
+
[
|
|
4850
|
+
string,
|
|
4851
|
+
string,
|
|
4852
|
+
string | null,
|
|
4853
|
+
string,
|
|
4854
|
+
string,
|
|
4855
|
+
number,
|
|
4856
|
+
string | null,
|
|
4857
|
+
string | null,
|
|
4858
|
+
string,
|
|
4859
|
+
string,
|
|
4860
|
+
]
|
|
4861
|
+
>(
|
|
4862
|
+
`INSERT INTO swarm_config (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt)
|
|
4863
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
4864
|
+
)
|
|
4865
|
+
.get(id, data.scope, scopeId, data.key, data.value, isSecret, envPath, description, now, now);
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
if (!row) throw new Error("Failed to upsert swarm config");
|
|
4869
|
+
|
|
4870
|
+
const config = rowToSwarmConfig(row);
|
|
4871
|
+
|
|
4872
|
+
// Write to envPath if set
|
|
4873
|
+
if (config.envPath) {
|
|
4874
|
+
try {
|
|
4875
|
+
writeEnvFile([config]);
|
|
4876
|
+
} catch (e) {
|
|
4877
|
+
console.error(`Failed to write env file ${config.envPath}:`, e);
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
return config;
|
|
4882
|
+
}
|
|
4883
|
+
|
|
4884
|
+
/**
|
|
4885
|
+
* Delete a config entry by ID.
|
|
4886
|
+
*/
|
|
4887
|
+
export function deleteSwarmConfig(id: string): boolean {
|
|
4888
|
+
const result = getDb().run("DELETE FROM swarm_config WHERE id = ?", [id]);
|
|
4889
|
+
return result.changes > 0;
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
/**
|
|
4893
|
+
* Get resolved (merged) config for a given agent and/or repo.
|
|
4894
|
+
* Scope resolution: repo > agent > global (most-specific wins).
|
|
4895
|
+
* Returns one entry per unique key with the most-specific scope winning.
|
|
4896
|
+
*/
|
|
4897
|
+
export function getResolvedConfig(agentId?: string, repoId?: string): SwarmConfig[] {
|
|
4898
|
+
// Start with global configs
|
|
4899
|
+
const configMap = new Map<string, SwarmConfig>();
|
|
4900
|
+
|
|
4901
|
+
const globalConfigs = getSwarmConfigs({ scope: "global" });
|
|
4902
|
+
for (const config of globalConfigs) {
|
|
4903
|
+
configMap.set(config.key, config);
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
// Overlay agent configs (agent wins over global)
|
|
4907
|
+
if (agentId) {
|
|
4908
|
+
const agentConfigs = getSwarmConfigs({ scope: "agent", scopeId: agentId });
|
|
4909
|
+
for (const config of agentConfigs) {
|
|
4910
|
+
configMap.set(config.key, config);
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
// Overlay repo configs (repo wins over agent and global)
|
|
4915
|
+
if (repoId) {
|
|
4916
|
+
const repoConfigs = getSwarmConfigs({ scope: "repo", scopeId: repoId });
|
|
4917
|
+
for (const config of repoConfigs) {
|
|
4918
|
+
configMap.set(config.key, config);
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
return Array.from(configMap.values()).sort((a, b) => a.key.localeCompare(b.key));
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
// ============================================================================
|
|
4926
|
+
// Swarm Repos Functions (Centralized Repository Management)
|
|
4927
|
+
// ============================================================================
|
|
4928
|
+
|
|
4929
|
+
type SwarmRepoRow = {
|
|
4930
|
+
id: string;
|
|
4931
|
+
url: string;
|
|
4932
|
+
name: string;
|
|
4933
|
+
clonePath: string;
|
|
4934
|
+
defaultBranch: string;
|
|
4935
|
+
autoClone: number; // SQLite boolean
|
|
4936
|
+
createdAt: string;
|
|
4937
|
+
lastUpdatedAt: string;
|
|
4938
|
+
};
|
|
4939
|
+
|
|
4940
|
+
function rowToSwarmRepo(row: SwarmRepoRow): SwarmRepo {
|
|
4941
|
+
return {
|
|
4942
|
+
id: row.id,
|
|
4943
|
+
url: row.url,
|
|
4944
|
+
name: row.name,
|
|
4945
|
+
clonePath: row.clonePath,
|
|
4946
|
+
defaultBranch: row.defaultBranch,
|
|
4947
|
+
autoClone: row.autoClone === 1,
|
|
4948
|
+
createdAt: row.createdAt,
|
|
4949
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
4950
|
+
};
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
export function getSwarmRepos(filters?: { autoClone?: boolean; name?: string }): SwarmRepo[] {
|
|
4954
|
+
const conditions: string[] = [];
|
|
4955
|
+
const params: (string | number)[] = [];
|
|
4956
|
+
|
|
4957
|
+
if (filters?.autoClone !== undefined) {
|
|
4958
|
+
conditions.push("autoClone = ?");
|
|
4959
|
+
params.push(filters.autoClone ? 1 : 0);
|
|
4960
|
+
}
|
|
4961
|
+
if (filters?.name) {
|
|
4962
|
+
conditions.push("name = ?");
|
|
4963
|
+
params.push(filters.name);
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4966
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4967
|
+
const query = `SELECT * FROM swarm_repos ${whereClause} ORDER BY name ASC`;
|
|
4968
|
+
|
|
4969
|
+
return getDb()
|
|
4970
|
+
.prepare<SwarmRepoRow, (string | number)[]>(query)
|
|
4971
|
+
.all(...params)
|
|
4972
|
+
.map(rowToSwarmRepo);
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
export function getSwarmRepoById(id: string): SwarmRepo | null {
|
|
4976
|
+
const row = getDb()
|
|
4977
|
+
.prepare<SwarmRepoRow, [string]>("SELECT * FROM swarm_repos WHERE id = ?")
|
|
4978
|
+
.get(id);
|
|
4979
|
+
return row ? rowToSwarmRepo(row) : null;
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
export function getSwarmRepoByName(name: string): SwarmRepo | null {
|
|
4983
|
+
const row = getDb()
|
|
4984
|
+
.prepare<SwarmRepoRow, [string]>("SELECT * FROM swarm_repos WHERE name = ?")
|
|
4985
|
+
.get(name);
|
|
4986
|
+
return row ? rowToSwarmRepo(row) : null;
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
export function getSwarmRepoByUrl(url: string): SwarmRepo | null {
|
|
4990
|
+
const row = getDb()
|
|
4991
|
+
.prepare<SwarmRepoRow, [string]>("SELECT * FROM swarm_repos WHERE url = ?")
|
|
4992
|
+
.get(url);
|
|
4993
|
+
return row ? rowToSwarmRepo(row) : null;
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
export function createSwarmRepo(data: {
|
|
4997
|
+
url: string;
|
|
4998
|
+
name: string;
|
|
4999
|
+
clonePath?: string;
|
|
5000
|
+
defaultBranch?: string;
|
|
5001
|
+
autoClone?: boolean;
|
|
5002
|
+
}): SwarmRepo {
|
|
5003
|
+
const id = crypto.randomUUID();
|
|
5004
|
+
const now = new Date().toISOString();
|
|
5005
|
+
const clonePath = data.clonePath || `/workspace/repos/${data.name}`;
|
|
5006
|
+
|
|
5007
|
+
const row = getDb()
|
|
5008
|
+
.prepare<SwarmRepoRow, [string, string, string, string, string, number, string, string]>(
|
|
5009
|
+
`INSERT INTO swarm_repos (id, url, name, clonePath, defaultBranch, autoClone, createdAt, lastUpdatedAt)
|
|
5010
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5011
|
+
)
|
|
5012
|
+
.get(
|
|
5013
|
+
id,
|
|
5014
|
+
data.url,
|
|
5015
|
+
data.name,
|
|
5016
|
+
clonePath,
|
|
5017
|
+
data.defaultBranch ?? "main",
|
|
5018
|
+
data.autoClone !== false ? 1 : 0,
|
|
5019
|
+
now,
|
|
5020
|
+
now,
|
|
5021
|
+
);
|
|
5022
|
+
|
|
5023
|
+
if (!row) throw new Error("Failed to create repo");
|
|
5024
|
+
return rowToSwarmRepo(row);
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
export function updateSwarmRepo(
|
|
5028
|
+
id: string,
|
|
5029
|
+
updates: Partial<{
|
|
5030
|
+
url: string;
|
|
5031
|
+
name: string;
|
|
5032
|
+
clonePath: string;
|
|
5033
|
+
defaultBranch: string;
|
|
5034
|
+
autoClone: boolean;
|
|
5035
|
+
}>,
|
|
5036
|
+
): SwarmRepo | null {
|
|
5037
|
+
const setClauses: string[] = [];
|
|
5038
|
+
const params: (string | number)[] = [];
|
|
5039
|
+
|
|
5040
|
+
const stringFields = ["url", "name", "clonePath", "defaultBranch"] as const;
|
|
5041
|
+
for (const field of stringFields) {
|
|
5042
|
+
if (updates[field] !== undefined) {
|
|
5043
|
+
setClauses.push(`${field} = ?`);
|
|
5044
|
+
params.push(updates[field]);
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
if (updates.autoClone !== undefined) {
|
|
5048
|
+
setClauses.push("autoClone = ?");
|
|
5049
|
+
params.push(updates.autoClone ? 1 : 0);
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
if (setClauses.length === 0) return getSwarmRepoById(id);
|
|
5053
|
+
|
|
5054
|
+
setClauses.push("lastUpdatedAt = ?");
|
|
5055
|
+
params.push(new Date().toISOString());
|
|
5056
|
+
params.push(id);
|
|
5057
|
+
|
|
5058
|
+
const row = getDb()
|
|
5059
|
+
.prepare<SwarmRepoRow, (string | number)[]>(
|
|
5060
|
+
`UPDATE swarm_repos SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`,
|
|
5061
|
+
)
|
|
5062
|
+
.get(...params);
|
|
5063
|
+
|
|
5064
|
+
return row ? rowToSwarmRepo(row) : null;
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
export function deleteSwarmRepo(id: string): boolean {
|
|
5068
|
+
const result = getDb().run("DELETE FROM swarm_repos WHERE id = ?", [id]);
|
|
5069
|
+
return result.changes > 0;
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
// ============================================================================
|
|
5073
|
+
// Agent Memory Functions
|
|
5074
|
+
// ============================================================================
|
|
5075
|
+
|
|
5076
|
+
type AgentMemoryRow = {
|
|
5077
|
+
id: string;
|
|
5078
|
+
agentId: string | null;
|
|
5079
|
+
scope: string;
|
|
5080
|
+
name: string;
|
|
5081
|
+
content: string;
|
|
5082
|
+
summary: string | null;
|
|
5083
|
+
embedding: Buffer | null;
|
|
5084
|
+
source: string;
|
|
5085
|
+
sourceTaskId: string | null;
|
|
5086
|
+
sourcePath: string | null;
|
|
5087
|
+
chunkIndex: number;
|
|
5088
|
+
totalChunks: number;
|
|
5089
|
+
tags: string;
|
|
5090
|
+
createdAt: string;
|
|
5091
|
+
accessedAt: string;
|
|
5092
|
+
};
|
|
5093
|
+
|
|
5094
|
+
function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
|
|
5095
|
+
return {
|
|
5096
|
+
id: row.id,
|
|
5097
|
+
agentId: row.agentId,
|
|
5098
|
+
scope: row.scope as AgentMemoryScope,
|
|
5099
|
+
name: row.name,
|
|
5100
|
+
content: row.content,
|
|
5101
|
+
summary: row.summary,
|
|
5102
|
+
source: row.source as AgentMemorySource,
|
|
5103
|
+
sourceTaskId: row.sourceTaskId,
|
|
5104
|
+
sourcePath: row.sourcePath,
|
|
5105
|
+
chunkIndex: row.chunkIndex,
|
|
5106
|
+
totalChunks: row.totalChunks,
|
|
5107
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
5108
|
+
createdAt: row.createdAt,
|
|
5109
|
+
accessedAt: row.accessedAt,
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
export interface CreateMemoryOptions {
|
|
5114
|
+
agentId?: string | null;
|
|
5115
|
+
scope: AgentMemoryScope;
|
|
5116
|
+
name: string;
|
|
5117
|
+
content: string;
|
|
5118
|
+
summary?: string | null;
|
|
5119
|
+
embedding?: Buffer | null;
|
|
5120
|
+
source: AgentMemorySource;
|
|
5121
|
+
sourceTaskId?: string | null;
|
|
5122
|
+
sourcePath?: string | null;
|
|
5123
|
+
chunkIndex?: number;
|
|
5124
|
+
totalChunks?: number;
|
|
5125
|
+
tags?: string[];
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
export function createMemory(data: CreateMemoryOptions): AgentMemory {
|
|
5129
|
+
const id = crypto.randomUUID();
|
|
5130
|
+
const now = new Date().toISOString();
|
|
5131
|
+
const row = getDb()
|
|
5132
|
+
.prepare<
|
|
5133
|
+
AgentMemoryRow,
|
|
5134
|
+
[
|
|
5135
|
+
string,
|
|
5136
|
+
string | null,
|
|
5137
|
+
string,
|
|
5138
|
+
string,
|
|
5139
|
+
string,
|
|
5140
|
+
string | null,
|
|
5141
|
+
Buffer | null,
|
|
5142
|
+
string,
|
|
5143
|
+
string | null,
|
|
5144
|
+
string | null,
|
|
5145
|
+
number,
|
|
5146
|
+
number,
|
|
5147
|
+
string,
|
|
5148
|
+
string,
|
|
5149
|
+
string,
|
|
5150
|
+
]
|
|
5151
|
+
>(
|
|
5152
|
+
`INSERT INTO agent_memory (id, agentId, scope, name, content, summary, embedding, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt)
|
|
5153
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5154
|
+
)
|
|
5155
|
+
.get(
|
|
5156
|
+
id,
|
|
5157
|
+
data.agentId ?? null,
|
|
5158
|
+
data.scope,
|
|
5159
|
+
data.name,
|
|
5160
|
+
data.content,
|
|
5161
|
+
data.summary ?? null,
|
|
5162
|
+
data.embedding ?? null,
|
|
5163
|
+
data.source,
|
|
5164
|
+
data.sourceTaskId ?? null,
|
|
5165
|
+
data.sourcePath ?? null,
|
|
5166
|
+
data.chunkIndex ?? 0,
|
|
5167
|
+
data.totalChunks ?? 1,
|
|
5168
|
+
JSON.stringify(data.tags ?? []),
|
|
5169
|
+
now,
|
|
5170
|
+
now,
|
|
5171
|
+
);
|
|
5172
|
+
|
|
5173
|
+
if (!row) throw new Error("Failed to create memory");
|
|
5174
|
+
return rowToAgentMemory(row);
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
export function getMemoryById(id: string): AgentMemory | null {
|
|
5178
|
+
const row = getDb()
|
|
5179
|
+
.prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
|
|
5180
|
+
.get(id);
|
|
5181
|
+
if (!row) return null;
|
|
5182
|
+
|
|
5183
|
+
// Update accessedAt
|
|
5184
|
+
getDb()
|
|
5185
|
+
.prepare("UPDATE agent_memory SET accessedAt = ? WHERE id = ?")
|
|
5186
|
+
.run(new Date().toISOString(), id);
|
|
5187
|
+
|
|
5188
|
+
return rowToAgentMemory(row);
|
|
5189
|
+
}
|
|
5190
|
+
|
|
5191
|
+
export function updateMemoryEmbedding(id: string, embedding: Buffer): void {
|
|
5192
|
+
getDb().prepare("UPDATE agent_memory SET embedding = ? WHERE id = ?").run(embedding, id);
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
export interface SearchMemoriesOptions {
|
|
5196
|
+
scope?: "agent" | "swarm" | "all";
|
|
5197
|
+
limit?: number;
|
|
5198
|
+
source?: AgentMemorySource;
|
|
5199
|
+
isLead?: boolean;
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
export function searchMemoriesByVector(
|
|
5203
|
+
queryEmbedding: Float32Array,
|
|
5204
|
+
agentId: string,
|
|
5205
|
+
options: SearchMemoriesOptions = {},
|
|
5206
|
+
): (AgentMemory & { similarity: number })[] {
|
|
5207
|
+
const { scope = "all", limit = 10, source, isLead = false } = options;
|
|
5208
|
+
|
|
5209
|
+
// Build WHERE clause
|
|
5210
|
+
const conditions: string[] = ["embedding IS NOT NULL"];
|
|
5211
|
+
const params: (string | null)[] = [];
|
|
5212
|
+
|
|
5213
|
+
if (!isLead) {
|
|
5214
|
+
// Workers see their own agent-scoped + all swarm-scoped
|
|
5215
|
+
if (scope === "agent") {
|
|
5216
|
+
conditions.push("agentId = ? AND scope = 'agent'");
|
|
5217
|
+
params.push(agentId);
|
|
5218
|
+
} else if (scope === "swarm") {
|
|
5219
|
+
conditions.push("scope = 'swarm'");
|
|
5220
|
+
} else {
|
|
5221
|
+
// "all" - own agent + swarm
|
|
5222
|
+
conditions.push("(agentId = ? OR scope = 'swarm')");
|
|
5223
|
+
params.push(agentId);
|
|
5224
|
+
}
|
|
5225
|
+
} else {
|
|
5226
|
+
// Leads see everything
|
|
5227
|
+
if (scope === "agent") {
|
|
5228
|
+
conditions.push("scope = 'agent'");
|
|
5229
|
+
} else if (scope === "swarm") {
|
|
5230
|
+
conditions.push("scope = 'swarm'");
|
|
5231
|
+
}
|
|
5232
|
+
// "all" for lead = no scope filter needed
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
if (source) {
|
|
5236
|
+
conditions.push("source = ?");
|
|
5237
|
+
params.push(source);
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5241
|
+
|
|
5242
|
+
const rows = getDb()
|
|
5243
|
+
.prepare<AgentMemoryRow, (string | null)[]>(`SELECT * FROM agent_memory ${whereClause}`)
|
|
5244
|
+
.all(...params);
|
|
5245
|
+
|
|
5246
|
+
// Import cosine similarity inline to avoid circular deps
|
|
5247
|
+
const { cosineSimilarity, deserializeEmbedding } = require("./embedding");
|
|
5248
|
+
|
|
5249
|
+
// Compute similarities and sort
|
|
5250
|
+
const results: (AgentMemory & { similarity: number })[] = [];
|
|
5251
|
+
for (const row of rows) {
|
|
5252
|
+
if (!row.embedding) continue;
|
|
5253
|
+
const embedding = deserializeEmbedding(row.embedding);
|
|
5254
|
+
// Skip embeddings with mismatched dimensions (can happen if embedding model changes)
|
|
5255
|
+
if (embedding.length !== queryEmbedding.length) continue;
|
|
5256
|
+
const similarity = cosineSimilarity(queryEmbedding, embedding) as number;
|
|
5257
|
+
results.push({ ...rowToAgentMemory(row), similarity });
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
5261
|
+
return results.slice(0, limit);
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
export interface ListMemoriesOptions {
|
|
5265
|
+
scope?: "agent" | "swarm" | "all";
|
|
5266
|
+
limit?: number;
|
|
5267
|
+
offset?: number;
|
|
5268
|
+
isLead?: boolean;
|
|
5269
|
+
}
|
|
5270
|
+
|
|
5271
|
+
export function listMemoriesByAgent(
|
|
5272
|
+
agentId: string,
|
|
5273
|
+
options: ListMemoriesOptions = {},
|
|
5274
|
+
): AgentMemory[] {
|
|
5275
|
+
const { scope = "all", limit = 20, offset = 0, isLead = false } = options;
|
|
5276
|
+
|
|
5277
|
+
const conditions: string[] = [];
|
|
5278
|
+
const params: (string | number)[] = [];
|
|
5279
|
+
|
|
5280
|
+
if (!isLead) {
|
|
5281
|
+
if (scope === "agent") {
|
|
5282
|
+
conditions.push("agentId = ? AND scope = 'agent'");
|
|
5283
|
+
params.push(agentId);
|
|
5284
|
+
} else if (scope === "swarm") {
|
|
5285
|
+
conditions.push("scope = 'swarm'");
|
|
5286
|
+
} else {
|
|
5287
|
+
conditions.push("(agentId = ? OR scope = 'swarm')");
|
|
5288
|
+
params.push(agentId);
|
|
5289
|
+
}
|
|
5290
|
+
} else {
|
|
5291
|
+
if (scope === "agent") {
|
|
5292
|
+
conditions.push("scope = 'agent'");
|
|
5293
|
+
} else if (scope === "swarm") {
|
|
5294
|
+
conditions.push("scope = 'swarm'");
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
5299
|
+
|
|
5300
|
+
params.push(limit, offset);
|
|
5301
|
+
|
|
5302
|
+
const rows = getDb()
|
|
5303
|
+
.prepare<AgentMemoryRow, (string | number)[]>(
|
|
5304
|
+
`SELECT * FROM agent_memory ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
|
|
5305
|
+
)
|
|
5306
|
+
.all(...params);
|
|
5307
|
+
|
|
5308
|
+
return rows.map(rowToAgentMemory);
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
export function deleteMemoriesBySourcePath(sourcePath: string, agentId: string): number {
|
|
5312
|
+
const result = getDb()
|
|
5313
|
+
.prepare("DELETE FROM agent_memory WHERE sourcePath = ? AND agentId = ?")
|
|
5314
|
+
.run(sourcePath, agentId);
|
|
5315
|
+
return result.changes;
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
export function deleteMemory(id: string): boolean {
|
|
5319
|
+
const result = getDb().prepare("DELETE FROM agent_memory WHERE id = ?").run(id);
|
|
5320
|
+
return result.changes > 0;
|
|
5321
|
+
}
|
|
5322
|
+
|
|
5323
|
+
export function getMemoryStats(agentId: string): {
|
|
5324
|
+
total: number;
|
|
5325
|
+
bySource: Record<string, number>;
|
|
5326
|
+
byScope: Record<string, number>;
|
|
5327
|
+
} {
|
|
5328
|
+
const total = getDb()
|
|
5329
|
+
.prepare<{ count: number }, [string]>(
|
|
5330
|
+
"SELECT COUNT(*) as count FROM agent_memory WHERE agentId = ?",
|
|
5331
|
+
)
|
|
5332
|
+
.get(agentId);
|
|
5333
|
+
|
|
5334
|
+
const bySourceRows = getDb()
|
|
5335
|
+
.prepare<{ source: string; count: number }, [string]>(
|
|
5336
|
+
"SELECT source, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY source",
|
|
5337
|
+
)
|
|
5338
|
+
.all(agentId);
|
|
5339
|
+
|
|
5340
|
+
const byScopeRows = getDb()
|
|
5341
|
+
.prepare<{ scope: string; count: number }, [string]>(
|
|
5342
|
+
"SELECT scope, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY scope",
|
|
5343
|
+
)
|
|
5344
|
+
.all(agentId);
|
|
5345
|
+
|
|
5346
|
+
const bySource: Record<string, number> = {};
|
|
5347
|
+
for (const row of bySourceRows) {
|
|
5348
|
+
bySource[row.source] = row.count;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
const byScope: Record<string, number> = {};
|
|
5352
|
+
for (const row of byScopeRows) {
|
|
5353
|
+
byScope[row.scope] = row.count;
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
return { total: total?.count ?? 0, bySource, byScope };
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
// ============================================================================
|
|
5360
|
+
// AgentMail Inbox Mapping Queries
|
|
5361
|
+
// ============================================================================
|
|
5362
|
+
|
|
5363
|
+
export interface AgentMailInboxMapping {
|
|
5364
|
+
id: string;
|
|
5365
|
+
inboxId: string;
|
|
5366
|
+
agentId: string;
|
|
5367
|
+
inboxEmail: string | null;
|
|
5368
|
+
createdAt: string;
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
export function getAgentMailInboxMapping(inboxId: string): AgentMailInboxMapping | null {
|
|
5372
|
+
return (
|
|
5373
|
+
getDb()
|
|
5374
|
+
.prepare<AgentMailInboxMapping, [string]>(
|
|
5375
|
+
"SELECT * FROM agentmail_inbox_mappings WHERE inboxId = ?",
|
|
5376
|
+
)
|
|
5377
|
+
.get(inboxId) ?? null
|
|
5378
|
+
);
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
export function getAgentMailInboxMappingsByAgent(agentId: string): AgentMailInboxMapping[] {
|
|
5382
|
+
return getDb()
|
|
5383
|
+
.prepare<AgentMailInboxMapping, [string]>(
|
|
5384
|
+
"SELECT * FROM agentmail_inbox_mappings WHERE agentId = ? ORDER BY createdAt DESC",
|
|
5385
|
+
)
|
|
5386
|
+
.all(agentId);
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
export function getAllAgentMailInboxMappings(): AgentMailInboxMapping[] {
|
|
5390
|
+
return getDb()
|
|
5391
|
+
.prepare<AgentMailInboxMapping, []>(
|
|
5392
|
+
"SELECT * FROM agentmail_inbox_mappings ORDER BY createdAt DESC",
|
|
5393
|
+
)
|
|
5394
|
+
.all();
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
export function createAgentMailInboxMapping(
|
|
5398
|
+
inboxId: string,
|
|
5399
|
+
agentId: string,
|
|
5400
|
+
inboxEmail?: string,
|
|
5401
|
+
): AgentMailInboxMapping {
|
|
5402
|
+
const id = crypto.randomUUID();
|
|
5403
|
+
const now = new Date().toISOString();
|
|
5404
|
+
|
|
5405
|
+
const row = getDb()
|
|
5406
|
+
.prepare<AgentMailInboxMapping, [string, string, string, string | null, string]>(
|
|
5407
|
+
`INSERT INTO agentmail_inbox_mappings (id, inboxId, agentId, inboxEmail, createdAt)
|
|
5408
|
+
VALUES (?, ?, ?, ?, ?)
|
|
5409
|
+
ON CONFLICT(inboxId) DO UPDATE SET agentId = excluded.agentId, inboxEmail = excluded.inboxEmail
|
|
5410
|
+
RETURNING *`,
|
|
5411
|
+
)
|
|
5412
|
+
.get(id, inboxId, agentId, inboxEmail ?? null, now);
|
|
5413
|
+
|
|
5414
|
+
if (!row) throw new Error("Failed to create AgentMail inbox mapping");
|
|
5415
|
+
return row;
|
|
5416
|
+
}
|
|
5417
|
+
|
|
5418
|
+
export function deleteAgentMailInboxMapping(inboxId: string): boolean {
|
|
5419
|
+
const result = getDb()
|
|
5420
|
+
.prepare("DELETE FROM agentmail_inbox_mappings WHERE inboxId = ?")
|
|
5421
|
+
.run(inboxId);
|
|
5422
|
+
return result.changes > 0;
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
/**
|
|
5426
|
+
* Find the most recent task by AgentMail thread ID
|
|
5427
|
+
* Includes completed/failed tasks to maintain thread continuity via parentTaskId
|
|
5428
|
+
*/
|
|
5429
|
+
export function findTaskByAgentMailThread(agentmailThreadId: string): AgentTask | null {
|
|
5430
|
+
const row = getDb()
|
|
5431
|
+
.prepare<AgentTaskRow, [string]>(
|
|
5432
|
+
`SELECT * FROM agent_tasks
|
|
5433
|
+
WHERE agentmailThreadId = ?
|
|
5434
|
+
ORDER BY createdAt DESC
|
|
5435
|
+
LIMIT 1`,
|
|
5436
|
+
)
|
|
5437
|
+
.get(agentmailThreadId);
|
|
5438
|
+
return row ? rowToAgentTask(row) : null;
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
// ============================================================================
|
|
5442
|
+
// Active Sessions (runner session tracking for concurrency awareness)
|
|
5443
|
+
// ============================================================================
|
|
5444
|
+
|
|
5445
|
+
export function insertActiveSession(session: {
|
|
5446
|
+
agentId: string;
|
|
5447
|
+
taskId?: string;
|
|
5448
|
+
triggerType: string;
|
|
5449
|
+
inboxMessageId?: string;
|
|
5450
|
+
taskDescription?: string;
|
|
5451
|
+
runnerSessionId?: string;
|
|
5452
|
+
}): ActiveSession {
|
|
5453
|
+
const id = crypto.randomUUID();
|
|
5454
|
+
const now = new Date().toISOString();
|
|
5455
|
+
|
|
5456
|
+
const row = getDb()
|
|
5457
|
+
.prepare<
|
|
5458
|
+
ActiveSession,
|
|
5459
|
+
[
|
|
5460
|
+
string,
|
|
5461
|
+
string,
|
|
5462
|
+
string | null,
|
|
5463
|
+
string,
|
|
5464
|
+
string | null,
|
|
5465
|
+
string | null,
|
|
5466
|
+
string | null,
|
|
5467
|
+
string,
|
|
5468
|
+
string,
|
|
5469
|
+
]
|
|
5470
|
+
>(
|
|
5471
|
+
`INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, startedAt, lastHeartbeatAt)
|
|
5472
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5473
|
+
RETURNING *`,
|
|
5474
|
+
)
|
|
5475
|
+
.get(
|
|
5476
|
+
id,
|
|
5477
|
+
session.agentId,
|
|
5478
|
+
session.taskId ?? null,
|
|
5479
|
+
session.triggerType,
|
|
5480
|
+
session.inboxMessageId ?? null,
|
|
5481
|
+
session.taskDescription ?? null,
|
|
5482
|
+
session.runnerSessionId ?? null,
|
|
5483
|
+
now,
|
|
5484
|
+
now,
|
|
5485
|
+
);
|
|
5486
|
+
|
|
5487
|
+
if (!row) throw new Error("Failed to insert active session");
|
|
5488
|
+
return row;
|
|
5489
|
+
}
|
|
5490
|
+
|
|
5491
|
+
export function deleteActiveSession(taskId: string): boolean {
|
|
5492
|
+
const result = getDb().prepare("DELETE FROM active_sessions WHERE taskId = ?").run(taskId);
|
|
5493
|
+
return result.changes > 0;
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
export function deleteActiveSessionById(id: string): boolean {
|
|
5497
|
+
const result = getDb().prepare("DELETE FROM active_sessions WHERE id = ?").run(id);
|
|
5498
|
+
return result.changes > 0;
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
export function getActiveSessions(agentId?: string): ActiveSession[] {
|
|
5502
|
+
if (agentId) {
|
|
5503
|
+
return getDb()
|
|
5504
|
+
.prepare<ActiveSession, [string]>(
|
|
5505
|
+
"SELECT * FROM active_sessions WHERE agentId = ? ORDER BY startedAt DESC",
|
|
5506
|
+
)
|
|
5507
|
+
.all(agentId);
|
|
5508
|
+
}
|
|
5509
|
+
return getDb()
|
|
5510
|
+
.prepare<ActiveSession, []>("SELECT * FROM active_sessions ORDER BY startedAt DESC")
|
|
5511
|
+
.all();
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
export function heartbeatActiveSession(taskId: string): boolean {
|
|
5515
|
+
const now = new Date().toISOString();
|
|
5516
|
+
const result = getDb()
|
|
5517
|
+
.prepare("UPDATE active_sessions SET lastHeartbeatAt = ? WHERE taskId = ?")
|
|
5518
|
+
.run(now, taskId);
|
|
5519
|
+
return result.changes > 0;
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
export function cleanupStaleSessions(maxAgeMinutes = 30): number {
|
|
5523
|
+
const cutoff = new Date(Date.now() - maxAgeMinutes * 60 * 1000).toISOString();
|
|
5524
|
+
const result = getDb()
|
|
5525
|
+
.prepare("DELETE FROM active_sessions WHERE lastHeartbeatAt < ?")
|
|
5526
|
+
.run(cutoff);
|
|
5527
|
+
return result.changes;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
export function cleanupAgentSessions(agentId: string): number {
|
|
5531
|
+
const result = getDb().prepare("DELETE FROM active_sessions WHERE agentId = ?").run(agentId);
|
|
5532
|
+
return result.changes;
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
/** Update providerSessionId on an active session identified by taskId */
|
|
5536
|
+
export function updateActiveSessionProviderSessionId(
|
|
5537
|
+
taskId: string,
|
|
5538
|
+
providerSessionId: string,
|
|
5539
|
+
): boolean {
|
|
5540
|
+
const result = getDb()
|
|
5541
|
+
.prepare("UPDATE active_sessions SET providerSessionId = ? WHERE taskId = ?")
|
|
5542
|
+
.run(providerSessionId, taskId);
|
|
5543
|
+
return result.changes > 0;
|
|
5544
|
+
}
|
|
5545
|
+
|
|
5546
|
+
/**
|
|
5547
|
+
* Reassociate session logs from a runner session to a real task ID.
|
|
5548
|
+
* Used when a pool task is claimed — logs were stored under a random UUID,
|
|
5549
|
+
* this updates them to use the real task ID.
|
|
5550
|
+
* Idempotent — safe to call multiple times.
|
|
5551
|
+
*/
|
|
5552
|
+
export function reassociateSessionLogs(runnerSessionId: string, realTaskId: string): number {
|
|
5553
|
+
const result = getDb()
|
|
5554
|
+
.prepare("UPDATE session_logs SET taskId = ? WHERE sessionId = ? AND taskId != ?")
|
|
5555
|
+
.run(realTaskId, runnerSessionId, realTaskId);
|
|
5556
|
+
return result.changes;
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
// ============================================================================
|
|
5560
|
+
// Heartbeat / Triage Query Functions
|
|
5561
|
+
// ============================================================================
|
|
5562
|
+
|
|
5563
|
+
/**
|
|
5564
|
+
* Get in_progress tasks that haven't been updated within the given threshold.
|
|
5565
|
+
* Used by the heartbeat to detect potentially stalled tasks.
|
|
5566
|
+
*/
|
|
5567
|
+
export function getStalledInProgressTasks(thresholdMinutes: number = 30): AgentTask[] {
|
|
5568
|
+
const cutoff = new Date(Date.now() - thresholdMinutes * 60 * 1000).toISOString();
|
|
5569
|
+
return getDb()
|
|
5570
|
+
.prepare<AgentTaskRow, [string]>(
|
|
5571
|
+
`SELECT * FROM agent_tasks
|
|
5572
|
+
WHERE status = 'in_progress' AND lastUpdatedAt < ?
|
|
5573
|
+
ORDER BY lastUpdatedAt ASC`,
|
|
5574
|
+
)
|
|
5575
|
+
.all(cutoff)
|
|
5576
|
+
.map(rowToAgentTask);
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
/**
|
|
5580
|
+
* Get idle, non-lead, non-offline agents that have capacity for more tasks.
|
|
5581
|
+
* Used by the heartbeat for auto-assignment of pool tasks.
|
|
5582
|
+
*/
|
|
5583
|
+
export function getIdleWorkersWithCapacity(): Agent[] {
|
|
5584
|
+
const agents = getDb()
|
|
5585
|
+
.prepare<AgentRow, []>(
|
|
5586
|
+
`SELECT * FROM agents
|
|
5587
|
+
WHERE status = 'idle' AND isLead = 0`,
|
|
5588
|
+
)
|
|
5589
|
+
.all()
|
|
5590
|
+
.map(rowToAgent);
|
|
5591
|
+
|
|
5592
|
+
return agents.filter((agent) => {
|
|
5593
|
+
const activeCount = getActiveTaskCount(agent.id);
|
|
5594
|
+
return activeCount < (agent.maxTasks ?? 1);
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
|
|
5598
|
+
/**
|
|
5599
|
+
* Get unassigned pool tasks ordered by priority (DESC) then creation time (ASC).
|
|
5600
|
+
* Used by the heartbeat for auto-assignment.
|
|
5601
|
+
*/
|
|
5602
|
+
export function getUnassignedPoolTasks(limit: number = 10): AgentTask[] {
|
|
5603
|
+
return getDb()
|
|
5604
|
+
.prepare<AgentTaskRow, [number]>(
|
|
5605
|
+
`SELECT * FROM agent_tasks
|
|
5606
|
+
WHERE status = 'unassigned'
|
|
5607
|
+
ORDER BY priority DESC, createdAt ASC
|
|
5608
|
+
LIMIT ?`,
|
|
5609
|
+
)
|
|
5610
|
+
.all(limit)
|
|
5611
|
+
.map(rowToAgentTask);
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
// ============================================================================
|
|
5615
|
+
// Workflow CRUD
|
|
5616
|
+
// ============================================================================
|
|
5617
|
+
|
|
5618
|
+
type WorkflowRow = {
|
|
5619
|
+
id: string;
|
|
5620
|
+
name: string;
|
|
5621
|
+
description: string | null;
|
|
5622
|
+
enabled: number;
|
|
5623
|
+
definition: string;
|
|
5624
|
+
triggers: string;
|
|
5625
|
+
cooldown: string | null;
|
|
5626
|
+
input: string | null;
|
|
5627
|
+
triggerSchema: string | null;
|
|
5628
|
+
dir: string | null;
|
|
5629
|
+
vcs_repo: string | null;
|
|
5630
|
+
createdByAgentId: string | null;
|
|
5631
|
+
createdAt: string;
|
|
5632
|
+
lastUpdatedAt: string;
|
|
5633
|
+
};
|
|
5634
|
+
|
|
5635
|
+
function rowToWorkflow(row: WorkflowRow): Workflow {
|
|
5636
|
+
return {
|
|
5637
|
+
id: row.id,
|
|
5638
|
+
name: row.name,
|
|
5639
|
+
description: row.description ?? undefined,
|
|
5640
|
+
enabled: row.enabled === 1,
|
|
5641
|
+
definition: JSON.parse(row.definition) as WorkflowDefinition,
|
|
5642
|
+
triggers: JSON.parse(row.triggers) as TriggerConfig[],
|
|
5643
|
+
cooldown: row.cooldown ? (JSON.parse(row.cooldown) as CooldownConfig) : undefined,
|
|
5644
|
+
input: row.input ? (JSON.parse(row.input) as Record<string, InputValue>) : undefined,
|
|
5645
|
+
triggerSchema: row.triggerSchema
|
|
5646
|
+
? (JSON.parse(row.triggerSchema) as Record<string, unknown>)
|
|
5647
|
+
: undefined,
|
|
5648
|
+
dir: row.dir ?? undefined,
|
|
5649
|
+
vcsRepo: row.vcs_repo ?? undefined,
|
|
5650
|
+
createdByAgentId: row.createdByAgentId ?? undefined,
|
|
5651
|
+
createdAt: row.createdAt,
|
|
5652
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
5653
|
+
};
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
export function createWorkflow(data: {
|
|
5657
|
+
name: string;
|
|
5658
|
+
description?: string;
|
|
5659
|
+
definition: WorkflowDefinition;
|
|
5660
|
+
triggers?: TriggerConfig[];
|
|
5661
|
+
cooldown?: CooldownConfig;
|
|
5662
|
+
input?: Record<string, InputValue>;
|
|
5663
|
+
triggerSchema?: Record<string, unknown>;
|
|
5664
|
+
dir?: string;
|
|
5665
|
+
vcsRepo?: string;
|
|
5666
|
+
createdByAgentId?: string;
|
|
5667
|
+
}): Workflow {
|
|
5668
|
+
const id = crypto.randomUUID();
|
|
5669
|
+
const row = getDb()
|
|
5670
|
+
.prepare<
|
|
5671
|
+
WorkflowRow,
|
|
5672
|
+
[
|
|
5673
|
+
string,
|
|
5674
|
+
string,
|
|
5675
|
+
string | null,
|
|
5676
|
+
string,
|
|
5677
|
+
string,
|
|
5678
|
+
string | null,
|
|
5679
|
+
string | null,
|
|
5680
|
+
string | null,
|
|
5681
|
+
string | null,
|
|
5682
|
+
string | null,
|
|
5683
|
+
string | null,
|
|
5684
|
+
]
|
|
5685
|
+
>(
|
|
5686
|
+
`INSERT INTO workflows (id, name, description, definition, triggers, cooldown, input, triggerSchema, dir, vcs_repo, createdByAgentId)
|
|
5687
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5688
|
+
)
|
|
5689
|
+
.get(
|
|
5690
|
+
id,
|
|
5691
|
+
data.name,
|
|
5692
|
+
data.description ?? null,
|
|
5693
|
+
JSON.stringify(data.definition),
|
|
5694
|
+
JSON.stringify(data.triggers ?? []),
|
|
5695
|
+
data.cooldown ? JSON.stringify(data.cooldown) : null,
|
|
5696
|
+
data.input ? JSON.stringify(data.input) : null,
|
|
5697
|
+
data.triggerSchema ? JSON.stringify(data.triggerSchema) : null,
|
|
5698
|
+
data.dir ?? null,
|
|
5699
|
+
data.vcsRepo ?? null,
|
|
5700
|
+
data.createdByAgentId ?? null,
|
|
5701
|
+
);
|
|
5702
|
+
if (!row) throw new Error("Failed to create workflow");
|
|
5703
|
+
return rowToWorkflow(row);
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
export function getWorkflow(id: string): Workflow | null {
|
|
5707
|
+
const row = getDb()
|
|
5708
|
+
.prepare<WorkflowRow, [string]>("SELECT * FROM workflows WHERE id = ?")
|
|
5709
|
+
.get(id);
|
|
5710
|
+
return row ? rowToWorkflow(row) : null;
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
export function listWorkflows(filters?: { enabled?: boolean }): Workflow[] {
|
|
5714
|
+
let query = "SELECT * FROM workflows WHERE 1=1";
|
|
5715
|
+
const params: (string | number)[] = [];
|
|
5716
|
+
if (filters?.enabled !== undefined) {
|
|
5717
|
+
query += " AND enabled = ?";
|
|
5718
|
+
params.push(filters.enabled ? 1 : 0);
|
|
5719
|
+
}
|
|
5720
|
+
query += " ORDER BY name ASC";
|
|
5721
|
+
return getDb()
|
|
5722
|
+
.prepare<WorkflowRow, (string | number)[]>(query)
|
|
5723
|
+
.all(...params)
|
|
5724
|
+
.map(rowToWorkflow);
|
|
5725
|
+
}
|
|
5726
|
+
|
|
5727
|
+
export function updateWorkflow(
|
|
5728
|
+
id: string,
|
|
5729
|
+
data: {
|
|
5730
|
+
name?: string;
|
|
5731
|
+
description?: string;
|
|
5732
|
+
enabled?: boolean;
|
|
5733
|
+
definition?: WorkflowDefinition;
|
|
5734
|
+
triggers?: TriggerConfig[];
|
|
5735
|
+
cooldown?: CooldownConfig | null;
|
|
5736
|
+
input?: Record<string, InputValue> | null;
|
|
5737
|
+
triggerSchema?: Record<string, unknown> | null;
|
|
5738
|
+
dir?: string | null;
|
|
5739
|
+
vcsRepo?: string | null;
|
|
5740
|
+
},
|
|
5741
|
+
): Workflow | null {
|
|
5742
|
+
const updates: string[] = [];
|
|
5743
|
+
const params: (string | number | null)[] = [];
|
|
5744
|
+
if (data.name !== undefined) {
|
|
5745
|
+
updates.push("name = ?");
|
|
5746
|
+
params.push(data.name);
|
|
5747
|
+
}
|
|
5748
|
+
if (data.description !== undefined) {
|
|
5749
|
+
updates.push("description = ?");
|
|
5750
|
+
params.push(data.description);
|
|
5751
|
+
}
|
|
5752
|
+
if (data.enabled !== undefined) {
|
|
5753
|
+
updates.push("enabled = ?");
|
|
5754
|
+
params.push(data.enabled ? 1 : 0);
|
|
5755
|
+
}
|
|
5756
|
+
if (data.definition !== undefined) {
|
|
5757
|
+
updates.push("definition = ?");
|
|
5758
|
+
params.push(JSON.stringify(data.definition));
|
|
5759
|
+
}
|
|
5760
|
+
if (data.triggers !== undefined) {
|
|
5761
|
+
updates.push("triggers = ?");
|
|
5762
|
+
params.push(JSON.stringify(data.triggers));
|
|
5763
|
+
}
|
|
5764
|
+
if (data.cooldown !== undefined) {
|
|
5765
|
+
updates.push("cooldown = ?");
|
|
5766
|
+
params.push(data.cooldown ? JSON.stringify(data.cooldown) : null);
|
|
5767
|
+
}
|
|
5768
|
+
if (data.input !== undefined) {
|
|
5769
|
+
updates.push("input = ?");
|
|
5770
|
+
params.push(data.input ? JSON.stringify(data.input) : null);
|
|
5771
|
+
}
|
|
5772
|
+
if (data.triggerSchema !== undefined) {
|
|
5773
|
+
updates.push("triggerSchema = ?");
|
|
5774
|
+
params.push(data.triggerSchema ? JSON.stringify(data.triggerSchema) : null);
|
|
5775
|
+
}
|
|
5776
|
+
if (data.dir !== undefined) {
|
|
5777
|
+
updates.push("dir = ?");
|
|
5778
|
+
params.push(data.dir ?? null);
|
|
5779
|
+
}
|
|
5780
|
+
if (data.vcsRepo !== undefined) {
|
|
5781
|
+
updates.push("vcs_repo = ?");
|
|
5782
|
+
params.push(data.vcsRepo ?? null);
|
|
5783
|
+
}
|
|
5784
|
+
if (updates.length === 0) return getWorkflow(id);
|
|
5785
|
+
updates.push("lastUpdatedAt = ?");
|
|
5786
|
+
params.push(new Date().toISOString());
|
|
5787
|
+
params.push(id);
|
|
5788
|
+
const row = getDb()
|
|
5789
|
+
.prepare<WorkflowRow, (string | number | null)[]>(
|
|
5790
|
+
`UPDATE workflows SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
|
|
5791
|
+
)
|
|
5792
|
+
.get(...params);
|
|
5793
|
+
return row ? rowToWorkflow(row) : null;
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5796
|
+
export function deleteWorkflow(id: string): boolean {
|
|
5797
|
+
const db = getDb();
|
|
5798
|
+
// Cascade delete in FK-safe order:
|
|
5799
|
+
// 1. Unlink agent_tasks (they reference steps and runs)
|
|
5800
|
+
db.run(
|
|
5801
|
+
`UPDATE agent_tasks SET workflowRunId = NULL, workflowRunStepId = NULL WHERE workflowRunId IN (SELECT id FROM workflow_runs WHERE workflowId = ?)`,
|
|
5802
|
+
[id],
|
|
5803
|
+
);
|
|
5804
|
+
// 2. Delete steps (they reference runs)
|
|
5805
|
+
db.run(
|
|
5806
|
+
`DELETE FROM workflow_run_steps WHERE runId IN (SELECT id FROM workflow_runs WHERE workflowId = ?)`,
|
|
5807
|
+
[id],
|
|
5808
|
+
);
|
|
5809
|
+
// 3. Delete runs (they reference workflow)
|
|
5810
|
+
db.run("DELETE FROM workflow_runs WHERE workflowId = ?", [id]);
|
|
5811
|
+
// 4. Delete workflow
|
|
5812
|
+
const result = db.run("DELETE FROM workflows WHERE id = ?", [id]);
|
|
5813
|
+
return result.changes > 0;
|
|
5814
|
+
}
|
|
5815
|
+
|
|
5816
|
+
/**
|
|
5817
|
+
* Find enabled workflows that have a schedule trigger matching the given scheduleId.
|
|
5818
|
+
* Uses SQLite JSON functions to query into the triggers JSON array.
|
|
5819
|
+
*/
|
|
5820
|
+
export function getWorkflowsByScheduleId(scheduleId: string): Workflow[] {
|
|
5821
|
+
const rows = getDb()
|
|
5822
|
+
.prepare<WorkflowRow, [string]>(
|
|
5823
|
+
`SELECT w.* FROM workflows w, json_each(w.triggers) AS t
|
|
5824
|
+
WHERE w.enabled = 1
|
|
5825
|
+
AND json_extract(t.value, '$.type') = 'schedule'
|
|
5826
|
+
AND json_extract(t.value, '$.scheduleId') = ?`,
|
|
5827
|
+
)
|
|
5828
|
+
.all(scheduleId);
|
|
5829
|
+
return rows.map(rowToWorkflow);
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
// ============================================================================
|
|
5833
|
+
// Workflow Run CRUD
|
|
5834
|
+
// ============================================================================
|
|
5835
|
+
|
|
5836
|
+
type WorkflowRunRow = {
|
|
5837
|
+
id: string;
|
|
5838
|
+
workflowId: string;
|
|
5839
|
+
status: string;
|
|
5840
|
+
triggerData: string | null;
|
|
5841
|
+
context: string | null;
|
|
5842
|
+
error: string | null;
|
|
5843
|
+
startedAt: string;
|
|
5844
|
+
lastUpdatedAt: string;
|
|
5845
|
+
finishedAt: string | null;
|
|
5846
|
+
};
|
|
5847
|
+
|
|
5848
|
+
function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
|
|
5849
|
+
return {
|
|
5850
|
+
id: row.id,
|
|
5851
|
+
workflowId: row.workflowId,
|
|
5852
|
+
status: row.status as WorkflowRunStatus,
|
|
5853
|
+
triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
|
|
5854
|
+
context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
|
|
5855
|
+
error: row.error ?? undefined,
|
|
5856
|
+
startedAt: row.startedAt,
|
|
5857
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
5858
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
5859
|
+
};
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
export function createWorkflowRun(data: {
|
|
5863
|
+
id: string;
|
|
5864
|
+
workflowId: string;
|
|
5865
|
+
triggerData?: unknown;
|
|
5866
|
+
}): WorkflowRun {
|
|
5867
|
+
const now = new Date().toISOString();
|
|
5868
|
+
const row = getDb()
|
|
5869
|
+
.prepare<WorkflowRunRow, [string, string, string, string | null]>(
|
|
5870
|
+
`INSERT INTO workflow_runs (id, workflowId, startedAt, triggerData) VALUES (?, ?, ?, ?) RETURNING *`,
|
|
5871
|
+
)
|
|
5872
|
+
.get(data.id, data.workflowId, now, data.triggerData ? JSON.stringify(data.triggerData) : null);
|
|
5873
|
+
if (!row) throw new Error("Failed to create workflow run");
|
|
5874
|
+
return rowToWorkflowRun(row);
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
export function getWorkflowRun(id: string): WorkflowRun | null {
|
|
5878
|
+
const row = getDb()
|
|
5879
|
+
.prepare<WorkflowRunRow, [string]>("SELECT * FROM workflow_runs WHERE id = ?")
|
|
5880
|
+
.get(id);
|
|
5881
|
+
return row ? rowToWorkflowRun(row) : null;
|
|
5882
|
+
}
|
|
5883
|
+
|
|
5884
|
+
export function updateWorkflowRun(
|
|
5885
|
+
id: string,
|
|
5886
|
+
data: {
|
|
5887
|
+
status?: WorkflowRunStatus;
|
|
5888
|
+
context?: Record<string, unknown>;
|
|
5889
|
+
error?: string;
|
|
5890
|
+
finishedAt?: string;
|
|
5891
|
+
},
|
|
5892
|
+
): WorkflowRun | null {
|
|
5893
|
+
const updates: string[] = [];
|
|
5894
|
+
const params: (string | null)[] = [];
|
|
5895
|
+
if (data.status !== undefined) {
|
|
5896
|
+
updates.push("status = ?");
|
|
5897
|
+
params.push(data.status);
|
|
5898
|
+
}
|
|
5899
|
+
if (data.context !== undefined) {
|
|
5900
|
+
updates.push("context = ?");
|
|
5901
|
+
params.push(JSON.stringify(data.context));
|
|
5902
|
+
}
|
|
5903
|
+
if (data.error !== undefined) {
|
|
5904
|
+
updates.push("error = ?");
|
|
5905
|
+
params.push(data.error);
|
|
5906
|
+
}
|
|
5907
|
+
if (data.finishedAt !== undefined) {
|
|
5908
|
+
updates.push("finishedAt = ?");
|
|
5909
|
+
params.push(data.finishedAt);
|
|
5910
|
+
}
|
|
5911
|
+
if (updates.length === 0) return getWorkflowRun(id);
|
|
5912
|
+
updates.push("lastUpdatedAt = ?");
|
|
5913
|
+
params.push(new Date().toISOString());
|
|
5914
|
+
params.push(id);
|
|
5915
|
+
const row = getDb()
|
|
5916
|
+
.prepare<WorkflowRunRow, (string | null)[]>(
|
|
5917
|
+
`UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
|
|
5918
|
+
)
|
|
5919
|
+
.get(...params);
|
|
5920
|
+
return row ? rowToWorkflowRun(row) : null;
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
export function listWorkflowRuns(workflowId: string): WorkflowRun[] {
|
|
5924
|
+
return getDb()
|
|
5925
|
+
.prepare<WorkflowRunRow, [string]>(
|
|
5926
|
+
"SELECT * FROM workflow_runs WHERE workflowId = ? ORDER BY startedAt DESC",
|
|
5927
|
+
)
|
|
5928
|
+
.all(workflowId)
|
|
5929
|
+
.map(rowToWorkflowRun);
|
|
5930
|
+
}
|
|
5931
|
+
|
|
5932
|
+
// ============================================================================
|
|
5933
|
+
// Workflow Run Step CRUD
|
|
5934
|
+
// ============================================================================
|
|
5935
|
+
|
|
5936
|
+
type WorkflowRunStepRow = {
|
|
5937
|
+
id: string;
|
|
5938
|
+
runId: string;
|
|
5939
|
+
nodeId: string;
|
|
5940
|
+
nodeType: string;
|
|
5941
|
+
status: string;
|
|
5942
|
+
input: string | null;
|
|
5943
|
+
output: string | null;
|
|
5944
|
+
error: string | null;
|
|
5945
|
+
startedAt: string;
|
|
5946
|
+
finishedAt: string | null;
|
|
5947
|
+
retryCount: number;
|
|
5948
|
+
maxRetries: number;
|
|
5949
|
+
nextRetryAt: string | null;
|
|
5950
|
+
idempotencyKey: string | null;
|
|
5951
|
+
diagnostics: string | null;
|
|
5952
|
+
nextPort: string | null;
|
|
5953
|
+
};
|
|
5954
|
+
|
|
5955
|
+
function rowToWorkflowRunStep(row: WorkflowRunStepRow): WorkflowRunStep {
|
|
5956
|
+
return {
|
|
5957
|
+
id: row.id,
|
|
5958
|
+
runId: row.runId,
|
|
5959
|
+
nodeId: row.nodeId,
|
|
5960
|
+
nodeType: row.nodeType,
|
|
5961
|
+
status: row.status as WorkflowRunStepStatus,
|
|
5962
|
+
input: row.input ? JSON.parse(row.input) : undefined,
|
|
5963
|
+
output: row.output ? JSON.parse(row.output) : undefined,
|
|
5964
|
+
error: row.error ?? undefined,
|
|
5965
|
+
startedAt: row.startedAt,
|
|
5966
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
5967
|
+
retryCount: row.retryCount,
|
|
5968
|
+
maxRetries: row.maxRetries,
|
|
5969
|
+
nextRetryAt: row.nextRetryAt ?? undefined,
|
|
5970
|
+
idempotencyKey: row.idempotencyKey ?? undefined,
|
|
5971
|
+
diagnostics: row.diagnostics ?? undefined,
|
|
5972
|
+
nextPort: row.nextPort ?? undefined,
|
|
5973
|
+
};
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
export function createWorkflowRunStep(data: {
|
|
5977
|
+
id: string;
|
|
5978
|
+
runId: string;
|
|
5979
|
+
nodeId: string;
|
|
5980
|
+
nodeType: string;
|
|
5981
|
+
input?: unknown;
|
|
5982
|
+
}): WorkflowRunStep {
|
|
5983
|
+
const now = new Date().toISOString();
|
|
5984
|
+
const row = getDb()
|
|
5985
|
+
.prepare<WorkflowRunStepRow, [string, string, string, string, string, string | null]>(
|
|
5986
|
+
`INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, status, startedAt, input)
|
|
5987
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?) RETURNING *`,
|
|
5988
|
+
)
|
|
5989
|
+
.get(
|
|
5990
|
+
data.id,
|
|
5991
|
+
data.runId,
|
|
5992
|
+
data.nodeId,
|
|
5993
|
+
data.nodeType,
|
|
5994
|
+
now,
|
|
5995
|
+
data.input ? JSON.stringify(data.input) : null,
|
|
5996
|
+
);
|
|
5997
|
+
if (!row) throw new Error("Failed to create workflow run step");
|
|
5998
|
+
return rowToWorkflowRunStep(row);
|
|
5999
|
+
}
|
|
6000
|
+
|
|
6001
|
+
export function getWorkflowRunStep(id: string): WorkflowRunStep | null {
|
|
6002
|
+
const row = getDb()
|
|
6003
|
+
.prepare<WorkflowRunStepRow, [string]>("SELECT * FROM workflow_run_steps WHERE id = ?")
|
|
6004
|
+
.get(id);
|
|
6005
|
+
return row ? rowToWorkflowRunStep(row) : null;
|
|
6006
|
+
}
|
|
6007
|
+
|
|
6008
|
+
export function updateWorkflowRunStep(
|
|
6009
|
+
id: string,
|
|
6010
|
+
data: {
|
|
6011
|
+
status?: WorkflowRunStepStatus;
|
|
6012
|
+
output?: unknown;
|
|
6013
|
+
error?: string;
|
|
6014
|
+
finishedAt?: string;
|
|
6015
|
+
retryCount?: number;
|
|
6016
|
+
maxRetries?: number;
|
|
6017
|
+
nextRetryAt?: string | null;
|
|
6018
|
+
idempotencyKey?: string;
|
|
6019
|
+
diagnostics?: string;
|
|
6020
|
+
nextPort?: string;
|
|
6021
|
+
},
|
|
6022
|
+
): WorkflowRunStep | null {
|
|
6023
|
+
const updates: string[] = [];
|
|
6024
|
+
const params: (string | number | null)[] = [];
|
|
6025
|
+
if (data.status !== undefined) {
|
|
6026
|
+
updates.push("status = ?");
|
|
6027
|
+
params.push(data.status);
|
|
6028
|
+
}
|
|
6029
|
+
if (data.output !== undefined) {
|
|
6030
|
+
updates.push("output = ?");
|
|
6031
|
+
params.push(JSON.stringify(data.output));
|
|
6032
|
+
}
|
|
6033
|
+
if (data.error !== undefined) {
|
|
6034
|
+
updates.push("error = ?");
|
|
6035
|
+
params.push(data.error);
|
|
6036
|
+
}
|
|
6037
|
+
if (data.finishedAt !== undefined) {
|
|
6038
|
+
updates.push("finishedAt = ?");
|
|
6039
|
+
params.push(data.finishedAt);
|
|
6040
|
+
}
|
|
6041
|
+
if (data.retryCount !== undefined) {
|
|
6042
|
+
updates.push("retryCount = ?");
|
|
6043
|
+
params.push(data.retryCount);
|
|
6044
|
+
}
|
|
6045
|
+
if (data.maxRetries !== undefined) {
|
|
6046
|
+
updates.push("maxRetries = ?");
|
|
6047
|
+
params.push(data.maxRetries);
|
|
6048
|
+
}
|
|
6049
|
+
if (data.nextRetryAt !== undefined) {
|
|
6050
|
+
updates.push("nextRetryAt = ?");
|
|
6051
|
+
params.push(data.nextRetryAt);
|
|
6052
|
+
}
|
|
6053
|
+
if (data.idempotencyKey !== undefined) {
|
|
6054
|
+
updates.push("idempotencyKey = ?");
|
|
6055
|
+
params.push(data.idempotencyKey);
|
|
6056
|
+
}
|
|
6057
|
+
if (data.diagnostics !== undefined) {
|
|
6058
|
+
updates.push("diagnostics = ?");
|
|
6059
|
+
params.push(data.diagnostics);
|
|
6060
|
+
}
|
|
6061
|
+
if (data.nextPort !== undefined) {
|
|
6062
|
+
updates.push("nextPort = ?");
|
|
6063
|
+
params.push(data.nextPort);
|
|
6064
|
+
}
|
|
6065
|
+
if (updates.length === 0) return getWorkflowRunStep(id);
|
|
6066
|
+
params.push(id);
|
|
6067
|
+
const row = getDb()
|
|
6068
|
+
.prepare<WorkflowRunStepRow, (string | number | null)[]>(
|
|
6069
|
+
`UPDATE workflow_run_steps SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
|
|
6070
|
+
)
|
|
6071
|
+
.get(...params);
|
|
6072
|
+
return row ? rowToWorkflowRunStep(row) : null;
|
|
6073
|
+
}
|
|
6074
|
+
|
|
6075
|
+
export function getWorkflowRunStepsByRunId(runId: string): WorkflowRunStep[] {
|
|
6076
|
+
return getDb()
|
|
6077
|
+
.prepare<WorkflowRunStepRow, [string]>(
|
|
6078
|
+
"SELECT * FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC",
|
|
6079
|
+
)
|
|
6080
|
+
.all(runId)
|
|
6081
|
+
.map(rowToWorkflowRunStep);
|
|
6082
|
+
}
|
|
6083
|
+
|
|
6084
|
+
// --- Stuck Workflow Run Recovery ---
|
|
6085
|
+
|
|
6086
|
+
export interface StuckWorkflowRun {
|
|
6087
|
+
runId: string;
|
|
6088
|
+
stepId: string;
|
|
6089
|
+
nodeId: string;
|
|
6090
|
+
taskStatus: string;
|
|
6091
|
+
taskOutput: string | null;
|
|
6092
|
+
workflowId: string;
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
export function getStuckWorkflowRuns(): StuckWorkflowRun[] {
|
|
6096
|
+
return getDb()
|
|
6097
|
+
.prepare<StuckWorkflowRun, []>(
|
|
6098
|
+
`SELECT
|
|
6099
|
+
wr.id as runId,
|
|
6100
|
+
wrs.id as stepId,
|
|
6101
|
+
wrs.nodeId,
|
|
6102
|
+
at.status as taskStatus,
|
|
6103
|
+
at.output as taskOutput,
|
|
6104
|
+
wr.workflowId
|
|
6105
|
+
FROM workflow_runs wr
|
|
6106
|
+
JOIN workflow_run_steps wrs ON wrs.runId = wr.id AND wrs.status = 'waiting'
|
|
6107
|
+
JOIN agent_tasks at ON at.workflowRunStepId = wrs.id
|
|
6108
|
+
WHERE wr.status = 'waiting'
|
|
6109
|
+
AND at.status IN ('completed', 'failed', 'cancelled')`,
|
|
6110
|
+
)
|
|
6111
|
+
.all();
|
|
6112
|
+
}
|
|
6113
|
+
|
|
6114
|
+
// --- New Workflow Query Functions ---
|
|
6115
|
+
|
|
6116
|
+
export function getLastSuccessfulRun(workflowId: string): WorkflowRun | null {
|
|
6117
|
+
const row = getDb()
|
|
6118
|
+
.prepare<WorkflowRunRow, [string]>(
|
|
6119
|
+
`SELECT * FROM workflow_runs
|
|
6120
|
+
WHERE workflowId = ? AND status = 'completed'
|
|
6121
|
+
ORDER BY finishedAt DESC LIMIT 1`,
|
|
6122
|
+
)
|
|
6123
|
+
.get(workflowId);
|
|
6124
|
+
return row ? rowToWorkflowRun(row) : null;
|
|
6125
|
+
}
|
|
6126
|
+
|
|
6127
|
+
export function getRetryableSteps(): WorkflowRunStep[] {
|
|
6128
|
+
const now = new Date().toISOString();
|
|
6129
|
+
return getDb()
|
|
6130
|
+
.prepare<WorkflowRunStepRow, [string]>(
|
|
6131
|
+
`SELECT * FROM workflow_run_steps
|
|
6132
|
+
WHERE status = 'failed'
|
|
6133
|
+
AND nextRetryAt IS NOT NULL
|
|
6134
|
+
AND nextRetryAt <= ?
|
|
6135
|
+
ORDER BY nextRetryAt ASC`,
|
|
6136
|
+
)
|
|
6137
|
+
.all(now)
|
|
6138
|
+
.map(rowToWorkflowRunStep);
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
export function getCompletedStepNodeIds(runId: string): string[] {
|
|
6142
|
+
const rows = getDb()
|
|
6143
|
+
.prepare<{ nodeId: string }, [string]>(
|
|
6144
|
+
`SELECT nodeId FROM workflow_run_steps
|
|
6145
|
+
WHERE runId = ? AND status = 'completed'`,
|
|
6146
|
+
)
|
|
6147
|
+
.all(runId);
|
|
6148
|
+
return rows.map((r) => r.nodeId);
|
|
6149
|
+
}
|
|
6150
|
+
|
|
6151
|
+
export function getTaskByWorkflowRunStepId(stepId: string): AgentTask | null {
|
|
6152
|
+
const row = getDb()
|
|
6153
|
+
.prepare<AgentTaskRow, [string]>(
|
|
6154
|
+
"SELECT * FROM agent_tasks WHERE workflowRunStepId = ? LIMIT 1",
|
|
6155
|
+
)
|
|
6156
|
+
.get(stepId);
|
|
6157
|
+
return row ? rowToAgentTask(row) : null;
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
export function getStepByIdempotencyKey(key: string): WorkflowRunStep | null {
|
|
6161
|
+
const row = getDb()
|
|
6162
|
+
.prepare<WorkflowRunStepRow, [string]>(
|
|
6163
|
+
"SELECT * FROM workflow_run_steps WHERE idempotencyKey = ?",
|
|
6164
|
+
)
|
|
6165
|
+
.get(key);
|
|
6166
|
+
return row ? rowToWorkflowRunStep(row) : null;
|
|
6167
|
+
}
|
|
6168
|
+
|
|
6169
|
+
// --- Workflow Version History ---
|
|
6170
|
+
|
|
6171
|
+
type WorkflowVersionRow = {
|
|
6172
|
+
id: string;
|
|
6173
|
+
workflowId: string;
|
|
6174
|
+
version: number;
|
|
6175
|
+
snapshot: string;
|
|
6176
|
+
changedByAgentId: string | null;
|
|
6177
|
+
createdAt: string;
|
|
6178
|
+
};
|
|
6179
|
+
|
|
6180
|
+
function rowToWorkflowVersion(row: WorkflowVersionRow): WorkflowVersion {
|
|
6181
|
+
return {
|
|
6182
|
+
id: row.id,
|
|
6183
|
+
workflowId: row.workflowId,
|
|
6184
|
+
version: row.version,
|
|
6185
|
+
snapshot: JSON.parse(row.snapshot) as WorkflowSnapshot,
|
|
6186
|
+
changedByAgentId: row.changedByAgentId ?? undefined,
|
|
6187
|
+
createdAt: row.createdAt,
|
|
6188
|
+
};
|
|
6189
|
+
}
|
|
6190
|
+
|
|
6191
|
+
export function createWorkflowVersion(data: {
|
|
6192
|
+
workflowId: string;
|
|
6193
|
+
version: number;
|
|
6194
|
+
snapshot: WorkflowSnapshot;
|
|
6195
|
+
changedByAgentId?: string;
|
|
6196
|
+
}): WorkflowVersion {
|
|
6197
|
+
const id = crypto.randomUUID();
|
|
6198
|
+
const row = getDb()
|
|
6199
|
+
.prepare<WorkflowVersionRow, [string, string, number, string, string | null]>(
|
|
6200
|
+
`INSERT INTO workflow_versions (id, workflowId, version, snapshot, changedByAgentId)
|
|
6201
|
+
VALUES (?, ?, ?, ?, ?) RETURNING *`,
|
|
6202
|
+
)
|
|
6203
|
+
.get(
|
|
6204
|
+
id,
|
|
6205
|
+
data.workflowId,
|
|
6206
|
+
data.version,
|
|
6207
|
+
JSON.stringify(data.snapshot),
|
|
6208
|
+
data.changedByAgentId ?? null,
|
|
6209
|
+
);
|
|
6210
|
+
if (!row) throw new Error("Failed to create workflow version");
|
|
6211
|
+
return rowToWorkflowVersion(row);
|
|
6212
|
+
}
|
|
6213
|
+
|
|
6214
|
+
export function getWorkflowVersions(workflowId: string): WorkflowVersion[] {
|
|
6215
|
+
return getDb()
|
|
6216
|
+
.prepare<WorkflowVersionRow, [string]>(
|
|
6217
|
+
"SELECT * FROM workflow_versions WHERE workflowId = ? ORDER BY version DESC",
|
|
6218
|
+
)
|
|
6219
|
+
.all(workflowId)
|
|
6220
|
+
.map(rowToWorkflowVersion);
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
export function getWorkflowVersion(workflowId: string, version: number): WorkflowVersion | null {
|
|
6224
|
+
const row = getDb()
|
|
6225
|
+
.prepare<WorkflowVersionRow, [string, number]>(
|
|
6226
|
+
"SELECT * FROM workflow_versions WHERE workflowId = ? AND version = ?",
|
|
6227
|
+
)
|
|
6228
|
+
.get(workflowId, version);
|
|
6229
|
+
return row ? rowToWorkflowVersion(row) : null;
|
|
6230
|
+
}
|
|
6231
|
+
|
|
6232
|
+
// ============================================================================
|
|
6233
|
+
// Prompt Template Operations
|
|
6234
|
+
// ============================================================================
|
|
6235
|
+
|
|
6236
|
+
type PromptTemplateRow = {
|
|
6237
|
+
id: string;
|
|
6238
|
+
eventType: string;
|
|
6239
|
+
scope: string;
|
|
6240
|
+
scopeId: string | null;
|
|
6241
|
+
state: string;
|
|
6242
|
+
body: string;
|
|
6243
|
+
isDefault: number; // SQLite boolean
|
|
6244
|
+
version: number;
|
|
6245
|
+
createdBy: string | null;
|
|
6246
|
+
createdAt: string;
|
|
6247
|
+
updatedAt: string;
|
|
6248
|
+
};
|
|
6249
|
+
|
|
6250
|
+
type PromptTemplateHistoryRow = {
|
|
6251
|
+
id: string;
|
|
6252
|
+
templateId: string;
|
|
6253
|
+
version: number;
|
|
6254
|
+
body: string;
|
|
6255
|
+
state: string;
|
|
6256
|
+
changedBy: string | null;
|
|
6257
|
+
changedAt: string;
|
|
6258
|
+
changeReason: string | null;
|
|
6259
|
+
};
|
|
6260
|
+
|
|
6261
|
+
function rowToPromptTemplate(row: PromptTemplateRow): PromptTemplate {
|
|
6262
|
+
return {
|
|
6263
|
+
id: row.id,
|
|
6264
|
+
eventType: row.eventType,
|
|
6265
|
+
scope: row.scope as "global" | "agent" | "repo",
|
|
6266
|
+
scopeId: row.scopeId ?? null,
|
|
6267
|
+
state: row.state as "enabled" | "default_prompt_fallback" | "skip_event",
|
|
6268
|
+
body: row.body,
|
|
6269
|
+
isDefault: row.isDefault === 1,
|
|
6270
|
+
version: row.version,
|
|
6271
|
+
createdBy: row.createdBy ?? null,
|
|
6272
|
+
createdAt: row.createdAt,
|
|
6273
|
+
updatedAt: row.updatedAt,
|
|
6274
|
+
};
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6277
|
+
function rowToPromptTemplateHistory(row: PromptTemplateHistoryRow): PromptTemplateHistory {
|
|
6278
|
+
return {
|
|
6279
|
+
id: row.id,
|
|
6280
|
+
templateId: row.templateId,
|
|
6281
|
+
version: row.version,
|
|
6282
|
+
body: row.body,
|
|
6283
|
+
state: row.state,
|
|
6284
|
+
changedBy: row.changedBy ?? null,
|
|
6285
|
+
changedAt: row.changedAt,
|
|
6286
|
+
changeReason: row.changeReason ?? null,
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
|
|
6290
|
+
/**
|
|
6291
|
+
* List prompt templates with optional filters.
|
|
6292
|
+
*/
|
|
6293
|
+
export function getPromptTemplates(filters?: {
|
|
6294
|
+
eventType?: string;
|
|
6295
|
+
scope?: string;
|
|
6296
|
+
scopeId?: string;
|
|
6297
|
+
isDefault?: boolean;
|
|
6298
|
+
}): PromptTemplate[] {
|
|
6299
|
+
const conditions: string[] = [];
|
|
6300
|
+
const params: (string | number)[] = [];
|
|
6301
|
+
|
|
6302
|
+
if (filters?.eventType) {
|
|
6303
|
+
conditions.push("eventType = ?");
|
|
6304
|
+
params.push(filters.eventType);
|
|
6305
|
+
}
|
|
6306
|
+
if (filters?.scope) {
|
|
6307
|
+
conditions.push("scope = ?");
|
|
6308
|
+
params.push(filters.scope);
|
|
6309
|
+
}
|
|
6310
|
+
if (filters?.scopeId) {
|
|
6311
|
+
conditions.push("scopeId = ?");
|
|
6312
|
+
params.push(filters.scopeId);
|
|
6313
|
+
}
|
|
6314
|
+
if (filters?.isDefault !== undefined) {
|
|
6315
|
+
conditions.push("isDefault = ?");
|
|
6316
|
+
params.push(filters.isDefault ? 1 : 0);
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6319
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
6320
|
+
const query = `SELECT * FROM prompt_templates ${whereClause} ORDER BY eventType ASC`;
|
|
6321
|
+
|
|
6322
|
+
return getDb()
|
|
6323
|
+
.prepare<PromptTemplateRow, (string | number)[]>(query)
|
|
6324
|
+
.all(...params)
|
|
6325
|
+
.map(rowToPromptTemplate);
|
|
6326
|
+
}
|
|
6327
|
+
|
|
6328
|
+
/**
|
|
6329
|
+
* Get a single prompt template by ID.
|
|
6330
|
+
*/
|
|
6331
|
+
export function getPromptTemplateById(id: string): PromptTemplate | null {
|
|
6332
|
+
const row = getDb()
|
|
6333
|
+
.prepare<PromptTemplateRow, [string]>("SELECT * FROM prompt_templates WHERE id = ?")
|
|
6334
|
+
.get(id);
|
|
6335
|
+
return row ? rowToPromptTemplate(row) : null;
|
|
6336
|
+
}
|
|
6337
|
+
|
|
6338
|
+
/**
|
|
6339
|
+
* Upsert a prompt template. Inserts or updates by (eventType, scope, scopeId) unique constraint.
|
|
6340
|
+
* Creates a history entry on both insert and update.
|
|
6341
|
+
*/
|
|
6342
|
+
export function upsertPromptTemplate(data: {
|
|
6343
|
+
eventType: string;
|
|
6344
|
+
scope: "global" | "agent" | "repo";
|
|
6345
|
+
scopeId?: string | null;
|
|
6346
|
+
state?: "enabled" | "default_prompt_fallback" | "skip_event";
|
|
6347
|
+
body: string;
|
|
6348
|
+
createdBy?: string | null;
|
|
6349
|
+
changedBy?: string | null;
|
|
6350
|
+
changeReason?: string | null;
|
|
6351
|
+
isDefault?: boolean;
|
|
6352
|
+
}): PromptTemplate {
|
|
6353
|
+
const now = new Date().toISOString();
|
|
6354
|
+
const scopeId = data.scope === "global" ? null : (data.scopeId ?? null);
|
|
6355
|
+
const state = data.state ?? "enabled";
|
|
6356
|
+
const createdBy = data.createdBy ?? data.changedBy ?? null;
|
|
6357
|
+
const changedBy = data.changedBy ?? data.createdBy ?? null;
|
|
6358
|
+
const changeReason = data.changeReason ?? null;
|
|
6359
|
+
|
|
6360
|
+
// Manual check for existing entry because SQLite's UNIQUE constraint
|
|
6361
|
+
// treats NULL != NULL, so ON CONFLICT never fires when scopeId is NULL (global scope).
|
|
6362
|
+
const existing =
|
|
6363
|
+
scopeId === null
|
|
6364
|
+
? getDb()
|
|
6365
|
+
.prepare<PromptTemplateRow, [string, string]>(
|
|
6366
|
+
"SELECT * FROM prompt_templates WHERE eventType = ? AND scope = ? AND scopeId IS NULL",
|
|
6367
|
+
)
|
|
6368
|
+
.get(data.eventType, data.scope)
|
|
6369
|
+
: getDb()
|
|
6370
|
+
.prepare<PromptTemplateRow, [string, string, string]>(
|
|
6371
|
+
"SELECT * FROM prompt_templates WHERE eventType = ? AND scope = ? AND scopeId = ?",
|
|
6372
|
+
)
|
|
6373
|
+
.get(data.eventType, data.scope, scopeId);
|
|
6374
|
+
|
|
6375
|
+
let row: PromptTemplateRow | null;
|
|
6376
|
+
|
|
6377
|
+
if (existing) {
|
|
6378
|
+
// If upserting at global scope and existing record has isDefault=true, flip it to false
|
|
6379
|
+
const newIsDefault =
|
|
6380
|
+
data.scope === "global" && existing.isDefault === 1 ? 0 : existing.isDefault;
|
|
6381
|
+
const newVersion = existing.version + 1;
|
|
6382
|
+
|
|
6383
|
+
row = getDb()
|
|
6384
|
+
.prepare<PromptTemplateRow, [string, string, number, number, string, string]>(
|
|
6385
|
+
`UPDATE prompt_templates SET body = ?, state = ?, isDefault = ?, version = ?, updatedAt = ?
|
|
6386
|
+
WHERE id = ? RETURNING *`,
|
|
6387
|
+
)
|
|
6388
|
+
.get(data.body, state, newIsDefault, newVersion, now, existing.id);
|
|
6389
|
+
|
|
6390
|
+
// Create history entry for the update
|
|
6391
|
+
getDb()
|
|
6392
|
+
.prepare(
|
|
6393
|
+
`INSERT INTO prompt_template_history (id, templateId, version, body, state, changedBy, changedAt, changeReason)
|
|
6394
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
6395
|
+
)
|
|
6396
|
+
.run(
|
|
6397
|
+
crypto.randomUUID(),
|
|
6398
|
+
existing.id,
|
|
6399
|
+
newVersion,
|
|
6400
|
+
data.body,
|
|
6401
|
+
state,
|
|
6402
|
+
changedBy,
|
|
6403
|
+
now,
|
|
6404
|
+
changeReason,
|
|
6405
|
+
);
|
|
6406
|
+
} else {
|
|
6407
|
+
const id = crypto.randomUUID();
|
|
6408
|
+
row = getDb()
|
|
6409
|
+
.prepare<
|
|
6410
|
+
PromptTemplateRow,
|
|
6411
|
+
[
|
|
6412
|
+
string,
|
|
6413
|
+
string,
|
|
6414
|
+
string,
|
|
6415
|
+
string | null,
|
|
6416
|
+
string,
|
|
6417
|
+
string,
|
|
6418
|
+
number,
|
|
6419
|
+
number,
|
|
6420
|
+
string | null,
|
|
6421
|
+
string,
|
|
6422
|
+
string,
|
|
6423
|
+
]
|
|
6424
|
+
>(
|
|
6425
|
+
`INSERT INTO prompt_templates (id, eventType, scope, scopeId, state, body, isDefault, version, createdBy, createdAt, updatedAt)
|
|
6426
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
6427
|
+
)
|
|
6428
|
+
.get(
|
|
6429
|
+
id,
|
|
6430
|
+
data.eventType,
|
|
6431
|
+
data.scope,
|
|
6432
|
+
scopeId,
|
|
6433
|
+
state,
|
|
6434
|
+
data.body,
|
|
6435
|
+
data.isDefault ? 1 : 0,
|
|
6436
|
+
1,
|
|
6437
|
+
createdBy,
|
|
6438
|
+
now,
|
|
6439
|
+
now,
|
|
6440
|
+
);
|
|
6441
|
+
|
|
6442
|
+
// Create history entry for the insert
|
|
6443
|
+
getDb()
|
|
6444
|
+
.prepare(
|
|
6445
|
+
`INSERT INTO prompt_template_history (id, templateId, version, body, state, changedBy, changedAt, changeReason)
|
|
6446
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
6447
|
+
)
|
|
6448
|
+
.run(
|
|
6449
|
+
crypto.randomUUID(),
|
|
6450
|
+
id,
|
|
6451
|
+
1,
|
|
6452
|
+
data.body,
|
|
6453
|
+
state,
|
|
6454
|
+
changedBy,
|
|
6455
|
+
now,
|
|
6456
|
+
changeReason ?? "Initial creation",
|
|
6457
|
+
);
|
|
6458
|
+
}
|
|
6459
|
+
|
|
6460
|
+
if (!row) throw new Error("Failed to upsert prompt template");
|
|
6461
|
+
return rowToPromptTemplate(row);
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6464
|
+
/**
|
|
6465
|
+
* Delete a prompt template by ID. Guards against deleting default templates.
|
|
6466
|
+
* Does NOT delete history rows (intentional for audit trail).
|
|
6467
|
+
*/
|
|
6468
|
+
export function deletePromptTemplate(id: string): boolean {
|
|
6469
|
+
const existing = getDb()
|
|
6470
|
+
.prepare<PromptTemplateRow, [string]>("SELECT * FROM prompt_templates WHERE id = ?")
|
|
6471
|
+
.get(id);
|
|
6472
|
+
|
|
6473
|
+
if (!existing) return false;
|
|
6474
|
+
if (existing.isDefault === 1) {
|
|
6475
|
+
throw new Error(
|
|
6476
|
+
"Cannot delete a default prompt template. Use resetPromptTemplateToDefault instead.",
|
|
6477
|
+
);
|
|
6478
|
+
}
|
|
6479
|
+
|
|
6480
|
+
const result = getDb().run("DELETE FROM prompt_templates WHERE id = ?", [id]);
|
|
6481
|
+
return result.changes > 0;
|
|
6482
|
+
}
|
|
6483
|
+
|
|
6484
|
+
/**
|
|
6485
|
+
* Reset a prompt template to its default state.
|
|
6486
|
+
* Sets body to defaultBody, isDefault=true, state='enabled', bumps version.
|
|
6487
|
+
*/
|
|
6488
|
+
export function resetPromptTemplateToDefault(id: string, defaultBody: string): PromptTemplate {
|
|
6489
|
+
const now = new Date().toISOString();
|
|
6490
|
+
const existing = getDb()
|
|
6491
|
+
.prepare<PromptTemplateRow, [string]>("SELECT * FROM prompt_templates WHERE id = ?")
|
|
6492
|
+
.get(id);
|
|
6493
|
+
|
|
6494
|
+
if (!existing) throw new Error(`Prompt template ${id} not found`);
|
|
6495
|
+
|
|
6496
|
+
const newVersion = existing.version + 1;
|
|
6497
|
+
|
|
6498
|
+
const row = getDb()
|
|
6499
|
+
.prepare<PromptTemplateRow, [string, number, string, string]>(
|
|
6500
|
+
`UPDATE prompt_templates SET body = ?, state = 'enabled', isDefault = 1, version = ?, updatedAt = ?
|
|
6501
|
+
WHERE id = ? RETURNING *`,
|
|
6502
|
+
)
|
|
6503
|
+
.get(defaultBody, newVersion, now, id);
|
|
6504
|
+
|
|
6505
|
+
if (!row) throw new Error("Failed to reset prompt template to default");
|
|
6506
|
+
|
|
6507
|
+
// Create history entry
|
|
6508
|
+
getDb()
|
|
6509
|
+
.prepare(
|
|
6510
|
+
`INSERT INTO prompt_template_history (id, templateId, version, body, state, changedBy, changedAt, changeReason)
|
|
6511
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
6512
|
+
)
|
|
6513
|
+
.run(
|
|
6514
|
+
crypto.randomUUID(),
|
|
6515
|
+
id,
|
|
6516
|
+
newVersion,
|
|
6517
|
+
defaultBody,
|
|
6518
|
+
"enabled",
|
|
6519
|
+
null,
|
|
6520
|
+
now,
|
|
6521
|
+
"Reset to default",
|
|
6522
|
+
);
|
|
6523
|
+
|
|
6524
|
+
return rowToPromptTemplate(row);
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6527
|
+
/**
|
|
6528
|
+
* Get version history for a prompt template, ordered by version DESC.
|
|
6529
|
+
*/
|
|
6530
|
+
export function getPromptTemplateHistory(templateId: string): PromptTemplateHistory[] {
|
|
6531
|
+
return getDb()
|
|
6532
|
+
.prepare<PromptTemplateHistoryRow, [string]>(
|
|
6533
|
+
"SELECT * FROM prompt_template_history WHERE templateId = ? ORDER BY version DESC",
|
|
6534
|
+
)
|
|
6535
|
+
.all(templateId)
|
|
6536
|
+
.map(rowToPromptTemplateHistory);
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6539
|
+
/**
|
|
6540
|
+
* Resolve the best prompt template for a given eventType using scope precedence.
|
|
6541
|
+
*
|
|
6542
|
+
* Two-pass resolution:
|
|
6543
|
+
* Pass 1 (exact match): Try exact eventType at agent → repo → global scope.
|
|
6544
|
+
* Pass 2 (wildcard): Generate wildcards from eventType (e.g. "github.pull_request.*", "github.*")
|
|
6545
|
+
* and try each at agent → repo → global scope.
|
|
6546
|
+
*
|
|
6547
|
+
* Exact match at ANY scope always beats wildcard at ANY scope.
|
|
6548
|
+
*
|
|
6549
|
+
* State behavior:
|
|
6550
|
+
* - 'enabled': return the template
|
|
6551
|
+
* - 'skip_event': return { skip: true }
|
|
6552
|
+
* - 'default_prompt_fallback': continue to next scope level
|
|
6553
|
+
*/
|
|
6554
|
+
export function resolvePromptTemplate(
|
|
6555
|
+
eventType: string,
|
|
6556
|
+
agentId?: string,
|
|
6557
|
+
repoId?: string,
|
|
6558
|
+
): { template: PromptTemplate } | { skip: true } | null {
|
|
6559
|
+
// Helper to look up a template at a specific scope
|
|
6560
|
+
const lookupAtScope = (
|
|
6561
|
+
et: string,
|
|
6562
|
+
scope: "global" | "agent" | "repo",
|
|
6563
|
+
scopeId: string | null,
|
|
6564
|
+
): PromptTemplateRow | undefined => {
|
|
6565
|
+
if (scopeId === null) {
|
|
6566
|
+
return (
|
|
6567
|
+
getDb()
|
|
6568
|
+
.prepare<PromptTemplateRow, [string, string]>(
|
|
6569
|
+
"SELECT * FROM prompt_templates WHERE eventType = ? AND scope = ? AND scopeId IS NULL",
|
|
6570
|
+
)
|
|
6571
|
+
.get(et, scope) ?? undefined
|
|
6572
|
+
);
|
|
6573
|
+
}
|
|
6574
|
+
return (
|
|
6575
|
+
getDb()
|
|
6576
|
+
.prepare<PromptTemplateRow, [string, string, string]>(
|
|
6577
|
+
"SELECT * FROM prompt_templates WHERE eventType = ? AND scope = ? AND scopeId = ?",
|
|
6578
|
+
)
|
|
6579
|
+
.get(et, scope, scopeId) ?? undefined
|
|
6580
|
+
);
|
|
6581
|
+
};
|
|
6582
|
+
|
|
6583
|
+
// Try resolution at the scope chain for a given eventType string
|
|
6584
|
+
const tryResolve = (et: string): { template: PromptTemplate } | { skip: true } | "continue" => {
|
|
6585
|
+
// Build scope chain: agent → repo → global
|
|
6586
|
+
const scopeChain: Array<{ scope: "global" | "agent" | "repo"; scopeId: string | null }> = [];
|
|
6587
|
+
if (agentId) scopeChain.push({ scope: "agent", scopeId: agentId });
|
|
6588
|
+
if (repoId) scopeChain.push({ scope: "repo", scopeId: repoId });
|
|
6589
|
+
scopeChain.push({ scope: "global", scopeId: null });
|
|
6590
|
+
|
|
6591
|
+
for (const { scope, scopeId } of scopeChain) {
|
|
6592
|
+
const row = lookupAtScope(et, scope, scopeId);
|
|
6593
|
+
if (!row) continue;
|
|
6594
|
+
|
|
6595
|
+
if (row.state === "enabled") {
|
|
6596
|
+
return { template: rowToPromptTemplate(row) };
|
|
6597
|
+
}
|
|
6598
|
+
if (row.state === "skip_event") {
|
|
6599
|
+
return { skip: true };
|
|
6600
|
+
}
|
|
6601
|
+
// default_prompt_fallback: continue to next scope
|
|
6602
|
+
}
|
|
6603
|
+
|
|
6604
|
+
return "continue";
|
|
6605
|
+
};
|
|
6606
|
+
|
|
6607
|
+
// Pass 1: exact match
|
|
6608
|
+
const exactResult = tryResolve(eventType);
|
|
6609
|
+
if (exactResult !== "continue") return exactResult;
|
|
6610
|
+
|
|
6611
|
+
// Pass 2: wildcard matching
|
|
6612
|
+
// e.g. "github.pull_request.review_submitted" → ["github.pull_request.*", "github.*"]
|
|
6613
|
+
const parts = eventType.split(".");
|
|
6614
|
+
const wildcards: string[] = [];
|
|
6615
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
6616
|
+
wildcards.push(`${parts.slice(0, i).join(".")}.*`);
|
|
6617
|
+
}
|
|
6618
|
+
|
|
6619
|
+
for (const wildcard of wildcards) {
|
|
6620
|
+
const wildcardResult = tryResolve(wildcard);
|
|
6621
|
+
if (wildcardResult !== "continue") return wildcardResult;
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
return null;
|
|
6625
|
+
}
|
|
6626
|
+
|
|
6627
|
+
/**
|
|
6628
|
+
* Checkout a prompt template to a specific version from history.
|
|
6629
|
+
* Copies body and state from the history entry into the live record, bumps version.
|
|
6630
|
+
*/
|
|
6631
|
+
export function checkoutPromptTemplate(id: string, targetVersion: number): PromptTemplate {
|
|
6632
|
+
const now = new Date().toISOString();
|
|
6633
|
+
|
|
6634
|
+
const existing = getDb()
|
|
6635
|
+
.prepare<PromptTemplateRow, [string]>("SELECT * FROM prompt_templates WHERE id = ?")
|
|
6636
|
+
.get(id);
|
|
6637
|
+
if (!existing) throw new Error(`Prompt template ${id} not found`);
|
|
6638
|
+
|
|
6639
|
+
const historyEntry = getDb()
|
|
6640
|
+
.prepare<PromptTemplateHistoryRow, [string, number]>(
|
|
6641
|
+
"SELECT * FROM prompt_template_history WHERE templateId = ? AND version = ?",
|
|
6642
|
+
)
|
|
6643
|
+
.get(id, targetVersion);
|
|
6644
|
+
if (!historyEntry)
|
|
6645
|
+
throw new Error(`No history entry at version ${targetVersion} for template ${id}`);
|
|
6646
|
+
|
|
6647
|
+
const newVersion = existing.version + 1;
|
|
6648
|
+
|
|
6649
|
+
const row = getDb()
|
|
6650
|
+
.prepare<PromptTemplateRow, [string, string, number, string, string]>(
|
|
6651
|
+
`UPDATE prompt_templates SET body = ?, state = ?, version = ?, updatedAt = ?
|
|
6652
|
+
WHERE id = ? RETURNING *`,
|
|
6653
|
+
)
|
|
6654
|
+
.get(historyEntry.body, historyEntry.state, newVersion, now, id);
|
|
6655
|
+
|
|
6656
|
+
if (!row) throw new Error("Failed to checkout prompt template");
|
|
6657
|
+
|
|
6658
|
+
// Create history entry for the checkout
|
|
6659
|
+
getDb()
|
|
6660
|
+
.prepare(
|
|
6661
|
+
`INSERT INTO prompt_template_history (id, templateId, version, body, state, changedBy, changedAt, changeReason)
|
|
6662
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
6663
|
+
)
|
|
6664
|
+
.run(
|
|
6665
|
+
crypto.randomUUID(),
|
|
6666
|
+
id,
|
|
6667
|
+
newVersion,
|
|
6668
|
+
historyEntry.body,
|
|
6669
|
+
historyEntry.state,
|
|
6670
|
+
null,
|
|
6671
|
+
now,
|
|
6672
|
+
`Checked out from version ${targetVersion}`,
|
|
6673
|
+
);
|
|
6674
|
+
|
|
6675
|
+
return rowToPromptTemplate(row);
|
|
6676
|
+
}
|
|
6677
|
+
|
|
6678
|
+
// ─── Channel Activity Cursors ─────────────────────────────────────────────────
|
|
6679
|
+
|
|
6680
|
+
type ChannelActivityCursorRow = {
|
|
6681
|
+
channelId: string;
|
|
6682
|
+
lastSeenTs: string;
|
|
6683
|
+
updatedAt: string;
|
|
6684
|
+
};
|
|
6685
|
+
|
|
6686
|
+
export interface ChannelActivityCursor {
|
|
6687
|
+
channelId: string;
|
|
6688
|
+
lastSeenTs: string;
|
|
6689
|
+
updatedAt: string;
|
|
6690
|
+
}
|
|
6691
|
+
|
|
6692
|
+
function rowToChannelActivityCursor(row: ChannelActivityCursorRow): ChannelActivityCursor {
|
|
6693
|
+
return {
|
|
6694
|
+
channelId: row.channelId,
|
|
6695
|
+
lastSeenTs: row.lastSeenTs,
|
|
6696
|
+
updatedAt: row.updatedAt,
|
|
6697
|
+
};
|
|
6698
|
+
}
|
|
6699
|
+
|
|
6700
|
+
export function getAllChannelActivityCursors(): ChannelActivityCursor[] {
|
|
6701
|
+
return getDb()
|
|
6702
|
+
.prepare<ChannelActivityCursorRow, []>("SELECT * FROM channel_activity_cursors")
|
|
6703
|
+
.all()
|
|
6704
|
+
.map(rowToChannelActivityCursor);
|
|
6705
|
+
}
|
|
6706
|
+
|
|
6707
|
+
export function getChannelActivityCursor(channelId: string): ChannelActivityCursor | null {
|
|
6708
|
+
const row = getDb()
|
|
6709
|
+
.prepare<ChannelActivityCursorRow, [string]>(
|
|
6710
|
+
"SELECT * FROM channel_activity_cursors WHERE channelId = ?",
|
|
6711
|
+
)
|
|
6712
|
+
.get(channelId);
|
|
6713
|
+
return row ? rowToChannelActivityCursor(row) : null;
|
|
6714
|
+
}
|
|
6715
|
+
|
|
6716
|
+
export function upsertChannelActivityCursor(channelId: string, lastSeenTs: string): void {
|
|
6717
|
+
getDb()
|
|
6718
|
+
.prepare(
|
|
6719
|
+
`INSERT INTO channel_activity_cursors (channelId, lastSeenTs, updatedAt)
|
|
6720
|
+
VALUES (?, ?, datetime('now'))
|
|
6721
|
+
ON CONFLICT(channelId) DO UPDATE SET lastSeenTs = excluded.lastSeenTs, updatedAt = excluded.updatedAt`,
|
|
6722
|
+
)
|
|
6723
|
+
.run(channelId, lastSeenTs);
|
|
6724
|
+
}
|