@desplega.ai/agent-swarm 1.93.0 → 1.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +2 -2
  2. package/openapi.json +180 -1
  3. package/package.json +1 -1
  4. package/src/be/db.ts +63 -7
  5. package/src/be/migrations/090_model_tiers.sql +2 -0
  6. package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
  7. package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
  8. package/src/be/migrations/093_slack_message_tracking.sql +6 -0
  9. package/src/be/migrations/runner.ts +52 -0
  10. package/src/be/modelsdev-cache.json +2060 -198
  11. package/src/be/scripts/boot-reembed.ts +74 -0
  12. package/src/be/scripts/db.ts +19 -3
  13. package/src/be/seed/index.ts +1 -1
  14. package/src/be/seed/registry.ts +2 -2
  15. package/src/be/seed/runner.ts +5 -5
  16. package/src/be/seed/types.ts +6 -1
  17. package/src/be/seed-pricing.ts +1 -0
  18. package/src/be/seed-scripts/index.ts +3 -2
  19. package/src/commands/runner.ts +83 -13
  20. package/src/http/index.ts +13 -2
  21. package/src/http/metrics.ts +55 -6
  22. package/src/http/schedules.ts +16 -15
  23. package/src/http/script-runs.ts +7 -1
  24. package/src/http/scripts.ts +147 -1
  25. package/src/http/tasks.ts +7 -0
  26. package/src/model-tiers.ts +140 -0
  27. package/src/providers/claude-managed-models.ts +9 -0
  28. package/src/providers/opencode-adapter.ts +1 -0
  29. package/src/providers/pi-mono-adapter.ts +78 -6
  30. package/src/scheduler/scheduler.ts +22 -34
  31. package/src/server-user.ts +8 -2
  32. package/src/slack/responses.ts +39 -11
  33. package/src/slack/watcher.ts +121 -8
  34. package/src/tests/agents-list-model-display.test.ts +13 -0
  35. package/src/tests/aws-error-classifier.test.ts +148 -0
  36. package/src/tests/claude-managed-adapter.test.ts +12 -0
  37. package/src/tests/context-window.test.ts +7 -0
  38. package/src/tests/http-api-integration.test.ts +19 -0
  39. package/src/tests/metrics-http.test.ts +137 -3
  40. package/src/tests/migration-046-budgets.test.ts +33 -0
  41. package/src/tests/migration-runner-regressions.test.ts +69 -0
  42. package/src/tests/model-control.test.ts +162 -46
  43. package/src/tests/opencode-adapter.test.ts +9 -0
  44. package/src/tests/pi-mono-adapter.test.ts +319 -0
  45. package/src/tests/providers/pi-cost.test.ts +9 -0
  46. package/src/tests/runner-fallback-output.test.ts +50 -0
  47. package/src/tests/scripts-boot-reembed.test.ts +163 -0
  48. package/src/tests/scripts-embeddings.test.ts +90 -0
  49. package/src/tests/seed.test.ts +26 -1
  50. package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
  51. package/src/tests/slack-watcher.test.ts +66 -0
  52. package/src/tests/workflow-agent-task.test.ts +5 -2
  53. package/src/tests/workflow-validation-port-routing.test.ts +181 -0
  54. package/src/tools/memory-get.ts +11 -0
  55. package/src/tools/memory-search.ts +18 -0
  56. package/src/tools/schedules/create-schedule.ts +71 -70
  57. package/src/tools/schedules/update-schedule.ts +43 -31
  58. package/src/tools/send-task.ts +16 -5
  59. package/src/tools/task-action.ts +11 -3
  60. package/src/types.ts +29 -0
  61. package/src/utils/aws-error-classifier.ts +97 -0
  62. package/src/utils/context-window.ts +2 -0
  63. package/src/utils/credentials.test.ts +68 -0
  64. package/src/utils/credentials.ts +44 -3
  65. package/src/utils/pretty-print.ts +25 -10
  66. package/src/workflows/engine.ts +3 -2
  67. package/src/workflows/executors/agent-task.ts +3 -1
package/README.md CHANGED
@@ -46,7 +46,7 @@
46
46
 
47
47
  ## What it does
48
48
 
49
- Agent Swarm runs a team of AI agents that coordinate autonomously. A **lead agent** receives tasks ( from Slack, GitHub, GitLab, Linear, Jira, email, or the API) breaks them down, and delegates to **worker agents** running in isolated environments (Docker). Workers execute tasks, ship solutions, and write their learnings back to a shared memory so the whole swarm gets smarter every session.
49
+ Agent Swarm runs a team of AI agents that coordinate autonomously. A **lead agent** receives tasks (from Slack, GitHub, GitLab, Linear, Jira, email, or the API), breaks them down, and delegates to **worker agents** running in isolated environments (Docker). Workers execute tasks, ship solutions, and write their learnings back to a shared memory so the whole swarm gets smarter every session.
50
50
 
51
51
  You can run agents for Marketing, Product, UX, Engineering, Support, Operations, HR, Finance, or any role you can think of. A centralized Lead coordinates them, and they share the learnings horizontally. That's the true difference between [*AI First*](https://www.pleasedontdeploy.com/i/197193364/ai-first) and [*AI Native*](https://www.pleasedontdeploy.com/i/197193364/third-the-ai-native-metamorphosis).
52
52
 
