@desplega.ai/agent-swarm 1.92.2 → 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 (122) hide show
  1. package/README.md +2 -2
  2. package/openapi.json +242 -3
  3. package/package.json +5 -5
  4. package/src/be/db.ts +152 -11
  5. package/src/be/memory/boot-reembed.ts +0 -1
  6. package/src/be/memory/providers/sqlite-store.ts +42 -25
  7. package/src/be/memory/raters/llm-client.ts +12 -5
  8. package/src/be/memory/types.ts +3 -0
  9. package/src/be/migrations/088_script_runs_list_indexes.sql +10 -0
  10. package/src/be/migrations/089_harness_variant.sql +2 -0
  11. package/src/be/migrations/090_model_tiers.sql +2 -0
  12. package/src/be/migrations/091_seed_swarm_operations_metrics.sql +12 -0
  13. package/src/be/migrations/092_metrics_dashboard_combobox_filters.sql +68 -0
  14. package/src/be/migrations/093_slack_message_tracking.sql +6 -0
  15. package/src/be/migrations/runner.ts +52 -0
  16. package/src/be/modelsdev-cache.json +3264 -1166
  17. package/src/be/scripts/boot-reembed.ts +74 -0
  18. package/src/be/scripts/db.ts +19 -3
  19. package/src/be/seed/index.ts +1 -1
  20. package/src/be/seed/registry.ts +2 -2
  21. package/src/be/seed/runner.ts +5 -5
  22. package/src/be/seed/types.ts +6 -1
  23. package/src/be/seed-pricing.ts +2 -0
  24. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +221 -0
  25. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +457 -0
  26. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +863 -0
  27. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +506 -0
  28. package/src/be/seed-scripts/index.ts +8 -7
  29. package/src/be/skill-sync.ts +28 -179
  30. package/src/commands/runner.ts +197 -10
  31. package/src/http/api-keys.ts +42 -0
  32. package/src/http/index.ts +13 -2
  33. package/src/http/mcp-bridge.ts +1 -1
  34. package/src/http/memory.ts +23 -24
  35. package/src/http/metrics.ts +55 -6
  36. package/src/http/schedules.ts +16 -15
  37. package/src/http/script-runs.ts +7 -1
  38. package/src/http/scripts.ts +147 -1
  39. package/src/http/tasks.ts +17 -6
  40. package/src/model-tiers.ts +140 -0
  41. package/src/providers/claude-adapter.ts +33 -1
  42. package/src/providers/claude-managed-adapter.ts +3 -0
  43. package/src/providers/claude-managed-models.ts +16 -0
  44. package/src/providers/codex-adapter.ts +8 -1
  45. package/src/providers/codex-models.ts +1 -0
  46. package/src/providers/codex-oauth/auth-json.ts +1 -0
  47. package/src/providers/harness-version.ts +7 -0
  48. package/src/providers/opencode-adapter.ts +12 -4
  49. package/src/providers/pi-mono-adapter.ts +90 -8
  50. package/src/providers/types.ts +2 -0
  51. package/src/scheduler/scheduler.ts +22 -34
  52. package/src/scripts-runtime/egress-secrets.ts +83 -0
  53. package/src/scripts-runtime/eval-harness.ts +4 -0
  54. package/src/scripts-runtime/executors/types.ts +7 -0
  55. package/src/scripts-runtime/loader.ts +2 -0
  56. package/src/server-user.ts +8 -2
  57. package/src/slack/channel-join.ts +41 -0
  58. package/src/slack/responses.ts +39 -11
  59. package/src/slack/watcher.ts +121 -8
  60. package/src/tests/additive-buffer.test.ts +0 -1
  61. package/src/tests/agents-list-model-display.test.ts +13 -0
  62. package/src/tests/api-key-tracking.test.ts +113 -0
  63. package/src/tests/approval-requests.test.ts +0 -6
  64. package/src/tests/aws-error-classifier.test.ts +148 -0
  65. package/src/tests/claude-managed-adapter.test.ts +12 -0
  66. package/src/tests/claude-managed-setup.test.ts +0 -4
  67. package/src/tests/codex-pool.test.ts +2 -6
  68. package/src/tests/context-window.test.ts +7 -0
  69. package/src/tests/http-api-integration.test.ts +23 -6
  70. package/src/tests/memory-edges.test.ts +0 -2
  71. package/src/tests/memory-rate-endpoint.test.ts +0 -2
  72. package/src/tests/memory-rater-e2e.test.ts +0 -2
  73. package/src/tests/memory-store.test.ts +19 -1
  74. package/src/tests/memory.test.ts +51 -0
  75. package/src/tests/metrics-http.test.ts +137 -3
  76. package/src/tests/migration-046-budgets.test.ts +33 -0
  77. package/src/tests/migration-runner-regressions.test.ts +69 -0
  78. package/src/tests/model-control.test.ts +162 -46
  79. package/src/tests/opencode-adapter.test.ts +9 -0
  80. package/src/tests/pi-mono-adapter.test.ts +319 -0
  81. package/src/tests/providers/pi-cost.test.ts +9 -0
  82. package/src/tests/reload-config.test.ts +33 -17
  83. package/src/tests/runner-fallback-output.test.ts +50 -0
  84. package/src/tests/runner-skills-refresh.test.ts +216 -46
  85. package/src/tests/script-runs-http.test.ts +7 -1
  86. package/src/tests/scripts-boot-reembed.test.ts +163 -0
  87. package/src/tests/scripts-embeddings.test.ts +90 -0
  88. package/src/tests/scripts-runtime-secret-egress.test.ts +129 -0
  89. package/src/tests/seed-scripts.test.ts +13 -1
  90. package/src/tests/seed.test.ts +26 -1
  91. package/src/tests/session-attach.test.ts +6 -6
  92. package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
  93. package/src/tests/skill-fs-writer.test.ts +250 -0
  94. package/src/tests/slack-attachments-block.test.ts +0 -1
  95. package/src/tests/slack-blocks.test.ts +0 -1
  96. package/src/tests/slack-channel-join.test.ts +80 -0
  97. package/src/tests/slack-identity-resolution.test.ts +0 -1
  98. package/src/tests/slack-watcher.test.ts +66 -0
  99. package/src/tests/structured-output.test.ts +0 -2
  100. package/src/tests/use-dismissible-card.test.ts +0 -4
  101. package/src/tests/workflow-agent-task.test.ts +5 -2
  102. package/src/tests/workflow-validation-port-routing.test.ts +181 -0
  103. package/src/tools/memory-get.ts +11 -0
  104. package/src/tools/memory-search.ts +18 -0
  105. package/src/tools/schedules/create-schedule.ts +71 -70
  106. package/src/tools/schedules/update-schedule.ts +43 -31
  107. package/src/tools/send-task.ts +16 -5
  108. package/src/tools/slack-post.ts +18 -15
  109. package/src/tools/slack-read.ts +9 -11
  110. package/src/tools/slack-reply.ts +18 -15
  111. package/src/tools/slack-start-thread.ts +17 -14
  112. package/src/tools/task-action.ts +11 -3
  113. package/src/types.ts +40 -0
  114. package/src/utils/aws-error-classifier.ts +97 -0
  115. package/src/utils/context-window.ts +5 -0
  116. package/src/utils/credentials.test.ts +68 -0
  117. package/src/utils/credentials.ts +66 -5
  118. package/src/utils/pretty-print.ts +25 -10
  119. package/src/utils/skill-fs-writer.ts +220 -0
  120. package/src/utils/skills-refresh.ts +123 -40
  121. package/src/workflows/engine.ts +3 -2
  122. package/src/workflows/executors/agent-task.ts +3 -1
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,
@@ -65,6 +66,7 @@ import type {
65
66
  ScriptRun,
66
67
  ScriptRunJournalEntry,
67
68
  ScriptRunKind,
69
+ ScriptRunListItem,
68
70
  ScriptRunStatus,
69
71
  Service,
70
72
  ServiceStatus,
@@ -999,6 +1001,8 @@ type AgentTaskRow = {
999
1001
  slackThreadTs: string | null;
1000
1002
  slackUserId: string | null;
1001
1003
  slackReplySent: number;
1004
+ slackProgressMessageTs: string | null;
1005
+ slackTreeRootMessageTs: string | null;
1002
1006
  vcsProvider: string | null;
1003
1007
  vcsRepo: string | null;
1004
1008
  vcsEventType: string | null;
@@ -1017,6 +1021,7 @@ type AgentTaskRow = {
1017
1021
  parentTaskId: string | null;
1018
1022
  claudeSessionId: string | null;
1019
1023
  model: string | null;
1024
+ modelTier: string | null;
1020
1025
  scheduleId: string | null;
1021
1026
  workflowRunId: string | null;
1022
1027
  workflowRunStepId: string | null;
@@ -1041,6 +1046,8 @@ type AgentTaskRow = {
1041
1046
  swarmVersion: string | null;
1042
1047
  provider: string | null;
1043
1048
  providerMeta: string | null;
1049
+ harnessVariant: string | null;
1050
+ harnessVariantMeta: string | null;
1044
1051
  totalCostUsd?: number | null;
1045
1052
  };
1046
1053
 
@@ -1085,6 +1092,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
1085
1092
  slackThreadTs: row.slackThreadTs ?? undefined,
1086
1093
  slackUserId: row.slackUserId ?? undefined,
1087
1094
  slackReplySent: !!row.slackReplySent,
1095
+ slackProgressMessageTs: row.slackProgressMessageTs ?? undefined,
1096
+ slackTreeRootMessageTs: row.slackTreeRootMessageTs ?? undefined,
1088
1097
  vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
1089
1098
  vcsRepo: row.vcsRepo ?? undefined,
1090
1099
  vcsEventType: row.vcsEventType ?? undefined,
@@ -1102,7 +1111,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
1102
1111
  dir: row.dir ?? undefined,
1103
1112
  parentTaskId: row.parentTaskId ?? undefined,
1104
1113
  claudeSessionId: row.claudeSessionId ?? undefined,
1105
- model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
1114
+ model: row.model ?? undefined,
1115
+ modelTier: parseModelTier(row.modelTier) ?? undefined,
1106
1116
  scheduleId: row.scheduleId ?? undefined,
1107
1117
  workflowRunId: row.workflowRunId ?? undefined,
1108
1118
  workflowRunStepId: row.workflowRunStepId ?? undefined,
@@ -1127,6 +1137,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
1127
1137
  swarmVersion: row.swarmVersion ?? undefined,
1128
1138
  provider: (row.provider as ProviderName | null) ?? undefined,
1129
1139
  providerMeta: parseProviderMeta(row.provider as ProviderName | null, row.providerMeta),
1140
+ harnessVariant: row.harnessVariant ?? undefined,
1141
+ harnessVariantMeta: row.harnessVariantMeta ? JSON.parse(row.harnessVariantMeta) : undefined,
1130
1142
  totalCostUsd: row.totalCostUsd ?? undefined,
1131
1143
  };
1132
1144
  }
@@ -1156,6 +1168,7 @@ function rowToAgentTaskSummary(row: AgentTaskRow): AgentTaskSummary {
1156
1168
  parentTaskId: t.parentTaskId,
1157
1169
  scheduleId: t.scheduleId,
1158
1170
  model: t.model,
1171
+ modelTier: t.modelTier,
1159
1172
  provider: t.provider,
1160
1173
  requestedByUserId: t.requestedByUserId,
1161
1174
  progress: t.progress,
@@ -1358,6 +1371,30 @@ export function markTaskSlackReplySent(taskId: string): void {
1358
1371
  getDb().run(`UPDATE agent_tasks SET slackReplySent = 1 WHERE id = ?`, [taskId]);
1359
1372
  }
1360
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
+
1361
1398
  export function getChildTasks(parentTaskId: string): AgentTask[] {
1362
1399
  return getDb()
1363
1400
  .prepare<AgentTaskRow, [string]>(
@@ -1398,6 +1435,8 @@ export function updateTaskClaudeSessionId(
1398
1435
  provider?: ProviderName,
1399
1436
  providerMeta?: Record<string, unknown>,
1400
1437
  model?: string,
1438
+ harnessVariant?: string,
1439
+ harnessVariantMeta?: Record<string, unknown>,
1401
1440
  ): AgentTask | null {
1402
1441
  const setClauses = ["claudeSessionId = ?", "lastUpdatedAt = ?"];
1403
1442
  const params: (string | null)[] = [claudeSessionId, new Date().toISOString()];
@@ -1414,6 +1453,14 @@ export function updateTaskClaudeSessionId(
1414
1453
  setClauses.push("model = ?");
1415
1454
  params.push(model);
1416
1455
  }
1456
+ if (harnessVariant !== undefined) {
1457
+ setClauses.push("harnessVariant = ?");
1458
+ params.push(harnessVariant);
1459
+ }
1460
+ if (harnessVariantMeta !== undefined) {
1461
+ setClauses.push("harnessVariantMeta = ?");
1462
+ params.push(JSON.stringify(harnessVariantMeta));
1463
+ }
1417
1464
 
1418
1465
  params.push(taskId);
1419
1466
 
@@ -2835,6 +2882,7 @@ export interface CreateTaskOptions {
2835
2882
  dir?: string;
2836
2883
  parentTaskId?: string;
2837
2884
  model?: string;
2885
+ modelTier?: ModelTier;
2838
2886
  scheduleId?: string;
2839
2887
  workflowRunId?: string;
2840
2888
  workflowRunStepId?: string;
@@ -3041,9 +3089,9 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
3041
3089
  vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
3042
3090
  vcsInstallationId, vcsNodeId,
3043
3091
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
3044
- mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
3092
+ mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, scheduleId,
3045
3093
  workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
3046
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3094
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
3047
3095
  )
3048
3096
  .get(
3049
3097
  id,
@@ -3078,6 +3126,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
3078
3126
  options?.dir ?? null,
3079
3127
  options?.parentTaskId ?? null,
3080
3128
  options?.model ?? null,
3129
+ options?.modelTier ?? null,
3081
3130
  options?.scheduleId ?? null,
3082
3131
  options?.workflowRunId ?? null,
3083
3132
  options?.workflowRunStepId ?? null,
@@ -5251,6 +5300,7 @@ type ScheduledTaskRow = {
5251
5300
  lastErrorAt: string | null;
5252
5301
  lastErrorMessage: string | null;
5253
5302
  model: string | null;
5303
+ modelTier: string | null;
5254
5304
  scheduleType: string;
5255
5305
  createdAt: string;
5256
5306
  lastUpdatedAt: string;
@@ -5291,7 +5341,8 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
5291
5341
  consecutiveErrors: row.consecutiveErrors ?? 0,
5292
5342
  lastErrorAt: normalizeDate(row.lastErrorAt) ?? undefined,
5293
5343
  lastErrorMessage: row.lastErrorMessage ?? undefined,
5294
- model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
5344
+ model: row.model ?? undefined,
5345
+ modelTier: parseModelTier(row.modelTier) ?? undefined,
5295
5346
  scheduleType: row.scheduleType as "recurring" | "one_time",
5296
5347
  createdAt: normalizeDateRequired(row.createdAt),
5297
5348
  lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
@@ -5386,6 +5437,7 @@ export interface CreateScheduledTaskData {
5386
5437
  createdByAgentId?: string;
5387
5438
  timezone?: string;
5388
5439
  model?: string;
5440
+ modelTier?: ModelTier;
5389
5441
  scheduleType?: "recurring" | "one_time";
5390
5442
  }
5391
5443
 
@@ -5398,8 +5450,8 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
5398
5450
  `INSERT INTO scheduled_tasks (
5399
5451
  id, name, description, cronExpression, intervalMs, taskTemplate,
5400
5452
  taskType, tags, priority, targetAgentId, enabled, nextRunAt,
5401
- createdByAgentId, timezone, model, scheduleType, createdAt, lastUpdatedAt
5402
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5453
+ createdByAgentId, timezone, model, modelTier, scheduleType, createdAt, lastUpdatedAt
5454
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
5403
5455
  )
5404
5456
  .get(
5405
5457
  id,
@@ -5417,6 +5469,7 @@ export function createScheduledTask(data: CreateScheduledTaskData): ScheduledTas
5417
5469
  data.createdByAgentId ?? null,
5418
5470
  data.timezone ?? "UTC",
5419
5471
  data.model ?? null,
5472
+ data.modelTier ?? null,
5420
5473
  data.scheduleType ?? "recurring",
5421
5474
  now,
5422
5475
  now,
@@ -5444,6 +5497,7 @@ export interface UpdateScheduledTaskData {
5444
5497
  lastErrorAt?: string | null;
5445
5498
  lastErrorMessage?: string | null;
5446
5499
  model?: string | null;
5500
+ modelTier?: ModelTier | null;
5447
5501
  scheduleType?: "recurring" | "one_time";
5448
5502
  lastUpdatedAt?: string;
5449
5503
  }
@@ -5523,6 +5577,10 @@ export function updateScheduledTask(
5523
5577
  updates.push("model = ?");
5524
5578
  params.push(data.model);
5525
5579
  }
5580
+ if (data.modelTier !== undefined) {
5581
+ updates.push("modelTier = ?");
5582
+ params.push(data.modelTier);
5583
+ }
5526
5584
  if (data.scheduleType !== undefined) {
5527
5585
  updates.push("scheduleType = ?");
5528
5586
  params.push(data.scheduleType);
@@ -10058,6 +10116,28 @@ export function setApiKeyName(
10058
10116
  return result.changes > 0;
10059
10117
  }
10060
10118
 
10119
+ /**
10120
+ * Clear a stale rate-limit record after a successful use proves the key is healthy.
10121
+ */
10122
+ export function clearKeyRateLimit(
10123
+ keyType: string,
10124
+ keySuffix: string,
10125
+ scope = "global",
10126
+ scopeId: string | null = null,
10127
+ ): boolean {
10128
+ const now = new Date().toISOString();
10129
+ const effectiveScopeId = scopeId ?? "";
10130
+ const result = getDb()
10131
+ .prepare(
10132
+ `UPDATE api_key_status
10133
+ SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
10134
+ WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
10135
+ AND status = 'rate_limited'`,
10136
+ )
10137
+ .run(now, keyType, keySuffix, scope, effectiveScopeId);
10138
+ return result.changes > 0;
10139
+ }
10140
+
10061
10141
  /**
10062
10142
  * Get all key status records for a credential type.
10063
10143
  */
@@ -11574,6 +11654,22 @@ type ScriptRunRow = {
11574
11654
  updated_by: string | null;
11575
11655
  };
11576
11656
 
11657
+ type ScriptRunListRow = Pick<
11658
+ ScriptRunRow,
11659
+ | "id"
11660
+ | "agentId"
11661
+ | "scriptName"
11662
+ | "kind"
11663
+ | "status"
11664
+ | "pid"
11665
+ | "startedAt"
11666
+ | "finishedAt"
11667
+ | "error"
11668
+ | "last_heartbeat_at"
11669
+ | "idempotencyKey"
11670
+ | "requestedByUserId"
11671
+ >;
11672
+
11577
11673
  function parseJsonColumn(value: string | null): unknown | undefined {
11578
11674
  if (value === null) return undefined;
11579
11675
  return JSON.parse(value);
@@ -11599,6 +11695,23 @@ function rowToScriptRun(row: ScriptRunRow): ScriptRun {
11599
11695
  };
11600
11696
  }
11601
11697
 
11698
+ function rowToScriptRunListItem(row: ScriptRunListRow): ScriptRunListItem {
11699
+ return {
11700
+ id: row.id,
11701
+ agentId: row.agentId,
11702
+ scriptName: row.scriptName ?? undefined,
11703
+ kind: row.kind as ScriptRunKind,
11704
+ status: row.status as ScriptRunStatus,
11705
+ pid: row.pid ?? undefined,
11706
+ startedAt: row.startedAt,
11707
+ finishedAt: row.finishedAt ?? undefined,
11708
+ error: row.error ?? undefined,
11709
+ lastHeartbeatAt: row.last_heartbeat_at ?? undefined,
11710
+ idempotencyKey: row.idempotencyKey ?? undefined,
11711
+ requestedByUserId: row.requestedByUserId ?? undefined,
11712
+ };
11713
+ }
11714
+
11602
11715
  export function createScriptRun(data: {
11603
11716
  id: string;
11604
11717
  agentId: string;
@@ -11731,9 +11844,10 @@ export function getScriptRunByIdempotencyKey(idempotencyKey: string): ScriptRun
11731
11844
  export function listScriptRuns(opts?: {
11732
11845
  status?: ScriptRunStatus;
11733
11846
  agentId?: string;
11847
+ scriptName?: string;
11734
11848
  limit?: number;
11735
11849
  offset?: number;
11736
- }): ScriptRun[] {
11850
+ }): ScriptRunListItem[] {
11737
11851
  const conditions: string[] = [];
11738
11852
  const params: Array<string | number> = [];
11739
11853
  if (opts?.status) {
@@ -11744,20 +11858,43 @@ export function listScriptRuns(opts?: {
11744
11858
  conditions.push("agentId = ?");
11745
11859
  params.push(opts.agentId);
11746
11860
  }
11861
+ if (opts?.scriptName) {
11862
+ conditions.push("scriptName = ?");
11863
+ params.push(opts.scriptName);
11864
+ }
11747
11865
 
11748
11866
  const limit = opts?.limit ?? 50;
11749
11867
  const offset = opts?.offset ?? 0;
11750
11868
  params.push(limit, offset);
11751
11869
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
11752
11870
  const rows = getDb()
11753
- .prepare<ScriptRunRow, Array<string | number>>(
11754
- `SELECT * FROM script_runs ${where} ORDER BY startedAt DESC LIMIT ? OFFSET ?`,
11871
+ .prepare<ScriptRunListRow, Array<string | number>>(
11872
+ `SELECT
11873
+ id,
11874
+ agentId,
11875
+ scriptName,
11876
+ kind,
11877
+ status,
11878
+ pid,
11879
+ startedAt,
11880
+ finishedAt,
11881
+ error,
11882
+ last_heartbeat_at,
11883
+ idempotencyKey,
11884
+ requestedByUserId
11885
+ FROM script_runs ${where}
11886
+ ORDER BY startedAt DESC
11887
+ LIMIT ? OFFSET ?`,
11755
11888
  )
11756
11889
  .all(...params);
11757
- return rows.map(rowToScriptRun);
11890
+ return rows.map(rowToScriptRunListItem);
11758
11891
  }
11759
11892
 
11760
- 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 {
11761
11898
  const conditions: string[] = [];
11762
11899
  const params: string[] = [];
11763
11900
  if (opts?.status) {
@@ -11768,6 +11905,10 @@ export function countScriptRuns(opts?: { status?: ScriptRunStatus; agentId?: str
11768
11905
  conditions.push("agentId = ?");
11769
11906
  params.push(opts.agentId);
11770
11907
  }
11908
+ if (opts?.scriptName) {
11909
+ conditions.push("scriptName = ?");
11910
+ params.push(opts.scriptName);
11911
+ }
11771
11912
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
11772
11913
  const row = getDb()
11773
11914
  .prepare<{ count: number }, string[]>(`SELECT COUNT(*) AS count FROM script_runs ${where}`)
@@ -13,7 +13,6 @@ import { getEmbeddingProvider, getMemoryStore } from "./index";
13
13
 
14
14
  const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
15
15
  const BATCH_SIZE = 20;
16
- const BACKFILL_KV_KEY = "memory:reembed:backfill_complete";
17
16
 
18
17
  export async function runBootReembed(): Promise<void> {
19
18
  const db = getDb();
@@ -488,29 +488,19 @@ export class SqliteMemoryStore implements MemoryStore {
488
488
  }
489
489
  }
490
490
 
491
- list(agentId: string, options: MemoryListOptions = {}): AgentMemory[] {
492
- const { scope = "all", limit = 20, offset = 0, isLead = false, source } = options;
493
- const db = getDb();
494
-
491
+ private buildListWhereClause(
492
+ agentId: string,
493
+ options: MemoryListOptions,
494
+ ): { whereClause: string; params: (Buffer | string | number | null)[] } {
495
+ const { scope = "all", isLead = false, ownerAgentId, source, sourcePath } = options;
495
496
  const conditions: string[] = [];
496
- const params: (string | number)[] = [];
497
+ const params: (Buffer | string | number | null)[] = [];
497
498
 
498
- if (!isLead) {
499
- if (scope === "agent") {
500
- conditions.push("agentId = ? AND scope = 'agent'");
501
- params.push(agentId);
502
- } else if (scope === "swarm") {
503
- conditions.push("scope = 'swarm'");
504
- } else {
505
- conditions.push("(agentId = ? OR scope = 'swarm')");
506
- params.push(agentId);
507
- }
508
- } else {
509
- if (scope === "agent") {
510
- conditions.push("scope = 'agent'");
511
- } else if (scope === "swarm") {
512
- conditions.push("scope = 'swarm'");
513
- }
499
+ this.addScopeConditions(conditions, params, agentId, scope, isLead);
500
+
501
+ if (ownerAgentId) {
502
+ conditions.push("agentId = ?");
503
+ params.push(ownerAgentId);
514
504
  }
515
505
 
516
506
  if (source) {
@@ -518,18 +508,45 @@ export class SqliteMemoryStore implements MemoryStore {
518
508
  params.push(source);
519
509
  }
520
510
 
521
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
522
- params.push(limit, offset);
511
+ const sourcePathNeedle = sourcePath?.trim().toLowerCase();
512
+ if (sourcePathNeedle) {
513
+ conditions.push("instr(lower(coalesce(sourcePath, '')), ?) > 0");
514
+ params.push(sourcePathNeedle);
515
+ }
516
+
517
+ return {
518
+ whereClause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
519
+ params,
520
+ };
521
+ }
522
+
523
+ list(agentId: string, options: MemoryListOptions = {}): AgentMemory[] {
524
+ const { limit = 20, offset = 0 } = options;
525
+ const db = getDb();
526
+ const { whereClause, params } = this.buildListWhereClause(agentId, options);
527
+ const queryParams = [...params, limit, offset];
523
528
 
524
529
  const rows = db
525
- .prepare<AgentMemoryRow, (string | number)[]>(
530
+ .prepare<AgentMemoryRow, (Buffer | string | number | null)[]>(
526
531
  `SELECT * FROM agent_memory ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
527
532
  )
528
- .all(...params);
533
+ .all(...queryParams);
529
534
 
530
535
  return rows.map(rowToAgentMemory);
531
536
  }
532
537
 
538
+ count(agentId: string, options: MemoryListOptions = {}): number {
539
+ const db = getDb();
540
+ const { whereClause, params } = this.buildListWhereClause(agentId, options);
541
+ const row = db
542
+ .prepare<{ count: number }, (Buffer | string | number | null)[]>(
543
+ `SELECT COUNT(*) AS count FROM agent_memory ${whereClause}`,
544
+ )
545
+ .get(...params);
546
+
547
+ return row?.count ?? 0;
548
+ }
549
+
533
550
  isSourceProtected(source: AgentMemorySource): boolean {
534
551
  return PROTECTED_SOURCES.has(source);
535
552
  }
@@ -75,6 +75,13 @@ AGENT RESPONSE / SUMMARY:
75
75
 
76
76
  Score 0..1.`;
77
77
 
78
+ const PLACEHOLDER_PREFIX = "$";
79
+ const QUERY_PLACEHOLDER = `${PLACEHOLDER_PREFIX}{query}`;
80
+ const MEMORY_ID_PLACEHOLDER = `${PLACEHOLDER_PREFIX}{memoryId}`;
81
+ const MEMORY_NAME_PLACEHOLDER = `${PLACEHOLDER_PREFIX}{memoryName}`;
82
+ const MEMORY_CONTENT_PLACEHOLDER = `${PLACEHOLDER_PREFIX}{memoryContent}`;
83
+ const RESPONSE_PLACEHOLDER = `${PLACEHOLDER_PREFIX}{response}`;
84
+
78
85
  /**
79
86
  * `claude -p --output-format json` returns a JSON envelope of the shape
80
87
  * `{ result: string, ... }`. We parse the envelope, then JSON-parse the
@@ -83,11 +90,11 @@ Score 0..1.`;
83
90
  type ClaudeCliEnvelope = { result?: unknown };
84
91
 
85
92
  function buildPrompt(input: LlmRaterInput): string {
86
- return PROMPT_TEMPLATE.replace("${query}", input.query)
87
- .replace("${memoryId}", input.memory.id)
88
- .replace("${memoryName}", input.memory.name)
89
- .replace("${memoryContent}", input.memory.content)
90
- .replace("${response}", input.response);
93
+ return PROMPT_TEMPLATE.replace(QUERY_PLACEHOLDER, input.query)
94
+ .replace(MEMORY_ID_PLACEHOLDER, input.memory.id)
95
+ .replace(MEMORY_NAME_PLACEHOLDER, input.memory.name)
96
+ .replace(MEMORY_CONTENT_PLACEHOLDER, input.memory.content)
97
+ .replace(RESPONSE_PLACEHOLDER, input.response);
91
98
  }
92
99
 
93
100
  function parseScoreAndReasoning(raw: unknown): LlmRaterResult | null {
@@ -22,6 +22,7 @@ export interface MemoryStore {
22
22
  peek(id: string): AgentMemory | null;
23
23
  search(embedding: Float32Array, agentId: string, options: MemorySearchOptions): MemoryCandidate[];
24
24
  list(agentId: string, options: MemoryListOptions): AgentMemory[];
25
+ count(agentId: string, options: MemoryListOptions): number;
25
26
  isSourceProtected(source: AgentMemorySource): boolean;
26
27
  listForCuration(
27
28
  agentId?: string,
@@ -80,7 +81,9 @@ export interface MemoryListOptions {
80
81
  limit?: number;
81
82
  offset?: number;
82
83
  isLead?: boolean;
84
+ ownerAgentId?: string;
83
85
  source?: AgentMemorySource;
86
+ sourcePath?: string;
84
87
  }
85
88
 
86
89
  export interface MemoryStats {
@@ -0,0 +1,10 @@
1
+ -- Keep script run list pages fast as historical rows accumulate.
2
+
3
+ CREATE INDEX IF NOT EXISTS idx_script_runs_startedAt
4
+ ON script_runs(startedAt DESC);
5
+
6
+ CREATE INDEX IF NOT EXISTS idx_script_runs_status_startedAt
7
+ ON script_runs(status, startedAt DESC);
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_script_runs_agentId_startedAt
10
+ ON script_runs(agentId, startedAt DESC);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE agent_tasks ADD COLUMN harnessVariant TEXT;
2
+ ALTER TABLE agent_tasks ADD COLUMN harnessVariantMeta TEXT;
@@ -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;