@desplega.ai/agent-swarm 1.81.0 → 1.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.json +132 -1
- package/package.json +3 -2
- package/src/be/db.ts +610 -96
- package/src/be/migrations/069_agent_tasks_perf_indexes.sql +17 -0
- package/src/be/migrations/070_seed_state.sql +22 -0
- package/src/be/migrations/071_codex_oauth_pool.sql +8 -0
- package/src/be/migrations/072_task_attachments.sql +38 -0
- package/src/be/scripts/typecheck.ts +450 -7
- package/src/be/seed/index.ts +9 -0
- package/src/be/seed/registry.ts +18 -0
- package/src/be/seed/runner.ts +98 -0
- package/src/be/seed/state-db.ts +36 -0
- package/src/be/seed/types.ts +59 -0
- package/src/be/seed-scripts/catalog/date-resolve.ts +104 -0
- package/src/be/seed-scripts/catalog/fetch-readable.ts +77 -0
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +104 -0
- package/src/be/seed-scripts/catalog/group-count.ts +70 -0
- package/src/be/seed-scripts/catalog/json-query.ts +124 -0
- package/src/be/seed-scripts/catalog/linear-issue.ts +103 -0
- package/src/be/seed-scripts/catalog/memory-dedup-check.ts +61 -0
- package/src/be/seed-scripts/catalog/slack-thread-flatten.ts +86 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +87 -0
- package/src/be/seed-scripts/catalog/text-diff.ts +103 -0
- package/src/be/seed-scripts/index.ts +183 -0
- package/src/cli.tsx +2 -0
- package/src/commands/codex-login.ts +36 -6
- package/src/commands/runner.ts +133 -44
- package/src/github/handlers.ts +7 -7
- package/src/http/agents.ts +7 -1
- package/src/http/index.ts +123 -74
- package/src/http/pages.ts +25 -5
- package/src/http/route-def.ts +63 -0
- package/src/http/schedules.ts +11 -2
- package/src/http/scripts.ts +10 -1
- package/src/http/sessions.ts +31 -3
- package/src/http/skills.ts +8 -1
- package/src/http/stats.ts +19 -0
- package/src/http/tasks.ts +13 -2
- package/src/http/utils.ts +44 -0
- package/src/http/workflows.ts +11 -1
- package/src/http.ts +2 -0
- package/src/otel-impl.ts +34 -4
- package/src/otel.ts +1 -3
- package/src/providers/claude-adapter.ts +61 -0
- package/src/providers/codex-adapter.ts +22 -1
- package/src/providers/codex-oauth/auth-json-fs.ts +52 -0
- package/src/providers/codex-oauth/auth-json.ts +3 -3
- package/src/providers/codex-oauth/storage.ts +81 -21
- package/src/providers/otel-env.ts +63 -0
- package/src/providers/types.ts +5 -0
- package/src/scripts-runtime/eval-harness.ts +70 -3
- package/src/scripts-runtime/executors/native.ts +19 -1
- package/src/scripts-runtime/executors/types.ts +17 -0
- package/src/scripts-runtime/loader.ts +2 -0
- package/src/server.ts +2 -0
- package/src/tests/claude-adapter-otel.test.ts +225 -0
- package/src/tests/codex-adapter-otel.test.ts +120 -0
- package/src/tests/codex-login.test.ts +142 -0
- package/src/tests/codex-oauth-adapter.test.ts +108 -0
- package/src/tests/codex-oauth-auth-json-fs.test.ts +112 -0
- package/src/tests/codex-oauth-storage.test.ts +262 -86
- package/src/tests/codex-pool.test.ts +284 -0
- package/src/tests/github-handlers.test.ts +29 -0
- package/src/tests/http-semconv-attributes.test.ts +92 -0
- package/src/tests/list-endpoint-slimming.test.ts +179 -0
- package/src/tests/mcp-tools-user.test.ts +48 -1
- package/src/tests/otel-env.test.ts +103 -0
- package/src/tests/otel-service-name.test.ts +55 -0
- package/src/tests/pagination-metrics.test.ts +165 -0
- package/src/tests/prompt-template-resolver.test.ts +1 -1
- package/src/tests/route-def-find-route.test.ts +106 -0
- package/src/tests/scripts-http.test.ts +110 -0
- package/src/tests/scripts-runtime.test.ts +1 -0
- package/src/tests/scripts-typecheck.test.ts +175 -0
- package/src/tests/seed-scripts.test.ts +220 -0
- package/src/tests/seed.test.ts +163 -0
- package/src/tests/send-task-requested-by.test.ts +154 -0
- package/src/tests/sessions.test.ts +53 -0
- package/src/tests/store-progress-attachments.test.ts +312 -0
- package/src/tests/workflow-http-v2.test.ts +16 -2
- package/src/tools/get-metrics.ts +46 -0
- package/src/tools/get-swarm.ts +10 -3
- package/src/tools/get-task-details.ts +15 -2
- package/src/tools/get-tasks.ts +22 -5
- package/src/tools/resolve-user.ts +25 -10
- package/src/tools/schedules/list-schedules.ts +15 -4
- package/src/tools/send-task.ts +16 -0
- package/src/tools/store-progress.ts +66 -4
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +3 -1
- package/src/tools/workflows/list-workflows.ts +12 -3
- package/src/types.ts +128 -0
- package/src/utils/internal-ai/register-bedrock.ts +34 -0
- package/src/utils/secret-scrubber.ts +3 -0
- /package/src/be/{seed.ts → seed-prompt-templates.ts} +0 -0
package/src/be/db.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
AgentTask,
|
|
16
16
|
AgentTaskSource,
|
|
17
17
|
AgentTaskStatus,
|
|
18
|
+
AgentTaskSummary,
|
|
18
19
|
AgentWithTasks,
|
|
19
20
|
Budget,
|
|
20
21
|
BudgetRefusalCause,
|
|
@@ -44,6 +45,7 @@ import type {
|
|
|
44
45
|
PageAuthMode,
|
|
45
46
|
PageContentType,
|
|
46
47
|
PageSnapshot,
|
|
48
|
+
PageSummary,
|
|
47
49
|
PageVersion,
|
|
48
50
|
PricingProvider,
|
|
49
51
|
PricingRow,
|
|
@@ -53,6 +55,7 @@ import type {
|
|
|
53
55
|
ProviderName,
|
|
54
56
|
RepoGuidelines,
|
|
55
57
|
ScheduledTask,
|
|
58
|
+
ScheduledTaskSummary,
|
|
56
59
|
Service,
|
|
57
60
|
ServiceStatus,
|
|
58
61
|
SessionCost,
|
|
@@ -64,6 +67,7 @@ import type {
|
|
|
64
67
|
SkillWithInstallInfo,
|
|
65
68
|
SwarmConfig,
|
|
66
69
|
SwarmRepo,
|
|
70
|
+
TaskAttachment,
|
|
67
71
|
TaskTemplate,
|
|
68
72
|
TaskTemplateKind,
|
|
69
73
|
TriggerConfig,
|
|
@@ -80,6 +84,7 @@ import type {
|
|
|
80
84
|
WorkflowRunStep,
|
|
81
85
|
WorkflowRunStepStatus,
|
|
82
86
|
WorkflowSnapshot,
|
|
87
|
+
WorkflowSummary,
|
|
83
88
|
WorkflowVersion,
|
|
84
89
|
} from "../types";
|
|
85
90
|
import { deriveProviderFromKeyType } from "../utils/credentials";
|
|
@@ -87,7 +92,7 @@ import { scrubSecrets } from "../utils/secret-scrubber";
|
|
|
87
92
|
import { decryptSecret, encryptSecret, getEncryptionKey, resolveEncryptionKey } from "./crypto";
|
|
88
93
|
import { normalizeDate, normalizeDateRequired } from "./date-utils";
|
|
89
94
|
import { runMigrations } from "./migrations/runner";
|
|
90
|
-
import { seedDefaultTemplates } from "./seed";
|
|
95
|
+
import { seedDefaultTemplates } from "./seed-prompt-templates";
|
|
91
96
|
import { isReservedConfigKey, reservedKeyError } from "./swarm-config-guard";
|
|
92
97
|
|
|
93
98
|
let db: Database | null = null;
|
|
@@ -128,6 +133,10 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
128
133
|
database.run("PRAGMA journal_mode = WAL;");
|
|
129
134
|
database.run("PRAGMA busy_timeout = 5000;");
|
|
130
135
|
database.run("PRAGMA foreign_keys = ON;");
|
|
136
|
+
database.run("PRAGMA synchronous = NORMAL;");
|
|
137
|
+
database.run("PRAGMA cache_size = -64000;");
|
|
138
|
+
database.run("PRAGMA mmap_size = 268435456;");
|
|
139
|
+
database.run("PRAGMA temp_store = MEMORY;");
|
|
131
140
|
|
|
132
141
|
// Load sqlite-vec extension for vector search.
|
|
133
142
|
// In compiled binaries (`bun build --compile`) the JS lives in /$bunfs/ and
|
|
@@ -575,8 +584,15 @@ type AgentRow = {
|
|
|
575
584
|
cred_status: string | null;
|
|
576
585
|
};
|
|
577
586
|
|
|
578
|
-
|
|
579
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Map an agent row to the `Agent` shape. When `slim` is true the six identity
|
|
589
|
+
* markdown blobs (`claudeMd`/`soulMd`/`identityMd`/`toolsMd`/`heartbeatMd`/
|
|
590
|
+
* `setupScript`) are omitted — they bloat list responses by ~16 KB/agent and
|
|
591
|
+
* are never needed at the swarm-overview level. Fetch them via
|
|
592
|
+
* `GET /api/agents/{id}` when required.
|
|
593
|
+
*/
|
|
594
|
+
function rowToAgent(row: AgentRow, slim = false): Agent {
|
|
595
|
+
const base: Agent = {
|
|
580
596
|
id: row.id,
|
|
581
597
|
name: row.name,
|
|
582
598
|
isLead: row.isLead === 1,
|
|
@@ -586,12 +602,6 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
586
602
|
capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
|
|
587
603
|
maxTasks: row.maxTasks ?? 1,
|
|
588
604
|
emptyPollCount: row.emptyPollCount ?? 0,
|
|
589
|
-
claudeMd: row.claudeMd ?? undefined,
|
|
590
|
-
soulMd: row.soulMd ?? undefined,
|
|
591
|
-
identityMd: row.identityMd ?? undefined,
|
|
592
|
-
setupScript: row.setupScript ?? undefined,
|
|
593
|
-
toolsMd: row.toolsMd ?? undefined,
|
|
594
|
-
heartbeatMd: row.heartbeatMd ?? undefined,
|
|
595
605
|
lastActivityAt: row.lastActivityAt ?? undefined,
|
|
596
606
|
provider: (row.provider as ProviderName | null) ?? undefined,
|
|
597
607
|
harnessProvider: (row.harness_provider as ProviderName | null) ?? null,
|
|
@@ -602,6 +612,16 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
602
612
|
: null,
|
|
603
613
|
credStatus: row.cred_status ? (JSON.parse(row.cred_status) as AgentCredStatus) : null,
|
|
604
614
|
};
|
|
615
|
+
if (slim) return base;
|
|
616
|
+
return {
|
|
617
|
+
...base,
|
|
618
|
+
claudeMd: row.claudeMd ?? undefined,
|
|
619
|
+
soulMd: row.soulMd ?? undefined,
|
|
620
|
+
identityMd: row.identityMd ?? undefined,
|
|
621
|
+
setupScript: row.setupScript ?? undefined,
|
|
622
|
+
toolsMd: row.toolsMd ?? undefined,
|
|
623
|
+
heartbeatMd: row.heartbeatMd ?? undefined,
|
|
624
|
+
};
|
|
605
625
|
}
|
|
606
626
|
|
|
607
627
|
export const agentQueries = {
|
|
@@ -680,8 +700,11 @@ export function getAgentById(id: string): Agent | null {
|
|
|
680
700
|
return row ? rowToAgent(row) : null;
|
|
681
701
|
}
|
|
682
702
|
|
|
683
|
-
export function getAllAgents(): Agent[] {
|
|
684
|
-
return agentQueries
|
|
703
|
+
export function getAllAgents(opts?: { slim?: boolean }): Agent[] {
|
|
704
|
+
return agentQueries
|
|
705
|
+
.getAll()
|
|
706
|
+
.all()
|
|
707
|
+
.map((row) => rowToAgent(row, opts?.slim ?? false));
|
|
685
708
|
}
|
|
686
709
|
|
|
687
710
|
export function getLeadAgent(): Agent | null {
|
|
@@ -778,7 +801,7 @@ export function listAgentsWithCredStatusByProvider(provider: string): Agent[] {
|
|
|
778
801
|
const rows = getDb()
|
|
779
802
|
.prepare<AgentRow, [string]>(`SELECT * FROM agents WHERE harness_provider = ? ORDER BY name`)
|
|
780
803
|
.all(provider);
|
|
781
|
-
return rows.map(rowToAgent);
|
|
804
|
+
return rows.map((row) => rowToAgent(row));
|
|
782
805
|
}
|
|
783
806
|
|
|
784
807
|
/**
|
|
@@ -1055,6 +1078,41 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1055
1078
|
};
|
|
1056
1079
|
}
|
|
1057
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* Slim list-row mapper — truncates the `task` text to a bounded preview and
|
|
1083
|
+
* drops completion/integration/context blobs (`output`, `failureReason`,
|
|
1084
|
+
* `providerMeta`, all `vcs*`/`slack*`/`agentmail*`/`credential*`/`mention*` and
|
|
1085
|
+
* context-window fields). The preview is long enough for pool-triage; the full
|
|
1086
|
+
* brief is on `get-task-details` / `GET /api/tasks/{id}`.
|
|
1087
|
+
*/
|
|
1088
|
+
function rowToAgentTaskSummary(row: AgentTaskRow): AgentTaskSummary {
|
|
1089
|
+
const t = rowToAgentTask(row);
|
|
1090
|
+
return {
|
|
1091
|
+
id: t.id,
|
|
1092
|
+
agentId: t.agentId,
|
|
1093
|
+
creatorAgentId: t.creatorAgentId,
|
|
1094
|
+
task: previewText(t.task, TASK_PREVIEW_LENGTH),
|
|
1095
|
+
status: t.status,
|
|
1096
|
+
source: t.source,
|
|
1097
|
+
taskType: t.taskType,
|
|
1098
|
+
tags: t.tags,
|
|
1099
|
+
priority: t.priority,
|
|
1100
|
+
dependsOn: t.dependsOn,
|
|
1101
|
+
offeredTo: t.offeredTo,
|
|
1102
|
+
acceptedAt: t.acceptedAt,
|
|
1103
|
+
parentTaskId: t.parentTaskId,
|
|
1104
|
+
scheduleId: t.scheduleId,
|
|
1105
|
+
model: t.model,
|
|
1106
|
+
provider: t.provider,
|
|
1107
|
+
requestedByUserId: t.requestedByUserId,
|
|
1108
|
+
progress: t.progress,
|
|
1109
|
+
createdAt: t.createdAt,
|
|
1110
|
+
lastUpdatedAt: t.lastUpdatedAt,
|
|
1111
|
+
finishedAt: t.finishedAt,
|
|
1112
|
+
peakContextPercent: t.peakContextPercent,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1058
1116
|
export const taskQueries = {
|
|
1059
1117
|
insert: () =>
|
|
1060
1118
|
getDb().prepare<
|
|
@@ -1224,7 +1282,7 @@ export function markTaskSlackReplySent(taskId: string): void {
|
|
|
1224
1282
|
export function getChildTasks(parentTaskId: string): AgentTask[] {
|
|
1225
1283
|
return getDb()
|
|
1226
1284
|
.prepare<AgentTaskRow, [string]>(
|
|
1227
|
-
`SELECT * FROM agent_tasks WHERE parentTaskId = ? ORDER BY createdAt ASC`,
|
|
1285
|
+
`SELECT * FROM agent_tasks WHERE parentTaskId = ? ORDER BY createdAt ASC, rowid ASC`,
|
|
1228
1286
|
)
|
|
1229
1287
|
.all(parentTaskId)
|
|
1230
1288
|
.map(rowToAgentTask);
|
|
@@ -1348,7 +1406,15 @@ export interface TaskFilters {
|
|
|
1348
1406
|
includeHeartbeat?: boolean;
|
|
1349
1407
|
}
|
|
1350
1408
|
|
|
1351
|
-
export function getAllTasks(filters?: TaskFilters): AgentTask[]
|
|
1409
|
+
export function getAllTasks(filters?: TaskFilters): AgentTask[];
|
|
1410
|
+
export function getAllTasks(
|
|
1411
|
+
filters: TaskFilters | undefined,
|
|
1412
|
+
opts: { slim: true },
|
|
1413
|
+
): AgentTaskSummary[];
|
|
1414
|
+
export function getAllTasks(
|
|
1415
|
+
filters?: TaskFilters,
|
|
1416
|
+
opts?: { slim?: boolean },
|
|
1417
|
+
): AgentTask[] | AgentTaskSummary[] {
|
|
1352
1418
|
const conditions: string[] = [];
|
|
1353
1419
|
const params: (string | AgentTaskStatus)[] = [];
|
|
1354
1420
|
|
|
@@ -1433,19 +1499,25 @@ export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
|
1433
1499
|
const offset = filters?.offset ?? 0;
|
|
1434
1500
|
const query = `SELECT * FROM agent_tasks ${whereClause} ORDER BY lastUpdatedAt DESC, priority DESC LIMIT ${limit} OFFSET ${offset}`;
|
|
1435
1501
|
|
|
1436
|
-
|
|
1502
|
+
const rows = getDb()
|
|
1437
1503
|
.prepare<AgentTaskRow, (string | AgentTaskStatus)[]>(query)
|
|
1438
|
-
.all(...params)
|
|
1439
|
-
.map(rowToAgentTask);
|
|
1504
|
+
.all(...params);
|
|
1440
1505
|
|
|
1441
|
-
// Filter for ready tasks (dependencies met) if requested
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1506
|
+
// Filter for ready tasks (dependencies met) if requested. Both the full and
|
|
1507
|
+
// the slim row shapes carry `id` + `dependsOn`, so the same predicate works.
|
|
1508
|
+
const isReady = (task: { id: string; dependsOn: string[] }): boolean => {
|
|
1509
|
+
if (!task.dependsOn || task.dependsOn.length === 0) return true;
|
|
1510
|
+
return checkDependencies(task.id).ready;
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
if (opts?.slim) {
|
|
1514
|
+
let tasks = rows.map(rowToAgentTaskSummary);
|
|
1515
|
+
if (filters?.readyOnly) tasks = tasks.filter(isReady);
|
|
1516
|
+
return tasks;
|
|
1447
1517
|
}
|
|
1448
1518
|
|
|
1519
|
+
let tasks = rows.map(rowToAgentTask);
|
|
1520
|
+
if (filters?.readyOnly) tasks = tasks.filter(isReady);
|
|
1449
1521
|
return tasks;
|
|
1450
1522
|
}
|
|
1451
1523
|
|
|
@@ -2000,7 +2072,7 @@ export function getPausedTasksForAgent(agentId: string): AgentTask[] {
|
|
|
2000
2072
|
.prepare<AgentTaskRow, [string]>(
|
|
2001
2073
|
`SELECT * FROM agent_tasks
|
|
2002
2074
|
WHERE agentId = ? AND status = 'paused'
|
|
2003
|
-
ORDER BY createdAt ASC`,
|
|
2075
|
+
ORDER BY createdAt ASC, rowid ASC`,
|
|
2004
2076
|
)
|
|
2005
2077
|
.all(agentId);
|
|
2006
2078
|
return rows.map(rowToAgentTask);
|
|
@@ -2054,6 +2126,172 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
|
|
|
2054
2126
|
return row ? rowToAgentTask(row) : null;
|
|
2055
2127
|
}
|
|
2056
2128
|
|
|
2129
|
+
// ============================================================================
|
|
2130
|
+
// Task Attachments (Phase 1 — pointer-based artifacts)
|
|
2131
|
+
// ============================================================================
|
|
2132
|
+
//
|
|
2133
|
+
// Pointer-only attachments live in their own table; `agent_tasks` is
|
|
2134
|
+
// untouched. Append-only in Phase 1 — `insertTaskAttachment` silently no-ops
|
|
2135
|
+
// on a duplicate (sha256 match, or kind+pointer+name tuple match) so
|
|
2136
|
+
// idempotent re-calls don't fan out duplicate rows. The `kind` enum here
|
|
2137
|
+
// MUST stay in sync with the SQL CHECK constraint (migration 072) and the
|
|
2138
|
+
// `TaskAttachmentKindSchema` zod enum.
|
|
2139
|
+
|
|
2140
|
+
type TaskAttachmentRow = {
|
|
2141
|
+
id: string;
|
|
2142
|
+
task_id: string;
|
|
2143
|
+
agent_id: string | null;
|
|
2144
|
+
name: string;
|
|
2145
|
+
kind: string;
|
|
2146
|
+
url: string | null;
|
|
2147
|
+
path: string | null;
|
|
2148
|
+
page_id: string | null;
|
|
2149
|
+
mime_type: string | null;
|
|
2150
|
+
size_bytes: number | null;
|
|
2151
|
+
sha256: string | null;
|
|
2152
|
+
intent: string | null;
|
|
2153
|
+
description: string | null;
|
|
2154
|
+
is_primary: number;
|
|
2155
|
+
created_at: string;
|
|
2156
|
+
};
|
|
2157
|
+
|
|
2158
|
+
function rowToTaskAttachment(row: TaskAttachmentRow): TaskAttachment {
|
|
2159
|
+
return {
|
|
2160
|
+
id: row.id,
|
|
2161
|
+
taskId: row.task_id,
|
|
2162
|
+
agentId: row.agent_id,
|
|
2163
|
+
name: row.name,
|
|
2164
|
+
kind: row.kind as TaskAttachment["kind"],
|
|
2165
|
+
url: row.url ?? undefined,
|
|
2166
|
+
path: row.path ?? undefined,
|
|
2167
|
+
pageId: row.page_id ?? undefined,
|
|
2168
|
+
mimeType: row.mime_type ?? undefined,
|
|
2169
|
+
sizeBytes: row.size_bytes ?? undefined,
|
|
2170
|
+
sha256: row.sha256 ?? undefined,
|
|
2171
|
+
intent: row.intent ?? undefined,
|
|
2172
|
+
description: row.description ?? undefined,
|
|
2173
|
+
isPrimary: !!row.is_primary,
|
|
2174
|
+
createdAt: row.created_at,
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
export interface InsertTaskAttachmentInput {
|
|
2179
|
+
taskId: string;
|
|
2180
|
+
agentId: string | null;
|
|
2181
|
+
name: string;
|
|
2182
|
+
kind: TaskAttachment["kind"];
|
|
2183
|
+
url?: string;
|
|
2184
|
+
path?: string;
|
|
2185
|
+
pageId?: string;
|
|
2186
|
+
mimeType?: string;
|
|
2187
|
+
sizeBytes?: number;
|
|
2188
|
+
sha256?: string;
|
|
2189
|
+
intent?: string;
|
|
2190
|
+
description?: string;
|
|
2191
|
+
isPrimary?: boolean;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/**
|
|
2195
|
+
* Insert a task attachment. Append-only + dedup:
|
|
2196
|
+
* - if sha256 is present and a row for this task already has that sha256,
|
|
2197
|
+
* skip (return existing row);
|
|
2198
|
+
* - otherwise skip if a row exists for the same task with the same
|
|
2199
|
+
* (kind, path|url|page_id, name) tuple.
|
|
2200
|
+
* Returns the stored attachment (newly inserted or pre-existing duplicate).
|
|
2201
|
+
*/
|
|
2202
|
+
export function insertTaskAttachment(input: InsertTaskAttachmentInput): TaskAttachment {
|
|
2203
|
+
const db = getDb();
|
|
2204
|
+
|
|
2205
|
+
if (input.sha256) {
|
|
2206
|
+
const existing = db
|
|
2207
|
+
.prepare<TaskAttachmentRow, [string, string]>(
|
|
2208
|
+
"SELECT * FROM task_attachments WHERE task_id = ? AND sha256 = ? LIMIT 1",
|
|
2209
|
+
)
|
|
2210
|
+
.get(input.taskId, input.sha256);
|
|
2211
|
+
if (existing) return rowToTaskAttachment(existing);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const tupleExisting = db
|
|
2215
|
+
.prepare<TaskAttachmentRow, [string, string, string, string, string, string]>(
|
|
2216
|
+
`SELECT * FROM task_attachments
|
|
2217
|
+
WHERE task_id = ?
|
|
2218
|
+
AND kind = ?
|
|
2219
|
+
AND IFNULL(path, '') = ?
|
|
2220
|
+
AND IFNULL(url, '') = ?
|
|
2221
|
+
AND IFNULL(page_id, '') = ?
|
|
2222
|
+
AND name = ?
|
|
2223
|
+
ORDER BY created_at ASC
|
|
2224
|
+
LIMIT 1`,
|
|
2225
|
+
)
|
|
2226
|
+
.get(
|
|
2227
|
+
input.taskId,
|
|
2228
|
+
input.kind,
|
|
2229
|
+
input.path ?? "",
|
|
2230
|
+
input.url ?? "",
|
|
2231
|
+
input.pageId ?? "",
|
|
2232
|
+
input.name,
|
|
2233
|
+
);
|
|
2234
|
+
if (tupleExisting) return rowToTaskAttachment(tupleExisting);
|
|
2235
|
+
|
|
2236
|
+
const id = crypto.randomUUID();
|
|
2237
|
+
const row = db
|
|
2238
|
+
.prepare<
|
|
2239
|
+
TaskAttachmentRow,
|
|
2240
|
+
[
|
|
2241
|
+
string,
|
|
2242
|
+
string,
|
|
2243
|
+
string | null,
|
|
2244
|
+
string,
|
|
2245
|
+
string,
|
|
2246
|
+
string | null,
|
|
2247
|
+
string | null,
|
|
2248
|
+
string | null,
|
|
2249
|
+
string | null,
|
|
2250
|
+
number | null,
|
|
2251
|
+
string | null,
|
|
2252
|
+
string | null,
|
|
2253
|
+
string | null,
|
|
2254
|
+
number,
|
|
2255
|
+
]
|
|
2256
|
+
>(
|
|
2257
|
+
`INSERT INTO task_attachments
|
|
2258
|
+
(id, task_id, agent_id, name, kind, url, path, page_id,
|
|
2259
|
+
mime_type, size_bytes, sha256, intent, description, is_primary)
|
|
2260
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2261
|
+
RETURNING *`,
|
|
2262
|
+
)
|
|
2263
|
+
.get(
|
|
2264
|
+
id,
|
|
2265
|
+
input.taskId,
|
|
2266
|
+
input.agentId ?? null,
|
|
2267
|
+
input.name,
|
|
2268
|
+
input.kind,
|
|
2269
|
+
input.url ?? null,
|
|
2270
|
+
input.path ?? null,
|
|
2271
|
+
input.pageId ?? null,
|
|
2272
|
+
input.mimeType ?? null,
|
|
2273
|
+
input.sizeBytes ?? null,
|
|
2274
|
+
input.sha256 ?? null,
|
|
2275
|
+
input.intent ?? null,
|
|
2276
|
+
input.description ?? null,
|
|
2277
|
+
input.isPrimary ? 1 : 0,
|
|
2278
|
+
);
|
|
2279
|
+
|
|
2280
|
+
if (!row) {
|
|
2281
|
+
throw new Error("Failed to insert task attachment");
|
|
2282
|
+
}
|
|
2283
|
+
return rowToTaskAttachment(row);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
export function getTaskAttachments(taskId: string): TaskAttachment[] {
|
|
2287
|
+
return getDb()
|
|
2288
|
+
.prepare<TaskAttachmentRow, [string]>(
|
|
2289
|
+
"SELECT * FROM task_attachments WHERE task_id = ? ORDER BY created_at ASC, rowid ASC",
|
|
2290
|
+
)
|
|
2291
|
+
.all(taskId)
|
|
2292
|
+
.map(rowToTaskAttachment);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2057
2295
|
// ============================================================================
|
|
2058
2296
|
// Combined Queries (Agent with Tasks)
|
|
2059
2297
|
// ============================================================================
|
|
@@ -2070,9 +2308,9 @@ export function getAgentWithTasks(id: string): AgentWithTasks | null {
|
|
|
2070
2308
|
return txn();
|
|
2071
2309
|
}
|
|
2072
2310
|
|
|
2073
|
-
export function getAllAgentsWithTasks(): AgentWithTasks[] {
|
|
2311
|
+
export function getAllAgentsWithTasks(opts?: { slim?: boolean }): AgentWithTasks[] {
|
|
2074
2312
|
const txn = getDb().transaction(() => {
|
|
2075
|
-
const agents = getAllAgents();
|
|
2313
|
+
const agents = getAllAgents({ slim: opts?.slim ?? false });
|
|
2076
2314
|
return agents.map((agent) => ({
|
|
2077
2315
|
...agent,
|
|
2078
2316
|
tasks: getTasksByAgentId(agent.id),
|
|
@@ -2166,8 +2404,13 @@ export function getLogsByAgentId(agentId: string): AgentLog[] {
|
|
|
2166
2404
|
return logQueries.getByAgentId().all(agentId).map(rowToAgentLog);
|
|
2167
2405
|
}
|
|
2168
2406
|
|
|
2169
|
-
export function getLogsByTaskId(taskId: string): AgentLog[] {
|
|
2170
|
-
return
|
|
2407
|
+
export function getLogsByTaskId(taskId: string, limit = 200): AgentLog[] {
|
|
2408
|
+
return getDb()
|
|
2409
|
+
.prepare<AgentLogRow, [string, number]>(
|
|
2410
|
+
"SELECT * FROM agent_log WHERE taskId = ? ORDER BY createdAt DESC LIMIT ?",
|
|
2411
|
+
)
|
|
2412
|
+
.all(taskId, limit)
|
|
2413
|
+
.map(rowToAgentLog);
|
|
2171
2414
|
}
|
|
2172
2415
|
|
|
2173
2416
|
export function getLogsByTaskIdChronological(taskId: string): AgentLog[] {
|
|
@@ -2629,7 +2872,7 @@ export function releaseStaleReviewingTasks(timeoutMinutes: number = 30): number
|
|
|
2629
2872
|
export function getOfferedTasksForAgent(agentId: string): AgentTask[] {
|
|
2630
2873
|
return getDb()
|
|
2631
2874
|
.prepare<AgentTaskRow, [string]>(
|
|
2632
|
-
"SELECT * FROM agent_tasks WHERE offeredTo = ? AND status = 'offered' ORDER BY createdAt ASC",
|
|
2875
|
+
"SELECT * FROM agent_tasks WHERE offeredTo = ? AND status = 'offered' ORDER BY createdAt ASC, rowid ASC",
|
|
2633
2876
|
)
|
|
2634
2877
|
.all(agentId)
|
|
2635
2878
|
.map(rowToAgentTask);
|
|
@@ -2682,7 +2925,7 @@ export function getUnassignedTasksCount(): number {
|
|
|
2682
2925
|
export function getUnassignedTaskIds(limit = 10): string[] {
|
|
2683
2926
|
const rows = getDb()
|
|
2684
2927
|
.prepare<{ id: string }, [number]>(
|
|
2685
|
-
"SELECT id FROM agent_tasks WHERE status = 'unassigned' ORDER BY priority DESC, createdAt ASC LIMIT ?",
|
|
2928
|
+
"SELECT id FROM agent_tasks WHERE status = 'unassigned' ORDER BY priority DESC, createdAt ASC, rowid ASC LIMIT ?",
|
|
2686
2929
|
)
|
|
2687
2930
|
.all(limit);
|
|
2688
2931
|
return rows.map((r) => r.id);
|
|
@@ -4477,6 +4720,21 @@ type ScheduledTaskRow = {
|
|
|
4477
4720
|
lastUpdatedAt: string;
|
|
4478
4721
|
};
|
|
4479
4722
|
|
|
4723
|
+
// ── List-endpoint slimming helpers ──────────────────────────────────────────
|
|
4724
|
+
// List endpoints ship slim rows by default; heavy text fields are replaced
|
|
4725
|
+
// with bounded previews. Lengths are generous enough for triage/recognition
|
|
4726
|
+
// while keeping list payloads small.
|
|
4727
|
+
/** Preview length for a schedule's `taskTemplate`. */
|
|
4728
|
+
const SCHEDULE_TEMPLATE_PREVIEW_LENGTH = 280;
|
|
4729
|
+
/** Preview length for a task's `task` text (pool-triage needs to read it). */
|
|
4730
|
+
const TASK_PREVIEW_LENGTH = 300;
|
|
4731
|
+
|
|
4732
|
+
/** Truncate text for a list-row preview. Appends an ellipsis when clipped. */
|
|
4733
|
+
function previewText(text: string | null | undefined, maxChars: number): string {
|
|
4734
|
+
const s = text ?? "";
|
|
4735
|
+
return s.length > maxChars ? `${s.slice(0, maxChars)}…` : s;
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4480
4738
|
function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
|
|
4481
4739
|
return {
|
|
4482
4740
|
id: row.id,
|
|
@@ -4511,7 +4769,28 @@ export interface ScheduledTaskFilters {
|
|
|
4511
4769
|
hideCompleted?: boolean;
|
|
4512
4770
|
}
|
|
4513
4771
|
|
|
4514
|
-
|
|
4772
|
+
/**
|
|
4773
|
+
* Slim list-row mapper — replaces the full `taskTemplate` (the per-run prompt,
|
|
4774
|
+
* avg ~3.6 KB) with a bounded `taskTemplatePreview`. Fetch the full template
|
|
4775
|
+
* via `getScheduledTaskById(id)`.
|
|
4776
|
+
*/
|
|
4777
|
+
function rowToScheduledTaskSummary(row: ScheduledTaskRow): ScheduledTaskSummary {
|
|
4778
|
+
const { taskTemplate, ...rest } = rowToScheduledTask(row);
|
|
4779
|
+
return {
|
|
4780
|
+
...rest,
|
|
4781
|
+
taskTemplatePreview: previewText(taskTemplate, SCHEDULE_TEMPLATE_PREVIEW_LENGTH),
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask[];
|
|
4786
|
+
export function getScheduledTasks(
|
|
4787
|
+
filters: ScheduledTaskFilters | undefined,
|
|
4788
|
+
opts: { slim: true },
|
|
4789
|
+
): ScheduledTaskSummary[];
|
|
4790
|
+
export function getScheduledTasks(
|
|
4791
|
+
filters?: ScheduledTaskFilters,
|
|
4792
|
+
opts?: { slim?: boolean },
|
|
4793
|
+
): ScheduledTask[] | ScheduledTaskSummary[] {
|
|
4515
4794
|
let query = "SELECT * FROM scheduled_tasks WHERE 1=1";
|
|
4516
4795
|
const params: (string | number)[] = [];
|
|
4517
4796
|
|
|
@@ -4536,10 +4815,10 @@ export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask
|
|
|
4536
4815
|
|
|
4537
4816
|
query += " ORDER BY name ASC";
|
|
4538
4817
|
|
|
4539
|
-
|
|
4818
|
+
const rows = getDb()
|
|
4540
4819
|
.prepare<ScheduledTaskRow, (string | number)[]>(query)
|
|
4541
|
-
.all(...params)
|
|
4542
|
-
|
|
4820
|
+
.all(...params);
|
|
4821
|
+
return opts?.slim ? rows.map(rowToScheduledTaskSummary) : rows.map(rowToScheduledTask);
|
|
4543
4822
|
}
|
|
4544
4823
|
|
|
4545
4824
|
export function getScheduledTaskById(id: string): ScheduledTask | null {
|
|
@@ -5580,7 +5859,7 @@ export function getIdleWorkersWithCapacity(): Agent[] {
|
|
|
5580
5859
|
WHERE status = 'idle' AND isLead = 0`,
|
|
5581
5860
|
)
|
|
5582
5861
|
.all()
|
|
5583
|
-
.map(rowToAgent);
|
|
5862
|
+
.map((row) => rowToAgent(row));
|
|
5584
5863
|
|
|
5585
5864
|
return agents.filter((agent) => {
|
|
5586
5865
|
const activeCount = getActiveTaskCount(agent.id);
|
|
@@ -5739,7 +6018,43 @@ export function getWorkflow(id: string): Workflow | null {
|
|
|
5739
6018
|
return row ? rowToWorkflow(row) : null;
|
|
5740
6019
|
}
|
|
5741
6020
|
|
|
5742
|
-
|
|
6021
|
+
/**
|
|
6022
|
+
* Slim list-row mapper — drops the heavy `definition` (avg ~18 KB/row) and the
|
|
6023
|
+
* trigger config, keeping a derived `nodeCount` so the list view can still
|
|
6024
|
+
* answer "how big is this workflow" without the full DAG. Fetch the full shape
|
|
6025
|
+
* via `getWorkflow(id)`.
|
|
6026
|
+
*/
|
|
6027
|
+
function rowToWorkflowSummary(row: WorkflowRow): WorkflowSummary {
|
|
6028
|
+
let nodeCount = 0;
|
|
6029
|
+
try {
|
|
6030
|
+
const def = JSON.parse(row.definition) as WorkflowDefinition;
|
|
6031
|
+
nodeCount = Array.isArray(def?.nodes) ? def.nodes.length : 0;
|
|
6032
|
+
} catch {
|
|
6033
|
+
nodeCount = 0;
|
|
6034
|
+
}
|
|
6035
|
+
return {
|
|
6036
|
+
id: row.id,
|
|
6037
|
+
name: row.name,
|
|
6038
|
+
description: row.description ?? undefined,
|
|
6039
|
+
enabled: row.enabled === 1,
|
|
6040
|
+
dir: row.dir ?? undefined,
|
|
6041
|
+
vcsRepo: row.vcs_repo ?? undefined,
|
|
6042
|
+
createdByAgentId: row.createdByAgentId ?? undefined,
|
|
6043
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
6044
|
+
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
6045
|
+
nodeCount,
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
6048
|
+
|
|
6049
|
+
export function listWorkflows(filters?: { enabled?: boolean }): Workflow[];
|
|
6050
|
+
export function listWorkflows(
|
|
6051
|
+
filters: { enabled?: boolean } | undefined,
|
|
6052
|
+
opts: { slim: true },
|
|
6053
|
+
): WorkflowSummary[];
|
|
6054
|
+
export function listWorkflows(
|
|
6055
|
+
filters?: { enabled?: boolean },
|
|
6056
|
+
opts?: { slim?: boolean },
|
|
6057
|
+
): Workflow[] | WorkflowSummary[] {
|
|
5743
6058
|
let query = "SELECT * FROM workflows WHERE 1=1";
|
|
5744
6059
|
const params: (string | number)[] = [];
|
|
5745
6060
|
if (filters?.enabled !== undefined) {
|
|
@@ -5747,10 +6062,10 @@ export function listWorkflows(filters?: { enabled?: boolean }): Workflow[] {
|
|
|
5747
6062
|
params.push(filters.enabled ? 1 : 0);
|
|
5748
6063
|
}
|
|
5749
6064
|
query += " ORDER BY name ASC";
|
|
5750
|
-
|
|
6065
|
+
const rows = getDb()
|
|
5751
6066
|
.prepare<WorkflowRow, (string | number)[]>(query)
|
|
5752
|
-
.all(...params)
|
|
5753
|
-
|
|
6067
|
+
.all(...params);
|
|
6068
|
+
return opts?.slim ? rows.map(rowToWorkflowSummary) : rows.map(rowToWorkflow);
|
|
5754
6069
|
}
|
|
5755
6070
|
|
|
5756
6071
|
export function updateWorkflow(
|
|
@@ -6378,22 +6693,84 @@ export function getPageBySlug(agentId: string, slug: string): Page | null {
|
|
|
6378
6693
|
return row ? rowToPage(row) : null;
|
|
6379
6694
|
}
|
|
6380
6695
|
|
|
6381
|
-
|
|
6382
|
-
|
|
6696
|
+
/**
|
|
6697
|
+
* Slim list-row mapper — drops the page `body` (the full HTML/JSON document,
|
|
6698
|
+
* up to ~290 KB and ~95% of a list payload) and `passwordHash`. Fetch the
|
|
6699
|
+
* full page via `getPage(id)`.
|
|
6700
|
+
*/
|
|
6701
|
+
function rowToPageSummary(row: PageRow): PageSummary {
|
|
6702
|
+
return {
|
|
6703
|
+
id: row.id,
|
|
6704
|
+
agentId: row.agentId,
|
|
6705
|
+
slug: row.slug,
|
|
6706
|
+
title: row.title,
|
|
6707
|
+
description: row.description ?? undefined,
|
|
6708
|
+
contentType: row.contentType as PageContentType,
|
|
6709
|
+
authMode: row.authMode as PageAuthMode,
|
|
6710
|
+
needsCredentials: row.needsCredentials
|
|
6711
|
+
? (JSON.parse(row.needsCredentials) as string[])
|
|
6712
|
+
: undefined,
|
|
6713
|
+
viewCount: typeof row.view_count === "number" ? row.view_count : 0,
|
|
6714
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
6715
|
+
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
6716
|
+
};
|
|
6717
|
+
}
|
|
6718
|
+
|
|
6719
|
+
export function listPagesByAgent(agentId: string, limit?: number, offset?: number): Page[];
|
|
6720
|
+
export function listPagesByAgent(
|
|
6721
|
+
agentId: string,
|
|
6722
|
+
limit: number | undefined,
|
|
6723
|
+
offset: number | undefined,
|
|
6724
|
+
opts: { slim: true },
|
|
6725
|
+
): PageSummary[];
|
|
6726
|
+
export function listPagesByAgent(
|
|
6727
|
+
agentId: string,
|
|
6728
|
+
limit = 100,
|
|
6729
|
+
offset = 0,
|
|
6730
|
+
opts?: { slim?: boolean },
|
|
6731
|
+
): Page[] | PageSummary[] {
|
|
6732
|
+
const rows = getDb()
|
|
6383
6733
|
.prepare<PageRow, [string, number, number]>(
|
|
6384
6734
|
"SELECT * FROM pages WHERE agentId = ? ORDER BY updatedAt DESC LIMIT ? OFFSET ?",
|
|
6385
6735
|
)
|
|
6386
|
-
.all(agentId, limit, offset)
|
|
6387
|
-
|
|
6388
|
-
}
|
|
6389
|
-
|
|
6390
|
-
export function listAllPages(limit
|
|
6391
|
-
|
|
6736
|
+
.all(agentId, limit, offset);
|
|
6737
|
+
return opts?.slim ? rows.map(rowToPageSummary) : rows.map(rowToPage);
|
|
6738
|
+
}
|
|
6739
|
+
|
|
6740
|
+
export function listAllPages(limit?: number, offset?: number): Page[];
|
|
6741
|
+
export function listAllPages(
|
|
6742
|
+
limit: number | undefined,
|
|
6743
|
+
offset: number | undefined,
|
|
6744
|
+
opts: { slim: true },
|
|
6745
|
+
): PageSummary[];
|
|
6746
|
+
export function listAllPages(
|
|
6747
|
+
limit = 100,
|
|
6748
|
+
offset = 0,
|
|
6749
|
+
opts?: { slim?: boolean },
|
|
6750
|
+
): Page[] | PageSummary[] {
|
|
6751
|
+
const rows = getDb()
|
|
6392
6752
|
.prepare<PageRow, [number, number]>(
|
|
6393
6753
|
"SELECT * FROM pages ORDER BY updatedAt DESC LIMIT ? OFFSET ?",
|
|
6394
6754
|
)
|
|
6395
|
-
.all(limit, offset)
|
|
6396
|
-
|
|
6755
|
+
.all(limit, offset);
|
|
6756
|
+
return opts?.slim ? rows.map(rowToPageSummary) : rows.map(rowToPage);
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
/**
|
|
6760
|
+
* Total page count — used to back a filter-aware `total` in the `/api/pages`
|
|
6761
|
+
* pager so the UI shows the real count, not just the current page's length.
|
|
6762
|
+
*/
|
|
6763
|
+
export function countAllPages(): number {
|
|
6764
|
+
const row = getDb().prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM pages").get();
|
|
6765
|
+
return row?.count ?? 0;
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
/** Page count scoped to a single agent — companion to `listPagesByAgent`. */
|
|
6769
|
+
export function countPagesByAgent(agentId: string): number {
|
|
6770
|
+
const row = getDb()
|
|
6771
|
+
.prepare<{ count: number }, [string]>("SELECT COUNT(*) AS count FROM pages WHERE agentId = ?")
|
|
6772
|
+
.get(agentId);
|
|
6773
|
+
return row?.count ?? 0;
|
|
6397
6774
|
}
|
|
6398
6775
|
|
|
6399
6776
|
/**
|
|
@@ -7842,11 +8219,16 @@ export interface SkillFilters {
|
|
|
7842
8219
|
includeContent?: boolean;
|
|
7843
8220
|
}
|
|
7844
8221
|
|
|
8222
|
+
/**
|
|
8223
|
+
* Explicit column list used when `includeContent: false` — selects every
|
|
8224
|
+
* skill column except the heavy `content` (the full SKILL.md, avg ~10 KB),
|
|
8225
|
+
* which is replaced with an empty string so the row still satisfies `Skill`.
|
|
8226
|
+
*/
|
|
8227
|
+
const SKILL_SLIM_COLUMNS =
|
|
8228
|
+
"id, name, description, type, scope, ownerAgentId, sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex, allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable, version, isEnabled, createdAt, lastUpdatedAt, lastFetchedAt, '' as content";
|
|
8229
|
+
|
|
7845
8230
|
export function listSkills(filters?: SkillFilters): Skill[] {
|
|
7846
|
-
const columns =
|
|
7847
|
-
filters?.includeContent === false
|
|
7848
|
-
? "id, name, description, type, scope, ownerAgentId, sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex, allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable, version, isEnabled, createdAt, lastUpdatedAt, lastFetchedAt, '' as content"
|
|
7849
|
-
: "*";
|
|
8231
|
+
const columns = filters?.includeContent === false ? SKILL_SLIM_COLUMNS : "*";
|
|
7850
8232
|
let query = `SELECT ${columns} FROM skills WHERE 1=1`;
|
|
7851
8233
|
const params: (string | number)[] = [];
|
|
7852
8234
|
|
|
@@ -7885,11 +8267,12 @@ export function listSkills(filters?: SkillFilters): Skill[] {
|
|
|
7885
8267
|
.map(rowToSkill);
|
|
7886
8268
|
}
|
|
7887
8269
|
|
|
7888
|
-
export function searchSkills(query: string, limit = 20): Skill[] {
|
|
8270
|
+
export function searchSkills(query: string, limit = 20, includeContent = true): Skill[] {
|
|
7889
8271
|
const term = `%${query}%`;
|
|
8272
|
+
const columns = includeContent === false ? SKILL_SLIM_COLUMNS : "*";
|
|
7890
8273
|
return getDb()
|
|
7891
8274
|
.prepare<SkillRow, [string, string, number]>(
|
|
7892
|
-
|
|
8275
|
+
`SELECT ${columns} FROM skills WHERE (name LIKE ? OR description LIKE ?) AND isEnabled = 1 ORDER BY name ASC LIMIT ?`,
|
|
7893
8276
|
)
|
|
7894
8277
|
.all(term, term, limit)
|
|
7895
8278
|
.map(rowToSkill);
|
|
@@ -9094,27 +9477,56 @@ export interface SessionListItem {
|
|
|
9094
9477
|
latestStatus: AgentTaskStatus;
|
|
9095
9478
|
}
|
|
9096
9479
|
|
|
9480
|
+
/**
|
|
9481
|
+
* Slim variant of {@link SessionListItem} — the `root` task is an
|
|
9482
|
+
* `AgentTaskSummary` (full `task` text + completion/integration blobs dropped).
|
|
9483
|
+
* The session list only renders a brief of the root; the full root + chain are
|
|
9484
|
+
* on `GET /api/sessions/{rootTaskId}`.
|
|
9485
|
+
*/
|
|
9486
|
+
export interface SessionListItemSummary {
|
|
9487
|
+
root: AgentTaskSummary;
|
|
9488
|
+
chainTaskCount: number;
|
|
9489
|
+
lastActivityAt: string;
|
|
9490
|
+
latestStatus: AgentTaskStatus;
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9097
9493
|
/**
|
|
9098
9494
|
* List the most recent sessions ordered by chain-wide latest activity.
|
|
9099
9495
|
* A "session" here is any task with `parentTaskId IS NULL` — its descendants
|
|
9100
9496
|
* (children, grand-children, …) are summarized via the recursive CTE.
|
|
9101
9497
|
*
|
|
9102
|
-
*
|
|
9103
|
-
*
|
|
9104
|
-
*
|
|
9498
|
+
* Single-pass CTE: seeds with root tasks matching the filter, walks the full
|
|
9499
|
+
* descendant tree once, then aggregates chainCount / lastActivityAt /
|
|
9500
|
+
* latestStatus in two lightweight non-recursive CTEs — replacing the original
|
|
9501
|
+
* pattern of 3 correlated subqueries each re-running the recursion per row.
|
|
9105
9502
|
*/
|
|
9106
|
-
|
|
9503
|
+
interface ListRecentSessionsOpts {
|
|
9107
9504
|
limit?: number;
|
|
9108
9505
|
offset?: number;
|
|
9109
9506
|
/** Filter to root tasks whose `source` is in this list. Empty/undefined → no source filter. */
|
|
9110
9507
|
source?: string[];
|
|
9111
9508
|
/** Case-insensitive substring match against `r.task`. */
|
|
9112
9509
|
q?: string;
|
|
9113
|
-
|
|
9510
|
+
/** When set, restrict to root tasks where `requestedByUserId` equals this value. NULL rows are excluded. */
|
|
9511
|
+
requestedByUserId?: string;
|
|
9512
|
+
/** When true, return slim `SessionListItemSummary` rows (default: full). */
|
|
9513
|
+
slim?: boolean;
|
|
9514
|
+
}
|
|
9515
|
+
|
|
9516
|
+
export function listRecentSessions(
|
|
9517
|
+
opts?: ListRecentSessionsOpts & { slim?: false },
|
|
9518
|
+
): SessionListItem[];
|
|
9519
|
+
export function listRecentSessions(
|
|
9520
|
+
opts: ListRecentSessionsOpts & { slim: true },
|
|
9521
|
+
): SessionListItemSummary[];
|
|
9522
|
+
export function listRecentSessions(
|
|
9523
|
+
opts?: ListRecentSessionsOpts,
|
|
9524
|
+
): SessionListItem[] | SessionListItemSummary[] {
|
|
9114
9525
|
const limit = opts?.limit ?? 25;
|
|
9115
9526
|
const offset = opts?.offset ?? 0;
|
|
9116
9527
|
const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
|
|
9117
9528
|
const q = opts?.q?.trim();
|
|
9529
|
+
const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
|
|
9118
9530
|
|
|
9119
9531
|
const conditions: string[] = ["r.parentTaskId IS NULL"];
|
|
9120
9532
|
const params: (string | number)[] = [];
|
|
@@ -9127,6 +9539,10 @@ export function listRecentSessions(opts?: {
|
|
|
9127
9539
|
conditions.push("lower(r.task) LIKE ?");
|
|
9128
9540
|
params.push(`%${q.toLowerCase()}%`);
|
|
9129
9541
|
}
|
|
9542
|
+
if (requestedByUserId) {
|
|
9543
|
+
conditions.push("r.requestedByUserId = ?");
|
|
9544
|
+
params.push(requestedByUserId);
|
|
9545
|
+
}
|
|
9130
9546
|
params.push(limit, offset);
|
|
9131
9547
|
|
|
9132
9548
|
const rootRows = getDb()
|
|
@@ -9134,48 +9550,55 @@ export function listRecentSessions(opts?: {
|
|
|
9134
9550
|
AgentTaskRow & { __chainCount: number; __lastActivityAt: string; __latestStatus: string },
|
|
9135
9551
|
typeof params
|
|
9136
9552
|
>(
|
|
9137
|
-
`
|
|
9553
|
+
`WITH RECURSIVE chain(root_id, id, lastUpdatedAt, status) AS (
|
|
9554
|
+
SELECT r.id, r.id, r.lastUpdatedAt, r.status
|
|
9555
|
+
FROM agent_tasks r
|
|
9556
|
+
WHERE ${conditions.join(" AND ")}
|
|
9557
|
+
UNION ALL
|
|
9558
|
+
SELECT c.root_id, t.id, t.lastUpdatedAt, t.status
|
|
9559
|
+
FROM agent_tasks t
|
|
9560
|
+
JOIN chain c ON t.parentTaskId = c.id
|
|
9561
|
+
),
|
|
9562
|
+
agg AS (
|
|
9563
|
+
SELECT
|
|
9564
|
+
root_id,
|
|
9565
|
+
COUNT(*) AS chainCount,
|
|
9566
|
+
MAX(lastUpdatedAt) AS lastActivityAt
|
|
9567
|
+
FROM chain
|
|
9568
|
+
GROUP BY root_id
|
|
9569
|
+
),
|
|
9570
|
+
latest_status AS (
|
|
9571
|
+
SELECT c.root_id, c.status AS latestStatus
|
|
9572
|
+
FROM chain c
|
|
9573
|
+
JOIN agg a ON c.root_id = a.root_id AND c.lastUpdatedAt = a.lastActivityAt
|
|
9574
|
+
GROUP BY c.root_id
|
|
9575
|
+
)
|
|
9576
|
+
SELECT
|
|
9138
9577
|
r.*,
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
SELECT r.id
|
|
9143
|
-
UNION ALL
|
|
9144
|
-
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
9145
|
-
)
|
|
9146
|
-
SELECT id FROM chain
|
|
9147
|
-
)
|
|
9148
|
-
) AS __chainCount,
|
|
9149
|
-
(SELECT MAX(d.lastUpdatedAt) FROM agent_tasks d
|
|
9150
|
-
WHERE d.id IN (
|
|
9151
|
-
WITH RECURSIVE chain(id) AS (
|
|
9152
|
-
SELECT r.id
|
|
9153
|
-
UNION ALL
|
|
9154
|
-
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
9155
|
-
)
|
|
9156
|
-
SELECT id FROM chain
|
|
9157
|
-
)
|
|
9158
|
-
) AS __lastActivityAt,
|
|
9159
|
-
(SELECT d.status FROM agent_tasks d
|
|
9160
|
-
WHERE d.id IN (
|
|
9161
|
-
WITH RECURSIVE chain(id) AS (
|
|
9162
|
-
SELECT r.id
|
|
9163
|
-
UNION ALL
|
|
9164
|
-
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
9165
|
-
)
|
|
9166
|
-
SELECT id FROM chain
|
|
9167
|
-
)
|
|
9168
|
-
ORDER BY d.lastUpdatedAt DESC
|
|
9169
|
-
LIMIT 1
|
|
9170
|
-
) AS __latestStatus
|
|
9578
|
+
a.chainCount AS __chainCount,
|
|
9579
|
+
a.lastActivityAt AS __lastActivityAt,
|
|
9580
|
+
COALESCE(ls.latestStatus, r.status) AS __latestStatus
|
|
9171
9581
|
FROM agent_tasks r
|
|
9172
|
-
|
|
9173
|
-
|
|
9582
|
+
JOIN agg a ON a.root_id = r.id
|
|
9583
|
+
LEFT JOIN latest_status ls ON ls.root_id = r.id
|
|
9584
|
+
ORDER BY a.lastActivityAt DESC
|
|
9174
9585
|
LIMIT ? OFFSET ?`,
|
|
9175
9586
|
)
|
|
9176
9587
|
.all(...params);
|
|
9177
9588
|
|
|
9178
|
-
|
|
9589
|
+
if (opts?.slim) {
|
|
9590
|
+
return rootRows.map((row): SessionListItemSummary => {
|
|
9591
|
+
const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
|
|
9592
|
+
return {
|
|
9593
|
+
root: rowToAgentTaskSummary(taskRow as AgentTaskRow),
|
|
9594
|
+
chainTaskCount: __chainCount,
|
|
9595
|
+
lastActivityAt: __lastActivityAt ?? row.lastUpdatedAt,
|
|
9596
|
+
latestStatus: (__latestStatus as AgentTaskStatus) ?? row.status,
|
|
9597
|
+
};
|
|
9598
|
+
});
|
|
9599
|
+
}
|
|
9600
|
+
|
|
9601
|
+
return rootRows.map((row): SessionListItem => {
|
|
9179
9602
|
const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
|
|
9180
9603
|
return {
|
|
9181
9604
|
root: rowToAgentTask(taskRow as AgentTaskRow),
|
|
@@ -9186,6 +9609,43 @@ export function listRecentSessions(opts?: {
|
|
|
9186
9609
|
});
|
|
9187
9610
|
}
|
|
9188
9611
|
|
|
9612
|
+
/**
|
|
9613
|
+
* Filter-aware count of sessions (root tasks) matching the same `source` / `q`
|
|
9614
|
+
* / `requestedByUserId` filters as `listRecentSessions`. Powers a correct
|
|
9615
|
+
* `total` in the `/api/sessions` pager — a session is a root task, so this is
|
|
9616
|
+
* a plain count, no recursive chain walk needed.
|
|
9617
|
+
*/
|
|
9618
|
+
export function countSessions(
|
|
9619
|
+
opts?: Pick<ListRecentSessionsOpts, "source" | "q" | "requestedByUserId">,
|
|
9620
|
+
): number {
|
|
9621
|
+
const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
|
|
9622
|
+
const q = opts?.q?.trim();
|
|
9623
|
+
const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
|
|
9624
|
+
|
|
9625
|
+
const conditions: string[] = ["parentTaskId IS NULL"];
|
|
9626
|
+
const params: string[] = [];
|
|
9627
|
+
|
|
9628
|
+
if (sources.length > 0) {
|
|
9629
|
+
conditions.push(`source IN (${sources.map(() => "?").join(", ")})`);
|
|
9630
|
+
params.push(...sources);
|
|
9631
|
+
}
|
|
9632
|
+
if (q && q.length > 0) {
|
|
9633
|
+
conditions.push("lower(task) LIKE ?");
|
|
9634
|
+
params.push(`%${q.toLowerCase()}%`);
|
|
9635
|
+
}
|
|
9636
|
+
if (requestedByUserId) {
|
|
9637
|
+
conditions.push("requestedByUserId = ?");
|
|
9638
|
+
params.push(requestedByUserId);
|
|
9639
|
+
}
|
|
9640
|
+
|
|
9641
|
+
const row = getDb()
|
|
9642
|
+
.prepare<{ count: number }, string[]>(
|
|
9643
|
+
`SELECT COUNT(*) AS count FROM agent_tasks WHERE ${conditions.join(" AND ")}`,
|
|
9644
|
+
)
|
|
9645
|
+
.get(...params);
|
|
9646
|
+
return row?.count ?? 0;
|
|
9647
|
+
}
|
|
9648
|
+
|
|
9189
9649
|
// ============================================================================
|
|
9190
9650
|
// Budgets, daily-spend aggregation, and budget-refusal notifications (Phase 2)
|
|
9191
9651
|
// ----------------------------------------------------------------------------
|
|
@@ -9678,6 +10138,60 @@ export function getInstanceActivity(): {
|
|
|
9678
10138
|
};
|
|
9679
10139
|
}
|
|
9680
10140
|
|
|
10141
|
+
export interface SwarmMetrics {
|
|
10142
|
+
tasks: { total: number; by_status: Record<string, number> };
|
|
10143
|
+
agents: { total: number; by_status: Record<string, number> };
|
|
10144
|
+
workflows: { total: number; enabled: number };
|
|
10145
|
+
pages: { total: number };
|
|
10146
|
+
sessions: { active: number };
|
|
10147
|
+
skills: { total: number };
|
|
10148
|
+
}
|
|
10149
|
+
|
|
10150
|
+
/**
|
|
10151
|
+
* Lightweight swarm-wide counts for UI footers/sidebars and MCP context —
|
|
10152
|
+
* a single object so callers never have to fetch full list payloads just to
|
|
10153
|
+
* count. Pure `COUNT(*)` / `GROUP BY` queries; the `agent_tasks` status
|
|
10154
|
+
* grouping rides the indexes added in migration 069.
|
|
10155
|
+
*/
|
|
10156
|
+
export function getSwarmMetrics(): SwarmMetrics {
|
|
10157
|
+
const db = getDb();
|
|
10158
|
+
|
|
10159
|
+
const groupCounts = (table: string): { total: number; by_status: Record<string, number> } => {
|
|
10160
|
+
const rows = db
|
|
10161
|
+
.prepare<{ status: string; count: number }, []>(
|
|
10162
|
+
`SELECT status, COUNT(*) AS count FROM ${table} GROUP BY status`,
|
|
10163
|
+
)
|
|
10164
|
+
.all();
|
|
10165
|
+
const by_status: Record<string, number> = {};
|
|
10166
|
+
let total = 0;
|
|
10167
|
+
for (const r of rows) {
|
|
10168
|
+
by_status[r.status] = r.count;
|
|
10169
|
+
total += r.count;
|
|
10170
|
+
}
|
|
10171
|
+
return { total, by_status };
|
|
10172
|
+
};
|
|
10173
|
+
|
|
10174
|
+
const workflowRow = db
|
|
10175
|
+
.prepare<{ total: number; enabled: number }, []>(
|
|
10176
|
+
"SELECT COUNT(*) AS total, SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) AS enabled FROM workflows",
|
|
10177
|
+
)
|
|
10178
|
+
.get();
|
|
10179
|
+
const pagesRow = db.prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM pages").get();
|
|
10180
|
+
const sessionsRow = db
|
|
10181
|
+
.prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM active_sessions")
|
|
10182
|
+
.get();
|
|
10183
|
+
const skillsRow = db.prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM skills").get();
|
|
10184
|
+
|
|
10185
|
+
return {
|
|
10186
|
+
tasks: groupCounts("agent_tasks"),
|
|
10187
|
+
agents: groupCounts("agents"),
|
|
10188
|
+
workflows: { total: workflowRow?.total ?? 0, enabled: workflowRow?.enabled ?? 0 },
|
|
10189
|
+
pages: { total: pagesRow?.count ?? 0 },
|
|
10190
|
+
sessions: { active: sessionsRow?.count ?? 0 },
|
|
10191
|
+
skills: { total: skillsRow?.count ?? 0 },
|
|
10192
|
+
};
|
|
10193
|
+
}
|
|
10194
|
+
|
|
9681
10195
|
/**
|
|
9682
10196
|
* `first_task` milestone: true once any task has reached `status = 'completed'`.
|
|
9683
10197
|
* Cheap LIMIT 1 probe; the row's contents don't matter, only existence.
|