@@ -127,7 +127,7 @@ Check [our templates](https://templates.agent-swarm.dev) for a quick start.
127
127
  - **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
128
128
  - **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
129
129
  - **Durable script workflows** — launch background script runs, inspect their journals, and track them from the dashboard when a one-shot `script-run` is too small. [Guide →](https://docs.agent-swarm.dev/docs/guides/script-workflow-runs)
130
- - **Harness & LLM agnostic** — run with Claude Code, Claude Bridge, OpenAI Codex, pi-mono, Devin, Claude Managed Agents, raw LLMs, or opencode. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
130
+ - **Harness & LLM agnostic** — run with Claude Code, Claude Bridge, OpenAI Codex, pi-mono, Devin, Claude Managed Agents, raw LLMs, or opencode. Tasks, schedules, and workflow agent-task nodes can use portable `modelTier` intent (`smol`, `regular`, `smart`, `ultra`) and resolve it per worker/provider at run time. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
131
131
  - **Follow-up continuity across all harnesses** — child tasks inherit a bounded prior-task context preamble built from the task chain, so continuity survives restarts and works the same across every provider. [Task lifecycle →](https://docs.agent-swarm.dev/docs/concepts/task-lifecycle)
132
132
  - **Skills & MCP servers** — reusable procedural knowledge, bundled skill reference files, and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
133
133
  - **External tool-router access** — the `x` command and `swarm_x` MCP tool let humans and agents execute approved third-party routes such as Composio without baking bespoke MCP servers first. [CLI →](https://docs.agent-swarm.dev/docs/reference/cli) · [Composio →](https://docs.agent-swarm.dev/docs/integrations/composio)
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.93.0",
5
+ "version": "1.94.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
@@ -6894,6 +6894,15 @@
6894
6894
  "model": {
6895
6895
  "type": "string"
6896
6896
  },
6897
+ "modelTier": {
6898
+ "type": "string",
6899
+ "enum": [
6900
+ "smol",
6901
+ "regular",
6902
+ "smart",
6903
+ "ultra"
6904
+ ]
6905
+ },
6897
6906
  "scheduleType": {
6898
6907
  "type": "string",
6899
6908
  "enum": [
@@ -7141,6 +7150,19 @@
7141
7150
  "model": {
7142
7151
  "type": "string"
7143
7152
  },
7153
+ "modelTier": {
7154
+ "type": [
7155
+ "string",
7156
+ "null"
7157
+ ],
7158
+ "enum": [
7159
+ "smol",
7160
+ "regular",
7161
+ "smart",
7162
+ "ultra",
7163
+ null
7164
+ ]
7165
+ },
7144
7166
  "nextRunAt": {
7145
7167
  "type": [
7146
7168
  "string",
@@ -7297,6 +7319,14 @@
7297
7319
  "name": "agentId",
7298
7320
  "in": "query"
7299
7321
  },
7322
+ {
7323
+ "schema": {
7324
+ "type": "string"
7325
+ },
7326
+ "required": false,
7327
+ "name": "scriptName",
7328
+ "in": "query"
7329
+ },
7300
7330
  {
7301
7331
  "schema": {
7302
7332
  "type": "integer",
@@ -9249,6 +9279,143 @@
9249
9279
  }
9250
9280
  }
9251
9281
  },
9282
+ "/api/scripts": {
9283
+ "get": {
9284
+ "operationId": "scripts_list",
9285
+ "summary": "List saved scripts",
9286
+ "description": "Dashboard read: lean projection without source. Scratch scripts are excluded unless includeScratch=true.",
9287
+ "tags": [
9288
+ "Scripts"
9289
+ ],
9290
+ "security": [
9291
+ {
9292
+ "bearerAuth": []
9293
+ }
9294
+ ],
9295
+ "parameters": [
9296
+ {
9297
+ "schema": {
9298
+ "type": "string",
9299
+ "enum": [
9300
+ "global",
9301
+ "agent"
9302
+ ]
9303
+ },
9304
+ "required": false,
9305
+ "name": "scope",
9306
+ "in": "query"
9307
+ },
9308
+ {
9309
+ "schema": {
9310
+ "type": "string",
9311
+ "enum": [
9312
+ "true",
9313
+ "false"
9314
+ ]
9315
+ },
9316
+ "required": false,
9317
+ "name": "includeScratch",
9318
+ "in": "query"
9319
+ }
9320
+ ],
9321
+ "responses": {
9322
+ "200": {
9323
+ "description": "Saved scripts"
9324
+ },
9325
+ "400": {
9326
+ "description": "Validation error"
9327
+ }
9328
+ }
9329
+ }
9330
+ },
9331
+ "/api/scripts/type-defs": {
9332
+ "get": {
9333
+ "operationId": "scripts_type_defs",
9334
+ "summary": "Get script SDK and stdlib type definitions",
9335
+ "description": "Static .d.ts blobs for editor integration (e.g. Monaco extraLibs). Cacheable.",
9336
+ "tags": [
9337
+ "Scripts"
9338
+ ],
9339
+ "security": [
9340
+ {
9341
+ "bearerAuth": []
9342
+ }
9343
+ ],
9344
+ "responses": {
9345
+ "200": {
9346
+ "description": "SDK and stdlib type definition blobs"
9347
+ }
9348
+ }
9349
+ }
9350
+ },
9351
+ "/api/scripts/{id}": {
9352
+ "get": {
9353
+ "operationId": "scripts_get",
9354
+ "summary": "Get a saved script by id",
9355
+ "description": "Dashboard read: full record including source and parsed signature.",
9356
+ "tags": [
9357
+ "Scripts"
9358
+ ],
9359
+ "security": [
9360
+ {
9361
+ "bearerAuth": []
9362
+ }
9363
+ ],
9364
+ "parameters": [
9365
+ {
9366
+ "schema": {
9367
+ "type": "string",
9368
+ "format": "uuid"
9369
+ },
9370
+ "required": true,
9371
+ "name": "id",
9372
+ "in": "path"
9373
+ }
9374
+ ],
9375
+ "responses": {
9376
+ "200": {
9377
+ "description": "Script detail"
9378
+ },
9379
+ "404": {
9380
+ "description": "Script not found"
9381
+ }
9382
+ }
9383
+ }
9384
+ },
9385
+ "/api/scripts/{id}/versions": {
9386
+ "get": {
9387
+ "operationId": "scripts_versions",
9388
+ "summary": "List versions of a saved script",
9389
+ "description": "Dashboard read: version history, newest first.",
9390
+ "tags": [
9391
+ "Scripts"
9392
+ ],
9393
+ "security": [
9394
+ {
9395
+ "bearerAuth": []
9396
+ }
9397
+ ],
9398
+ "parameters": [
9399
+ {
9400
+ "schema": {
9401
+ "type": "string",
9402
+ "format": "uuid"
9403
+ },
9404
+ "required": true,
9405
+ "name": "id",
9406
+ "in": "path"
9407
+ }
9408
+ ],
9409
+ "responses": {
9410
+ "200": {
9411
+ "description": "Script versions"
9412
+ },
9413
+ "404": {
9414
+ "description": "Script not found"
9415
+ }
9416
+ }
9417
+ }
9418
+ },
9252
9419
  "/api/mcp-bridge": {
9253
9420
  "post": {
9254
9421
  "summary": "Generic MCP tool proxy for the scripts SDK bridge",
@@ -10745,6 +10912,18 @@
10745
10912
  },
10746
10913
  "requestedByUserId": {
10747
10914
  "type": "string"
10915
+ },
10916
+ "model": {
10917
+ "type": "string"
10918
+ },
10919
+ "modelTier": {
10920
+ "type": "string",
10921
+ "enum": [
10922
+ "smol",
10923
+ "regular",
10924
+ "smart",
10925
+ "ultra"
10926
+ ]
10748
10927
  }
10749
10928
  },
10750
10929
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.93.0",
3
+ "version": "1.94.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
package/src/be/db.ts CHANGED
@@ -2,6 +2,7 @@ import { Database } from "bun:sqlite";
2
2
  import { parseProviderMeta } from "@/utils/provider-metadata.ts";
3
3
  import pkg from "../../package.json";
4
4
  import { addEyesReactionOnTaskStart } from "../github/task-reactions";
5
+ import { type ModelTier, parseModelTier } from "../model-tiers";
5
6
  import { configureDbResolver } from "../prompts/resolver";
6
7
  import type {
7
8
  ActiveSession,
@@ -1000,6 +1001,8 @@ type AgentTaskRow = {
1000
1001
  slackThreadTs: string | null;
1001
1002
  slackUserId: string | null;
1002
1003
  slackReplySent: number;
1004
+ slackProgressMessageTs: string | null;
1005
+ slackTreeRootMessageTs: string | null;
1003
1006
  vcsProvider: string | null;
1004
1007
  vcsRepo: string | null;
1005
1008
  vcsEventType: string | null;
@@ -1018,6 +1021,7 @@ type AgentTaskRow = {
1018
1021
  parentTaskId: string | null;
1019
1022
  claudeSessionId: string | null;
1020
1023
  model: string | null;
1024
+ modelTier: string | null;
1021
1025
  scheduleId: string | null;
1022
1026
  workflowRunId: string | null;
1023
1027
  workflowRunStepId: string | null;
@@ -1088,6 +1092,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
1088
1092
  slackThreadTs: row.slackThreadTs ?? undefined,
1089
1093
  slackUserId: row.slackUserId ?? undefined,
1090
1094
  slackReplySent: !!row.slackReplySent,
1095
+ slackProgressMessageTs: row.slackProgressMessageTs ?? undefined,
1096
+ slackTreeRootMessageTs: row.slackTreeRootMessageTs ?? undefined,
1091
1097
  vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
1092
1098
  vcsRepo: row.vcsRepo ?? undefined,
1093
1099
  vcsEventType: row.vcsEventType ?? undefined,
@@ -1105,7 +1111,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
1105
1111
  dir: row.dir ?? undefined,
1106
1112
  parentTaskId: row.parentTaskId ?? undefined,
1107
1113
  claudeSessionId: row.claudeSessionId ?? undefined,
1108
- model: (row.model as "haiku" | "sonnet" | "opus" | "fable" | null) ?? undefined,
1114
+ model: row.model ?? undefined,
1115
+ modelTier: parseModelTier(row.modelTier) ?? undefined,
1109
1116
  scheduleId: row.scheduleId ?? undefined,
1110
1117
  workflowRunId: row.workflowRunId ?? undefined,
1111
1118
  workflowRunStepId: row.workflowRunStepId ?? undefined,
@@ -1161,6 +1168,7 @@ function rowToAgentTaskSummary(row: AgentTaskRow): AgentTaskSummary {
1161
1168
  parentTaskId: t.parentTaskId,
1162
1169
  scheduleId: t.scheduleId,
1163
1170
  model: t.model,
1171
+ modelTier: t.modelTier,
1164
1172
  provider: t.provider,
1165
1173
  requestedByUserId: t.requestedByUserId,
1166
1174
  progress: t.progress,
@@ -1363,6 +1371,30 @@ export function markTaskSlackReplySent(taskId: string): void {
1363
1371
  getDb().run(`UPDATE agent_tasks SET slackReplySent = 1 WHERE id = ?`, [taskId]);
1364
1372
  }
1365
1373
 
1374
+ export function setSlackMessageTracking(
1375
+ taskId: string,
1376
+ fields: {
1377
+ slackProgressMessageTs?: string | null;
1378
+ slackTreeRootMessageTs?: string | null;
1379
+ },
1380
+ ): void {
1381
+ const sets: string[] = [];
1382
+ const args: (string | null)[] = [];
1383
+
1384
+ if (Object.hasOwn(fields, "slackProgressMessageTs")) {
1385
+ sets.push("slackProgressMessageTs = ?");
1386
+ args.push(fields.slackProgressMessageTs ?? null);
1387
+ }
1388
+ if (Object.hasOwn(fields, "slackTreeRootMessageTs")) {
1389
+ sets.push("slackTreeRootMessageTs = ?");
1390
+ args.push(fields.slackTreeRootMessageTs ?? null);
1391
+ }
1392
+ if (sets.length === 0) return;
1393
+
1394
+ args.push(taskId);
1395
+ getDb().run(`UPDATE agent_tasks SET ${sets.join(", ")} WHERE id = ?`, args);
1396
+ }
1397
+
1366
1398
  export function getChildTasks(parentTaskId: string): AgentTask[] {
1367
1399
  return getDb()
1368
1400
  .prepare<AgentTaskRow, [string]>(
@@ -2850,6 +2882,7 @@ export interface CreateTaskOptions {
2850
2882
  dir?: string;
2851
2883
  parentTaskId?: string;
2852
2884
  model?: string;
2885
+ modelTier?: ModelTier;
2853
2886
  scheduleId?: string;
2854
2887
  workflowRunId?: string;
2855
2888
  workflowRunStepId?: string;
@@ -3056,9 +3089,9 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
3056
3089
  vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
3057
3090
  vcsInstallationId, vcsNodeId,
3058
3091
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
3059
- mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
3092
+ mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, scheduleId,
3060
3093
  workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
3061
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3094
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3062
3095
  )
3063
3096
  .get(
3064
3097
  id,
@@ -3093,6 +3126,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
3093
3126
  options?.dir ?? null,
3094
3127
  options?.parentTaskId ?? null,
3095
3128
  options?.model ?? null,
3129
+ options?.modelTier ?? null,
3096
3130
  options?.scheduleId ?? null,
3097
3131
  options?.workflowRunId ?? null,
3098
3132
  options?.workflowRunStepId ?? null,
@@ -5266,6 +5300,7 @@ type ScheduledTaskRow = {
5266
5300
  lastErrorAt: string | null;
5267
5301
  lastErrorMessage: string | null;
5268
5302
  model: string | null;
5303
+ modelTier: string | null;
5269
5304
  scheduleType: string;
5270
5305
  createdAt: string;
5271
5306
  lastUpdatedAt: string;
@@ -5306,7 +5341,8 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
5306
5341
  consecutiveErrors: row.consecutiveErrors ?? 0,
5307
5342
  lastErrorAt: normalizeDate(row.lastErrorAt) ?? undefined,
5308
5343
  lastErrorMessage: row.lastErrorMessage ?? undefined,
5309
- model: (row.model as "haiku" | "sonnet" | "opus" | "fable" | null) ?? undefined,
5344
+ model: row.model ?? undefined,
5345
+ modelTier: parseModelTier(row.modelTier) ?? undefined,
5310
5346
  scheduleType: row.scheduleType as "recurring" | "one_time",
5311
5347
  createdAt: normalizeDateRequired(row.createdAt),
5312
5348
  lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
@@ -5401,6 +5437,7 @@ export interface CreateScheduledTaskData {
5401
5437
  createdByAgentId?: string;
5402
5438
  timezone?: string;
5403
5439
  model?: string;
5440
+ modelTier?: ModelTier;
5404
5441
  scheduleType?: "recurring" | "one_time";
5405
5442
  }
5406
5443
 
@@ -5413,8 +5450,8 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
5413
5450
  `INSERT INTO scheduled_tasks (
5414
5451
  id, name, description, cronExpression, intervalMs, taskTemplate,
5415
5452
  taskType, tags, priority, targetAgentId, enabled, nextRunAt,
5416
- createdByAgentId, timezone, model, scheduleType, createdAt, lastUpdatedAt
5417
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5453
+ createdByAgentId, timezone, model, modelTier, scheduleType, createdAt, lastUpdatedAt
5454
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5418
5455
  )
5419
5456
  .get(
5420
5457
  id,
@@ -5432,6 +5469,7 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
5432
5469
  data.createdByAgentId ?? null,
5433
5470
  data.timezone ?? "UTC",
5434
5471
  data.model ?? null,
5472
+ data.modelTier ?? null,
5435
5473
  data.scheduleType ?? "recurring",
5436
5474
  now,
5437
5475
  now,
@@ -5459,6 +5497,7 @@ export interface UpdateScheduledTaskData {
5459
5497
  lastErrorAt?: string | null;
5460
5498
  lastErrorMessage?: string | null;
5461
5499
  model?: string | null;
5500
+ modelTier?: ModelTier | null;
5462
5501
  scheduleType?: "recurring" | "one_time";
5463
5502
  lastUpdatedAt?: string;
5464
5503
  }
@@ -5538,6 +5577,10 @@ export function updateScheduledTask(
5538
5577
  updates.push("model = ?");
5539
5578
  params.push(data.model);
5540
5579
  }
5580
+ if (data.modelTier !== undefined) {
5581
+ updates.push("modelTier = ?");
5582
+ params.push(data.modelTier);
5583
+ }
5541
5584
  if (data.scheduleType !== undefined) {
5542
5585
  updates.push("scheduleType = ?");
5543
5586
  params.push(data.scheduleType);
@@ -11801,6 +11844,7 @@ export function getScriptRunByIdempotencyKey(idempotencyKey: string): ScriptRun
11801
11844
  export function listScriptRuns(opts?: {
11802
11845
  status?: ScriptRunStatus;
11803
11846
  agentId?: string;
11847
+ scriptName?: string;
11804
11848
  limit?: number;
11805
11849
  offset?: number;
11806
11850
  }): ScriptRunListItem[] {
@@ -11814,6 +11858,10 @@ export function listScriptRuns(opts?: {
11814
11858
  conditions.push("agentId = ?");
11815
11859
  params.push(opts.agentId);
11816
11860
  }
11861
+ if (opts?.scriptName) {
11862
+ conditions.push("scriptName = ?");
11863
+ params.push(opts.scriptName);
11864
+ }
11817
11865
 
11818
11866
  const limit = opts?.limit ?? 50;
11819
11867
  const offset = opts?.offset ?? 0;
@@ -11842,7 +11890,11 @@ export function listScriptRuns(opts?: {
11842
11890
  return rows.map(rowToScriptRunListItem);
11843
11891
  }
11844
11892
 
11845
- export function countScriptRuns(opts?: { status?: ScriptRunStatus; agentId?: string }): number {
11893
+ export function countScriptRuns(opts?: {
11894
+ status?: ScriptRunStatus;
11895
+ agentId?: string;
11896
+ scriptName?: string;
11897
+ }): number {
11846
11898
  const conditions: string[] = [];
11847
11899
  const params: string[] = [];
11848
11900
  if (opts?.status) {
@@ -11853,6 +11905,10 @@ export function countScriptRuns(opts?: { status?: ScriptRunStatus; agentId?: str
11853
11905
  conditions.push("agentId = ?");
11854
11906
  params.push(opts.agentId);
11855
11907
  }
11908
+ if (opts?.scriptName) {
11909
+ conditions.push("scriptName = ?");
11910
+ params.push(opts.scriptName);
11911
+ }
11856
11912
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
11857
11913
  const row = getDb()
11858
11914
  .prepare<{ count: number }, string[]>(`SELECT COUNT(*) AS count FROM script_runs ${where}`)
@@ -0,0 +1,2 @@
1
+ ALTER TABLE agent_tasks ADD COLUMN modelTier TEXT;
2
+ ALTER TABLE scheduled_tasks ADD COLUMN modelTier TEXT;
@@ -0,0 +1,12 @@
1
+ -- Keep the seeded system dashboard aligned with the production swarm dashboard.
2
+ -- Migration 081 created the starter row; this forward migration updates both
3
+ -- fresh installs and existing databases without rewriting the applied seed.
4
+
5
+ UPDATE metrics
6
+ SET
7
+ title = 'Swarm operations overview',
8
+ description = 'A starter dashboard mixing raw SQL widgets with chart and table visualizations.',
9
+ definition = '{"version":1,"widgets":[{"id":"tasks-created-per-day","title":"Tasks created per day","description":"Daily task volume for the selected time range.","query":{"sql":"SELECT date(createdAt) AS day, COUNT(*) AS tasks FROM agent_tasks WHERE createdAt >= datetime(''now'', ?) GROUP BY day ORDER BY day","params":["{{rangeModifier}}"],"maxRows":100},"viz":{"type":"line","x":"day","y":"tasks","columns":[{"key":"day","label":"Day"},{"key":"tasks","label":"Tasks","format":"integer"}],"format":"integer"}},{"id":"usage-by-user","title":"Usage by user","description":"Tasks requested and session cost by user for the selected time range, requester filter, and agent filter.","query":{"sql":"SELECT COALESCE(u.name, ''Unassigned'') AS user, COUNT(DISTINCT t.id) AS tasks, ROUND(COALESCE(SUM(sc.totalCostUsd), 0), 4) AS cost_usd FROM agent_tasks t LEFT JOIN users u ON u.id = t.requestedByUserId LEFT JOIN session_costs sc ON sc.taskId = t.id WHERE t.createdAt >= datetime(''now'', ?) AND (? = '''' OR COALESCE(u.id, '''') = ? OR COALESCE(u.name, '''') LIKE ''%'' || ? || ''%'') AND (? = '''' OR COALESCE(t.agentId, '''') = ?) GROUP BY COALESCE(u.name, ''Unassigned'') ORDER BY cost_usd DESC, tasks DESC","params":["{{rangeModifier}}","{{userFilter}}","{{userFilter}}","{{userFilter}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"bar","x":"user","y":"tasks","columns":[{"key":"user","label":"User"},{"key":"tasks","label":"Tasks","format":"integer"},{"key":"cost_usd","label":"Cost","format":"currency"}],"format":"integer"}},{"id":"usage-by-model","title":"Usage by model","description":"Tasks, sessions, tokens, and total cost by model for the selected time range and agent filter.","query":{"sql":"SELECT model, COUNT(DISTINCT taskId) AS tasks, COUNT(*) AS sessions, SUM(inputTokens + outputTokens) AS tokens, ROUND(SUM(totalCostUsd), 2) AS cost_usd FROM session_costs WHERE createdAt >= datetime(''now'', ?) AND (? = '''' OR agentId = ?) GROUP BY model ORDER BY cost_usd DESC","params":["{{rangeModifier}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"bar","x":"model","y":"cost_usd","columns":[{"key":"model","label":"Model"},{"key":"tasks","label":"Tasks","format":"integer"},{"key":"sessions","label":"Sessions","format":"integer"},{"key":"tokens","label":"Tokens","format":"integer"},{"key":"cost_usd","label":"Cost","format":"currency"}],"format":"currency"}},{"id":"avg-cost-per-task-by-model","title":"Avg cost per task by model","description":"Average session cost per task, grouped by model, for the selected time range and agent filter.","query":{"sql":"SELECT model, COUNT(DISTINCT taskId) AS tasks, ROUND(SUM(totalCostUsd) * 1.0 / NULLIF(COUNT(DISTINCT taskId), 0), 4) AS avg_cost_per_task FROM session_costs WHERE createdAt >= datetime(''now'', ?) AND (? = '''' OR agentId = ?) GROUP BY model ORDER BY avg_cost_per_task DESC","params":["{{rangeModifier}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"bar","x":"model","y":"avg_cost_per_task","columns":[{"key":"model","label":"Model"},{"key":"tasks","label":"Tasks","format":"integer"},{"key":"avg_cost_per_task","label":"Avg cost / task","format":"currency"}],"format":"currency"}},{"id":"avg-task-time-by-model","title":"Avg task time by model","description":"Average wall-clock task duration (minutes) by model for finished tasks in the selected time range and agent filter.","query":{"sql":"SELECT model, COUNT(*) AS tasks, ROUND(AVG((julianday(finishedAt) - julianday(createdAt)) * 1440), 1) AS avg_minutes FROM agent_tasks WHERE finishedAt IS NOT NULL AND model IS NOT NULL AND createdAt >= datetime(''now'', ?) AND (? = '''' OR agentId = ?) GROUP BY model ORDER BY tasks DESC","params":["{{rangeModifier}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"bar","x":"model","y":"avg_minutes","columns":[{"key":"model","label":"Model"},{"key":"tasks","label":"Tasks","format":"integer"},{"key":"avg_minutes","label":"Avg time","format":"duration"}],"format":"duration"}},{"id":"cost-per-minute-by-model","title":"Cost per minute by model","description":"Total session cost divided by active session minutes, grouped by model. Active minutes = sum of session durations.","query":{"sql":"SELECT model, ROUND(SUM(durationMs) / 60000.0, 1) AS active_minutes, ROUND(SUM(totalCostUsd), 2) AS cost_usd, ROUND(SUM(totalCostUsd) / NULLIF(SUM(durationMs) / 60000.0, 0), 4) AS cost_per_minute FROM session_costs WHERE createdAt >= datetime(''now'', ?) AND (? = '''' OR agentId = ?) AND durationMs > 0 GROUP BY model ORDER BY cost_per_minute DESC","params":["{{rangeModifier}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"bar","x":"model","y":"cost_per_minute","columns":[{"key":"model","label":"Model"},{"key":"active_minutes","label":"Active minutes","format":"number"},{"key":"cost_usd","label":"Total cost","format":"currency"},{"key":"cost_per_minute","label":"Cost / min","format":"currency"}],"format":"currency"}},{"id":"cost-per-minute-by-agent","title":"Cost per minute by agent","description":"Total session cost divided by active session minutes, grouped by agent.","query":{"sql":"SELECT COALESCE(a.name, sc.agentId, ''unknown'') AS agent, ROUND(SUM(sc.durationMs) / 60000.0, 1) AS active_minutes, ROUND(SUM(sc.totalCostUsd), 2) AS cost_usd, ROUND(SUM(sc.totalCostUsd) / NULLIF(SUM(sc.durationMs) / 60000.0, 0), 4) AS cost_per_minute FROM session_costs sc LEFT JOIN agents a ON a.id = sc.agentId WHERE sc.createdAt >= datetime(''now'', ?) AND (? = '''' OR sc.agentId = ?) AND sc.durationMs > 0 GROUP BY agent ORDER BY cost_per_minute DESC","params":["{{rangeModifier}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"bar","x":"agent","y":"cost_per_minute","columns":[{"key":"agent","label":"Agent"},{"key":"active_minutes","label":"Active minutes","format":"number"},{"key":"cost_usd","label":"Total cost","format":"currency"},{"key":"cost_per_minute","label":"Cost / min","format":"currency"}],"format":"currency"}},{"id":"agent-performance","title":"Tasks, avg time & cost by agent","description":"Finished tasks per agent with average wall-clock duration and average cost per task for the selected time range.","query":{"sql":"SELECT COALESCE(a.name, t.agentId, ''unassigned'') AS agent, COUNT(*) AS tasks, ROUND(AVG((julianday(t.finishedAt) - julianday(t.createdAt)) * 1440), 1) AS avg_minutes, ROUND(COALESCE(SUM(sc.cost), 0), 2) AS cost_usd, ROUND(COALESCE(SUM(sc.cost), 0) / COUNT(*), 4) AS avg_cost_per_task, ROUND(COALESCE(SUM(sc.cost), 0) / NULLIF(SUM(sc.dur) / 60000.0, 0), 4) AS cost_per_minute FROM agent_tasks t LEFT JOIN agents a ON a.id = t.agentId LEFT JOIN (SELECT taskId, SUM(totalCostUsd) AS cost, SUM(durationMs) AS dur FROM session_costs GROUP BY taskId) sc ON sc.taskId = t.id WHERE t.finishedAt IS NOT NULL AND t.createdAt >= datetime(''now'', ?) AND (? = '''' OR COALESCE(t.agentId, '''') = ?) GROUP BY agent ORDER BY tasks DESC","params":["{{rangeModifier}}","{{agentFilter}}","{{agentFilter}}"],"maxRows":100},"viz":{"type":"table","columns":[{"key":"agent","label":"Agent"},{"key":"tasks","label":"Tasks","format":"integer"},{"key":"avg_minutes","label":"Avg time","format":"duration"},{"key":"avg_cost_per_task","label":"Avg cost / task","format":"currency"},{"key":"cost_usd","label":"Total cost","format":"currency"},{"key":"cost_per_minute","label":"Cost / min","format":"currency"}]}},{"id":"task-outcomes-by-day","title":"Task outcomes by day","description":"Completed and failed tasks for the selected time range.","query":{"sql":"SELECT date(finishedAt) AS day, SUM(CASE WHEN status = ''completed'' THEN 1 ELSE 0 END) AS completed, SUM(CASE WHEN status = ''failed'' THEN 1 ELSE 0 END) AS failed FROM agent_tasks WHERE finishedAt IS NOT NULL AND finishedAt >= datetime(''now'', ?) GROUP BY day ORDER BY day","params":["{{rangeModifier}}"],"maxRows":100},"viz":{"type":"multi-line","x":"day","series":["completed","failed"],"columns":[{"key":"day","label":"Day"},{"key":"completed","label":"Completed","format":"integer"},{"key":"failed","label":"Failed","format":"integer"}],"format":"integer"}},{"id":"recent-task-outcomes","title":"Recent task outcomes","description":"Task status breakdown for tasks created in the selected time range.","query":{"sql":"SELECT status, COUNT(*) AS tasks FROM agent_tasks WHERE createdAt >= datetime(''now'', ?) GROUP BY status ORDER BY tasks DESC","params":["{{rangeModifier}}"],"maxRows":100},"viz":{"type":"table","columns":[{"key":"status","label":"Status"},{"key":"tasks","label":"Tasks","format":"integer"}]}}],"variables":[{"key":"rangeModifier","label":"Time range","type":"select","defaultValue":"-30 days","options":[{"label":"Last 7 days","value":"-7 days"},{"label":"Last 30 days","value":"-30 days"},{"label":"Last 90 days","value":"-90 days"}]},{"key":"userFilter","label":"Requester user ID or name","type":"text","defaultValue":""},{"key":"agentFilter","label":"Agent ID","type":"text","defaultValue":""}],"layout":{"columns":3},"refreshSeconds":60}',
10
+ updatedAt = CURRENT_TIMESTAMP
11
+ WHERE agentId = 'system'
12
+ AND slug = 'swarm-operations-overview';
@@ -0,0 +1,68 @@
1
+ -- Convert the seeded system dashboard user/agent filters from free-text inputs
2
+ -- to dynamic select variables. The UI renders select variables with resolved
3
+ -- options as searchable comboboxes.
4
+
5
+ UPDATE metrics
6
+ SET
7
+ definition = json_set(
8
+ replace(
9
+ replace(
10
+ replace(
11
+ replace(
12
+ replace(
13
+ definition,
14
+ '? = '''' OR COALESCE(u.id, '''') = ? OR COALESCE(u.name, '''') LIKE ''%'' || ? || ''%''',
15
+ '? = ''all'' OR COALESCE(u.id, '''') = ? OR COALESCE(u.name, '''') LIKE ''%'' || ? || ''%'''
16
+ ),
17
+ '? = '''' OR agentId = ?',
18
+ '? = ''all'' OR agentId = ?'
19
+ ),
20
+ '? = '''' OR sc.agentId = ?',
21
+ '? = ''all'' OR sc.agentId = ?'
22
+ ),
23
+ '? = '''' OR COALESCE(t.agentId, '''') = ?',
24
+ '? = ''all'' OR COALESCE(t.agentId, '''') = ?'
25
+ ),
26
+ '? = '''' OR COALESCE(agentId, '''') = ?',
27
+ '? = ''all'' OR COALESCE(agentId, '''') = ?'
28
+ ),
29
+ '$.variables',
30
+ json('[
31
+ {
32
+ "key": "rangeModifier",
33
+ "label": "Time range",
34
+ "type": "select",
35
+ "defaultValue": "-30 days",
36
+ "options": [
37
+ { "label": "Last 7 days", "value": "-7 days" },
38
+ { "label": "Last 30 days", "value": "-30 days" },
39
+ { "label": "Last 90 days", "value": "-90 days" }
40
+ ]
41
+ },
42
+ {
43
+ "key": "userFilter",
44
+ "label": "Requester",
45
+ "type": "select",
46
+ "defaultValue": "all",
47
+ "optionsQuery": {
48
+ "sql": "SELECT id, label FROM (SELECT ''all'' AS id, ''All requesters'' AS label, 0 AS sort_key UNION ALL SELECT id, COALESCE(NULLIF(name, ''''), email, id) AS label, 1 AS sort_key FROM users WHERE id IS NOT NULL) ORDER BY sort_key, label COLLATE NOCASE",
49
+ "valueKey": "id",
50
+ "labelKey": "label"
51
+ }
52
+ },
53
+ {
54
+ "key": "agentFilter",
55
+ "label": "Agent",
56
+ "type": "select",
57
+ "defaultValue": "all",
58
+ "optionsQuery": {
59
+ "sql": "SELECT id, label FROM (SELECT ''all'' AS id, ''All agents'' AS label, 0 AS sort_key UNION ALL SELECT id, COALESCE(NULLIF(name, ''''), id) AS label, 1 AS sort_key FROM agents WHERE id IS NOT NULL) ORDER BY sort_key, label COLLATE NOCASE",
60
+ "valueKey": "id",
61
+ "labelKey": "label"
62
+ }
63
+ }
64
+ ]')
65
+ ),
66
+ updatedAt = CURRENT_TIMESTAMP
67
+ WHERE agentId = 'system'
68
+ AND slug = 'swarm-operations-overview';
@@ -0,0 +1,6 @@
1
+ -- Persist Slack message timestamps used by the watcher to update progress in place.
2
+ -- Without this, a server restart drops process-local maps and the next watcher
3
+ -- tick posts a duplicate progress/tree message in the same Slack thread.
4
+
5
+ ALTER TABLE agent_tasks ADD COLUMN slackProgressMessageTs TEXT;
6
+ ALTER TABLE agent_tasks ADD COLUMN slackTreeRootMessageTs TEXT;
@@ -36,6 +36,56 @@ const BASELINE_TABLES = [
36
36
  "context_versions",
37
37
  ];
38
38
 
39
+ // 090 was renumbered after being applied in production (2026-06-10): PR #722
40
+ // shipped the metrics seed as 090_seed_swarm_operations_metrics; PR #719 then
41
+ // took 090 for model tiers and renumbered the seed to 091. Databases that
42
+ // applied seed-as-090 recorded version 90 with the seed's checksum, so the
43
+ // runner skipped 090_model_tiers forever and task inserts crashed on the
44
+ // missing modelTier column. Detect that exact history and repair it in place:
45
+ // apply the missing ALTERs and repoint row 90 at 090_model_tiers. No-op on
46
+ // fresh databases and on histories where 090_model_tiers applied normally.
47
+ const SEED_APPLIED_AS_090_CHECKSUM =
48
+ "8ca4a05263b42d115b419f468bf5113caa5b7ee4363177568897513549224b01";
49
+
50
+ function repairRenumberedModelTiers(db: Database, migrations: Migration[]): void {
51
+ const modelTiers = migrations.find((m) => m.name === "090_model_tiers");
52
+ if (!modelTiers) return;
53
+
54
+ const row = db
55
+ .prepare<AppliedMigration, []>(
56
+ "SELECT version, name, checksum FROM _migrations WHERE version = 90",
57
+ )
58
+ .get();
59
+ if (
60
+ !row ||
61
+ row.name !== "090_seed_swarm_operations_metrics" ||
62
+ row.checksum !== SEED_APPLIED_AS_090_CHECKSUM
63
+ ) {
64
+ return;
65
+ }
66
+
67
+ console.warn(
68
+ "[migrations] Repairing renumbered migration 090: applying 090_model_tiers over seed-as-090 history",
69
+ );
70
+
71
+ db.transaction(() => {
72
+ for (const table of ["agent_tasks", "scheduled_tasks"]) {
73
+ const hasColumn = db
74
+ .prepare<{ n: number }, [string]>(
75
+ "SELECT COUNT(*) AS n FROM pragma_table_info(?) WHERE name = 'modelTier'",
76
+ )
77
+ .get(table);
78
+ if (!hasColumn?.n) {
79
+ db.run(`ALTER TABLE ${table} ADD COLUMN modelTier TEXT`);
80
+ }
81
+ }
82
+ db.run("UPDATE _migrations SET name = ?, checksum = ? WHERE version = 90", [
83
+ modelTiers.name,
84
+ modelTiers.checksum,
85
+ ]);
86
+ })();
87
+ }
88
+
39
89
  function shouldBootstrapInitialMigration(db: Database): boolean {
40
90
  const rows = db
41
91
  .prepare<{ name: string }, []>(
@@ -107,6 +157,8 @@ export function runMigrations(db: Database): void {
107
157
  return;
108
158
  }
109
159
 
160
+ repairRenumberedModelTiers(db, migrations);
161
+
110
162
  // 3. Get applied migrations
111
163
  const applied = new Map<number, AppliedMigration>();
112
164
  const rows = db