@desplega.ai/agent-swarm 1.81.1 → 1.83.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 +127 -3
- package/package.json +3 -2
- package/src/be/db.ts +616 -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/migrations/073_task_attachments_agent_fs_ids.sql +15 -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/provider-credentials.ts +11 -0
- package/src/commands/runner.ts +133 -44
- 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 +24 -3
- package/src/http/skills.ts +8 -1
- package/src/http/stats.ts +19 -0
- package/src/http/tasks.ts +20 -5
- 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/pi-mono-adapter.ts +20 -3
- package/src/providers/types.ts +10 -1
- 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/slack/blocks.ts +132 -1
- package/src/slack/responses.ts +15 -5
- package/src/slack/watcher.ts +12 -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/credential-check.test.ts +47 -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/rest-api.test.ts +51 -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/slack-attachments-block.test.ts +240 -0
- package/src/tests/slack-blocks.test.ts +162 -0
- package/src/tests/slack-watcher.test.ts +83 -0
- package/src/tests/store-progress-attachments-handler.test.ts +480 -0
- package/src/tests/store-progress-attachments.test.ts +353 -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 +102 -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 +149 -1
- package/src/utils/constants.ts +58 -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,185 @@ 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
|
+
agent_fs_org_id: string | null;
|
|
2150
|
+
agent_fs_drive_id: string | null;
|
|
2151
|
+
mime_type: string | null;
|
|
2152
|
+
size_bytes: number | null;
|
|
2153
|
+
sha256: string | null;
|
|
2154
|
+
intent: string | null;
|
|
2155
|
+
description: string | null;
|
|
2156
|
+
is_primary: number;
|
|
2157
|
+
created_at: string;
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
function rowToTaskAttachment(row: TaskAttachmentRow): TaskAttachment {
|
|
2161
|
+
return {
|
|
2162
|
+
id: row.id,
|
|
2163
|
+
taskId: row.task_id,
|
|
2164
|
+
agentId: row.agent_id,
|
|
2165
|
+
name: row.name,
|
|
2166
|
+
kind: row.kind as TaskAttachment["kind"],
|
|
2167
|
+
url: row.url ?? undefined,
|
|
2168
|
+
path: row.path ?? undefined,
|
|
2169
|
+
pageId: row.page_id ?? undefined,
|
|
2170
|
+
orgId: row.agent_fs_org_id ?? undefined,
|
|
2171
|
+
driveId: row.agent_fs_drive_id ?? undefined,
|
|
2172
|
+
mimeType: row.mime_type ?? undefined,
|
|
2173
|
+
sizeBytes: row.size_bytes ?? undefined,
|
|
2174
|
+
sha256: row.sha256 ?? undefined,
|
|
2175
|
+
intent: row.intent ?? undefined,
|
|
2176
|
+
description: row.description ?? undefined,
|
|
2177
|
+
isPrimary: !!row.is_primary,
|
|
2178
|
+
createdAt: row.created_at,
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
export interface InsertTaskAttachmentInput {
|
|
2183
|
+
taskId: string;
|
|
2184
|
+
agentId: string | null;
|
|
2185
|
+
name: string;
|
|
2186
|
+
kind: TaskAttachment["kind"];
|
|
2187
|
+
url?: string;
|
|
2188
|
+
path?: string;
|
|
2189
|
+
pageId?: string;
|
|
2190
|
+
/** agent-fs only — paired with `driveId` to build a public live-host URL. */
|
|
2191
|
+
orgId?: string;
|
|
2192
|
+
/** agent-fs only — paired with `orgId` to build a public live-host URL. */
|
|
2193
|
+
driveId?: string;
|
|
2194
|
+
mimeType?: string;
|
|
2195
|
+
sizeBytes?: number;
|
|
2196
|
+
sha256?: string;
|
|
2197
|
+
intent?: string;
|
|
2198
|
+
description?: string;
|
|
2199
|
+
isPrimary?: boolean;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Insert a task attachment. Append-only + dedup:
|
|
2204
|
+
* - if sha256 is present and a row for this task already has that sha256,
|
|
2205
|
+
* skip (return existing row);
|
|
2206
|
+
* - otherwise skip if a row exists for the same task with the same
|
|
2207
|
+
* (kind, path|url|page_id, name) tuple.
|
|
2208
|
+
* Returns the stored attachment (newly inserted or pre-existing duplicate).
|
|
2209
|
+
*/
|
|
2210
|
+
export function insertTaskAttachment(input: InsertTaskAttachmentInput): TaskAttachment {
|
|
2211
|
+
const db = getDb();
|
|
2212
|
+
|
|
2213
|
+
if (input.sha256) {
|
|
2214
|
+
const existing = db
|
|
2215
|
+
.prepare<TaskAttachmentRow, [string, string]>(
|
|
2216
|
+
"SELECT * FROM task_attachments WHERE task_id = ? AND sha256 = ? LIMIT 1",
|
|
2217
|
+
)
|
|
2218
|
+
.get(input.taskId, input.sha256);
|
|
2219
|
+
if (existing) return rowToTaskAttachment(existing);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
const tupleExisting = db
|
|
2223
|
+
.prepare<TaskAttachmentRow, [string, string, string, string, string, string]>(
|
|
2224
|
+
`SELECT * FROM task_attachments
|
|
2225
|
+
WHERE task_id = ?
|
|
2226
|
+
AND kind = ?
|
|
2227
|
+
AND IFNULL(path, '') = ?
|
|
2228
|
+
AND IFNULL(url, '') = ?
|
|
2229
|
+
AND IFNULL(page_id, '') = ?
|
|
2230
|
+
AND name = ?
|
|
2231
|
+
ORDER BY created_at ASC
|
|
2232
|
+
LIMIT 1`,
|
|
2233
|
+
)
|
|
2234
|
+
.get(
|
|
2235
|
+
input.taskId,
|
|
2236
|
+
input.kind,
|
|
2237
|
+
input.path ?? "",
|
|
2238
|
+
input.url ?? "",
|
|
2239
|
+
input.pageId ?? "",
|
|
2240
|
+
input.name,
|
|
2241
|
+
);
|
|
2242
|
+
if (tupleExisting) return rowToTaskAttachment(tupleExisting);
|
|
2243
|
+
|
|
2244
|
+
const id = crypto.randomUUID();
|
|
2245
|
+
const row = db
|
|
2246
|
+
.prepare<
|
|
2247
|
+
TaskAttachmentRow,
|
|
2248
|
+
[
|
|
2249
|
+
string,
|
|
2250
|
+
string,
|
|
2251
|
+
string | null,
|
|
2252
|
+
string,
|
|
2253
|
+
string,
|
|
2254
|
+
string | null,
|
|
2255
|
+
string | null,
|
|
2256
|
+
string | null,
|
|
2257
|
+
string | null,
|
|
2258
|
+
string | null,
|
|
2259
|
+
string | null,
|
|
2260
|
+
number | null,
|
|
2261
|
+
string | null,
|
|
2262
|
+
string | null,
|
|
2263
|
+
string | null,
|
|
2264
|
+
number,
|
|
2265
|
+
]
|
|
2266
|
+
>(
|
|
2267
|
+
`INSERT INTO task_attachments
|
|
2268
|
+
(id, task_id, agent_id, name, kind, url, path, page_id,
|
|
2269
|
+
agent_fs_org_id, agent_fs_drive_id,
|
|
2270
|
+
mime_type, size_bytes, sha256, intent, description, is_primary)
|
|
2271
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2272
|
+
RETURNING *`,
|
|
2273
|
+
)
|
|
2274
|
+
.get(
|
|
2275
|
+
id,
|
|
2276
|
+
input.taskId,
|
|
2277
|
+
input.agentId ?? null,
|
|
2278
|
+
input.name,
|
|
2279
|
+
input.kind,
|
|
2280
|
+
input.url ?? null,
|
|
2281
|
+
input.path ?? null,
|
|
2282
|
+
input.pageId ?? null,
|
|
2283
|
+
input.orgId ?? null,
|
|
2284
|
+
input.driveId ?? null,
|
|
2285
|
+
input.mimeType ?? null,
|
|
2286
|
+
input.sizeBytes ?? null,
|
|
2287
|
+
input.sha256 ?? null,
|
|
2288
|
+
input.intent ?? null,
|
|
2289
|
+
input.description ?? null,
|
|
2290
|
+
input.isPrimary ? 1 : 0,
|
|
2291
|
+
);
|
|
2292
|
+
|
|
2293
|
+
if (!row) {
|
|
2294
|
+
throw new Error("Failed to insert task attachment");
|
|
2295
|
+
}
|
|
2296
|
+
return rowToTaskAttachment(row);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
export function getTaskAttachments(taskId: string): TaskAttachment[] {
|
|
2300
|
+
return getDb()
|
|
2301
|
+
.prepare<TaskAttachmentRow, [string]>(
|
|
2302
|
+
"SELECT * FROM task_attachments WHERE task_id = ? ORDER BY created_at ASC, rowid ASC",
|
|
2303
|
+
)
|
|
2304
|
+
.all(taskId)
|
|
2305
|
+
.map(rowToTaskAttachment);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2057
2308
|
// ============================================================================
|
|
2058
2309
|
// Combined Queries (Agent with Tasks)
|
|
2059
2310
|
// ============================================================================
|
|
@@ -2070,9 +2321,9 @@ export function getAgentWithTasks(id: string): AgentWithTasks | null {
|
|
|
2070
2321
|
return txn();
|
|
2071
2322
|
}
|
|
2072
2323
|
|
|
2073
|
-
export function getAllAgentsWithTasks(): AgentWithTasks[] {
|
|
2324
|
+
export function getAllAgentsWithTasks(opts?: { slim?: boolean }): AgentWithTasks[] {
|
|
2074
2325
|
const txn = getDb().transaction(() => {
|
|
2075
|
-
const agents = getAllAgents();
|
|
2326
|
+
const agents = getAllAgents({ slim: opts?.slim ?? false });
|
|
2076
2327
|
return agents.map((agent) => ({
|
|
2077
2328
|
...agent,
|
|
2078
2329
|
tasks: getTasksByAgentId(agent.id),
|
|
@@ -2166,8 +2417,13 @@ export function getLogsByAgentId(agentId: string): AgentLog[] {
|
|
|
2166
2417
|
return logQueries.getByAgentId().all(agentId).map(rowToAgentLog);
|
|
2167
2418
|
}
|
|
2168
2419
|
|
|
2169
|
-
export function getLogsByTaskId(taskId: string): AgentLog[] {
|
|
2170
|
-
return
|
|
2420
|
+
export function getLogsByTaskId(taskId: string, limit = 200): AgentLog[] {
|
|
2421
|
+
return getDb()
|
|
2422
|
+
.prepare<AgentLogRow, [string, number]>(
|
|
2423
|
+
"SELECT * FROM agent_log WHERE taskId = ? ORDER BY createdAt DESC LIMIT ?",
|
|
2424
|
+
)
|
|
2425
|
+
.all(taskId, limit)
|
|
2426
|
+
.map(rowToAgentLog);
|
|
2171
2427
|
}
|
|
2172
2428
|
|
|
2173
2429
|
export function getLogsByTaskIdChronological(taskId: string): AgentLog[] {
|
|
@@ -2629,7 +2885,7 @@ export function releaseStaleReviewingTasks(timeoutMinutes: number = 30): number
|
|
|
2629
2885
|
export function getOfferedTasksForAgent(agentId: string): AgentTask[] {
|
|
2630
2886
|
return getDb()
|
|
2631
2887
|
.prepare<AgentTaskRow, [string]>(
|
|
2632
|
-
"SELECT * FROM agent_tasks WHERE offeredTo = ? AND status = 'offered' ORDER BY createdAt ASC",
|
|
2888
|
+
"SELECT * FROM agent_tasks WHERE offeredTo = ? AND status = 'offered' ORDER BY createdAt ASC, rowid ASC",
|
|
2633
2889
|
)
|
|
2634
2890
|
.all(agentId)
|
|
2635
2891
|
.map(rowToAgentTask);
|
|
@@ -2682,7 +2938,7 @@ export function getUnassignedTasksCount(): number {
|
|
|
2682
2938
|
export function getUnassignedTaskIds(limit = 10): string[] {
|
|
2683
2939
|
const rows = getDb()
|
|
2684
2940
|
.prepare<{ id: string }, [number]>(
|
|
2685
|
-
"SELECT id FROM agent_tasks WHERE status = 'unassigned' ORDER BY priority DESC, createdAt ASC LIMIT ?",
|
|
2941
|
+
"SELECT id FROM agent_tasks WHERE status = 'unassigned' ORDER BY priority DESC, createdAt ASC, rowid ASC LIMIT ?",
|
|
2686
2942
|
)
|
|
2687
2943
|
.all(limit);
|
|
2688
2944
|
return rows.map((r) => r.id);
|
|
@@ -4477,6 +4733,21 @@ type ScheduledTaskRow = {
|
|
|
4477
4733
|
lastUpdatedAt: string;
|
|
4478
4734
|
};
|
|
4479
4735
|
|
|
4736
|
+
// ── List-endpoint slimming helpers ──────────────────────────────────────────
|
|
4737
|
+
// List endpoints ship slim rows by default; heavy text fields are replaced
|
|
4738
|
+
// with bounded previews. Lengths are generous enough for triage/recognition
|
|
4739
|
+
// while keeping list payloads small.
|
|
4740
|
+
/** Preview length for a schedule's `taskTemplate`. */
|
|
4741
|
+
const SCHEDULE_TEMPLATE_PREVIEW_LENGTH = 280;
|
|
4742
|
+
/** Preview length for a task's `task` text (pool-triage needs to read it). */
|
|
4743
|
+
const TASK_PREVIEW_LENGTH = 300;
|
|
4744
|
+
|
|
4745
|
+
/** Truncate text for a list-row preview. Appends an ellipsis when clipped. */
|
|
4746
|
+
function previewText(text: string | null | undefined, maxChars: number): string {
|
|
4747
|
+
const s = text ?? "";
|
|
4748
|
+
return s.length > maxChars ? `${s.slice(0, maxChars)}…` : s;
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4480
4751
|
function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
|
|
4481
4752
|
return {
|
|
4482
4753
|
id: row.id,
|
|
@@ -4511,7 +4782,28 @@ export interface ScheduledTaskFilters {
|
|
|
4511
4782
|
hideCompleted?: boolean;
|
|
4512
4783
|
}
|
|
4513
4784
|
|
|
4514
|
-
|
|
4785
|
+
/**
|
|
4786
|
+
* Slim list-row mapper — replaces the full `taskTemplate` (the per-run prompt,
|
|
4787
|
+
* avg ~3.6 KB) with a bounded `taskTemplatePreview`. Fetch the full template
|
|
4788
|
+
* via `getScheduledTaskById(id)`.
|
|
4789
|
+
*/
|
|
4790
|
+
function rowToScheduledTaskSummary(row: ScheduledTaskRow): ScheduledTaskSummary {
|
|
4791
|
+
const { taskTemplate, ...rest } = rowToScheduledTask(row);
|
|
4792
|
+
return {
|
|
4793
|
+
...rest,
|
|
4794
|
+
taskTemplatePreview: previewText(taskTemplate, SCHEDULE_TEMPLATE_PREVIEW_LENGTH),
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask[];
|
|
4799
|
+
export function getScheduledTasks(
|
|
4800
|
+
filters: ScheduledTaskFilters | undefined,
|
|
4801
|
+
opts: { slim: true },
|
|
4802
|
+
): ScheduledTaskSummary[];
|
|
4803
|
+
export function getScheduledTasks(
|
|
4804
|
+
filters?: ScheduledTaskFilters,
|
|
4805
|
+
opts?: { slim?: boolean },
|
|
4806
|
+
): ScheduledTask[] | ScheduledTaskSummary[] {
|
|
4515
4807
|
let query = "SELECT * FROM scheduled_tasks WHERE 1=1";
|
|
4516
4808
|
const params: (string | number)[] = [];
|
|
4517
4809
|
|
|
@@ -4536,10 +4828,10 @@ export function getScheduledTasks(filters?: ScheduledTaskFilters): ScheduledTask
|
|
|
4536
4828
|
|
|
4537
4829
|
query += " ORDER BY name ASC";
|
|
4538
4830
|
|
|
4539
|
-
|
|
4831
|
+
const rows = getDb()
|
|
4540
4832
|
.prepare<ScheduledTaskRow, (string | number)[]>(query)
|
|
4541
|
-
.all(...params)
|
|
4542
|
-
|
|
4833
|
+
.all(...params);
|
|
4834
|
+
return opts?.slim ? rows.map(rowToScheduledTaskSummary) : rows.map(rowToScheduledTask);
|
|
4543
4835
|
}
|
|
4544
4836
|
|
|
4545
4837
|
export function getScheduledTaskById(id: string): ScheduledTask | null {
|
|
@@ -5580,7 +5872,7 @@ export function getIdleWorkersWithCapacity(): Agent[] {
|
|
|
5580
5872
|
WHERE status = 'idle' AND isLead = 0`,
|
|
5581
5873
|
)
|
|
5582
5874
|
.all()
|
|
5583
|
-
.map(rowToAgent);
|
|
5875
|
+
.map((row) => rowToAgent(row));
|
|
5584
5876
|
|
|
5585
5877
|
return agents.filter((agent) => {
|
|
5586
5878
|
const activeCount = getActiveTaskCount(agent.id);
|
|
@@ -5739,7 +6031,43 @@ export function getWorkflow(id: string): Workflow | null {
|
|
|
5739
6031
|
return row ? rowToWorkflow(row) : null;
|
|
5740
6032
|
}
|
|
5741
6033
|
|
|
5742
|
-
|
|
6034
|
+
/**
|
|
6035
|
+
* Slim list-row mapper — drops the heavy `definition` (avg ~18 KB/row) and the
|
|
6036
|
+
* trigger config, keeping a derived `nodeCount` so the list view can still
|
|
6037
|
+
* answer "how big is this workflow" without the full DAG. Fetch the full shape
|
|
6038
|
+
* via `getWorkflow(id)`.
|
|
6039
|
+
*/
|
|
6040
|
+
function rowToWorkflowSummary(row: WorkflowRow): WorkflowSummary {
|
|
6041
|
+
let nodeCount = 0;
|
|
6042
|
+
try {
|
|
6043
|
+
const def = JSON.parse(row.definition) as WorkflowDefinition;
|
|
6044
|
+
nodeCount = Array.isArray(def?.nodes) ? def.nodes.length : 0;
|
|
6045
|
+
} catch {
|
|
6046
|
+
nodeCount = 0;
|
|
6047
|
+
}
|
|
6048
|
+
return {
|
|
6049
|
+
id: row.id,
|
|
6050
|
+
name: row.name,
|
|
6051
|
+
description: row.description ?? undefined,
|
|
6052
|
+
enabled: row.enabled === 1,
|
|
6053
|
+
dir: row.dir ?? undefined,
|
|
6054
|
+
vcsRepo: row.vcs_repo ?? undefined,
|
|
6055
|
+
createdByAgentId: row.createdByAgentId ?? undefined,
|
|
6056
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
6057
|
+
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
6058
|
+
nodeCount,
|
|
6059
|
+
};
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
export function listWorkflows(filters?: { enabled?: boolean }): Workflow[];
|
|
6063
|
+
export function listWorkflows(
|
|
6064
|
+
filters: { enabled?: boolean } | undefined,
|
|
6065
|
+
opts: { slim: true },
|
|
6066
|
+
): WorkflowSummary[];
|
|
6067
|
+
export function listWorkflows(
|
|
6068
|
+
filters?: { enabled?: boolean },
|
|
6069
|
+
opts?: { slim?: boolean },
|
|
6070
|
+
): Workflow[] | WorkflowSummary[] {
|
|
5743
6071
|
let query = "SELECT * FROM workflows WHERE 1=1";
|
|
5744
6072
|
const params: (string | number)[] = [];
|
|
5745
6073
|
if (filters?.enabled !== undefined) {
|
|
@@ -5747,10 +6075,10 @@ export function listWorkflows(filters?: { enabled?: boolean }): Workflow[] {
|
|
|
5747
6075
|
params.push(filters.enabled ? 1 : 0);
|
|
5748
6076
|
}
|
|
5749
6077
|
query += " ORDER BY name ASC";
|
|
5750
|
-
|
|
6078
|
+
const rows = getDb()
|
|
5751
6079
|
.prepare<WorkflowRow, (string | number)[]>(query)
|
|
5752
|
-
.all(...params)
|
|
5753
|
-
|
|
6080
|
+
.all(...params);
|
|
6081
|
+
return opts?.slim ? rows.map(rowToWorkflowSummary) : rows.map(rowToWorkflow);
|
|
5754
6082
|
}
|
|
5755
6083
|
|
|
5756
6084
|
export function updateWorkflow(
|
|
@@ -6378,22 +6706,84 @@ export function getPageBySlug(agentId: string, slug: string): Page | null {
|
|
|
6378
6706
|
return row ? rowToPage(row) : null;
|
|
6379
6707
|
}
|
|
6380
6708
|
|
|
6381
|
-
|
|
6382
|
-
|
|
6709
|
+
/**
|
|
6710
|
+
* Slim list-row mapper — drops the page `body` (the full HTML/JSON document,
|
|
6711
|
+
* up to ~290 KB and ~95% of a list payload) and `passwordHash`. Fetch the
|
|
6712
|
+
* full page via `getPage(id)`.
|
|
6713
|
+
*/
|
|
6714
|
+
function rowToPageSummary(row: PageRow): PageSummary {
|
|
6715
|
+
return {
|
|
6716
|
+
id: row.id,
|
|
6717
|
+
agentId: row.agentId,
|
|
6718
|
+
slug: row.slug,
|
|
6719
|
+
title: row.title,
|
|
6720
|
+
description: row.description ?? undefined,
|
|
6721
|
+
contentType: row.contentType as PageContentType,
|
|
6722
|
+
authMode: row.authMode as PageAuthMode,
|
|
6723
|
+
needsCredentials: row.needsCredentials
|
|
6724
|
+
? (JSON.parse(row.needsCredentials) as string[])
|
|
6725
|
+
: undefined,
|
|
6726
|
+
viewCount: typeof row.view_count === "number" ? row.view_count : 0,
|
|
6727
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
6728
|
+
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
6729
|
+
};
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
export function listPagesByAgent(agentId: string, limit?: number, offset?: number): Page[];
|
|
6733
|
+
export function listPagesByAgent(
|
|
6734
|
+
agentId: string,
|
|
6735
|
+
limit: number | undefined,
|
|
6736
|
+
offset: number | undefined,
|
|
6737
|
+
opts: { slim: true },
|
|
6738
|
+
): PageSummary[];
|
|
6739
|
+
export function listPagesByAgent(
|
|
6740
|
+
agentId: string,
|
|
6741
|
+
limit = 100,
|
|
6742
|
+
offset = 0,
|
|
6743
|
+
opts?: { slim?: boolean },
|
|
6744
|
+
): Page[] | PageSummary[] {
|
|
6745
|
+
const rows = getDb()
|
|
6383
6746
|
.prepare<PageRow, [string, number, number]>(
|
|
6384
6747
|
"SELECT * FROM pages WHERE agentId = ? ORDER BY updatedAt DESC LIMIT ? OFFSET ?",
|
|
6385
6748
|
)
|
|
6386
|
-
.all(agentId, limit, offset)
|
|
6387
|
-
|
|
6388
|
-
}
|
|
6389
|
-
|
|
6390
|
-
export function listAllPages(limit
|
|
6391
|
-
|
|
6749
|
+
.all(agentId, limit, offset);
|
|
6750
|
+
return opts?.slim ? rows.map(rowToPageSummary) : rows.map(rowToPage);
|
|
6751
|
+
}
|
|
6752
|
+
|
|
6753
|
+
export function listAllPages(limit?: number, offset?: number): Page[];
|
|
6754
|
+
export function listAllPages(
|
|
6755
|
+
limit: number | undefined,
|
|
6756
|
+
offset: number | undefined,
|
|
6757
|
+
opts: { slim: true },
|
|
6758
|
+
): PageSummary[];
|
|
6759
|
+
export function listAllPages(
|
|
6760
|
+
limit = 100,
|
|
6761
|
+
offset = 0,
|
|
6762
|
+
opts?: { slim?: boolean },
|
|
6763
|
+
): Page[] | PageSummary[] {
|
|
6764
|
+
const rows = getDb()
|
|
6392
6765
|
.prepare<PageRow, [number, number]>(
|
|
6393
6766
|
"SELECT * FROM pages ORDER BY updatedAt DESC LIMIT ? OFFSET ?",
|
|
6394
6767
|
)
|
|
6395
|
-
.all(limit, offset)
|
|
6396
|
-
|
|
6768
|
+
.all(limit, offset);
|
|
6769
|
+
return opts?.slim ? rows.map(rowToPageSummary) : rows.map(rowToPage);
|
|
6770
|
+
}
|
|
6771
|
+
|
|
6772
|
+
/**
|
|
6773
|
+
* Total page count — used to back a filter-aware `total` in the `/api/pages`
|
|
6774
|
+
* pager so the UI shows the real count, not just the current page's length.
|
|
6775
|
+
*/
|
|
6776
|
+
export function countAllPages(): number {
|
|
6777
|
+
const row = getDb().prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM pages").get();
|
|
6778
|
+
return row?.count ?? 0;
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
/** Page count scoped to a single agent — companion to `listPagesByAgent`. */
|
|
6782
|
+
export function countPagesByAgent(agentId: string): number {
|
|
6783
|
+
const row = getDb()
|
|
6784
|
+
.prepare<{ count: number }, [string]>("SELECT COUNT(*) AS count FROM pages WHERE agentId = ?")
|
|
6785
|
+
.get(agentId);
|
|
6786
|
+
return row?.count ?? 0;
|
|
6397
6787
|
}
|
|
6398
6788
|
|
|
6399
6789
|
/**
|
|
@@ -7842,11 +8232,16 @@ export interface SkillFilters {
|
|
|
7842
8232
|
includeContent?: boolean;
|
|
7843
8233
|
}
|
|
7844
8234
|
|
|
8235
|
+
/**
|
|
8236
|
+
* Explicit column list used when `includeContent: false` — selects every
|
|
8237
|
+
* skill column except the heavy `content` (the full SKILL.md, avg ~10 KB),
|
|
8238
|
+
* which is replaced with an empty string so the row still satisfies `Skill`.
|
|
8239
|
+
*/
|
|
8240
|
+
const SKILL_SLIM_COLUMNS =
|
|
8241
|
+
"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";
|
|
8242
|
+
|
|
7845
8243
|
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
|
-
: "*";
|
|
8244
|
+
const columns = filters?.includeContent === false ? SKILL_SLIM_COLUMNS : "*";
|
|
7850
8245
|
let query = `SELECT ${columns} FROM skills WHERE 1=1`;
|
|
7851
8246
|
const params: (string | number)[] = [];
|
|
7852
8247
|
|
|
@@ -7885,11 +8280,12 @@ export function listSkills(filters?: SkillFilters): Skill[] {
|
|
|
7885
8280
|
.map(rowToSkill);
|
|
7886
8281
|
}
|
|
7887
8282
|
|
|
7888
|
-
export function searchSkills(query: string, limit = 20): Skill[] {
|
|
8283
|
+
export function searchSkills(query: string, limit = 20, includeContent = true): Skill[] {
|
|
7889
8284
|
const term = `%${query}%`;
|
|
8285
|
+
const columns = includeContent === false ? SKILL_SLIM_COLUMNS : "*";
|
|
7890
8286
|
return getDb()
|
|
7891
8287
|
.prepare<SkillRow, [string, string, number]>(
|
|
7892
|
-
|
|
8288
|
+
`SELECT ${columns} FROM skills WHERE (name LIKE ? OR description LIKE ?) AND isEnabled = 1 ORDER BY name ASC LIMIT ?`,
|
|
7893
8289
|
)
|
|
7894
8290
|
.all(term, term, limit)
|
|
7895
8291
|
.map(rowToSkill);
|
|
@@ -9094,16 +9490,30 @@ export interface SessionListItem {
|
|
|
9094
9490
|
latestStatus: AgentTaskStatus;
|
|
9095
9491
|
}
|
|
9096
9492
|
|
|
9493
|
+
/**
|
|
9494
|
+
* Slim variant of {@link SessionListItem} — the `root` task is an
|
|
9495
|
+
* `AgentTaskSummary` (full `task` text + completion/integration blobs dropped).
|
|
9496
|
+
* The session list only renders a brief of the root; the full root + chain are
|
|
9497
|
+
* on `GET /api/sessions/{rootTaskId}`.
|
|
9498
|
+
*/
|
|
9499
|
+
export interface SessionListItemSummary {
|
|
9500
|
+
root: AgentTaskSummary;
|
|
9501
|
+
chainTaskCount: number;
|
|
9502
|
+
lastActivityAt: string;
|
|
9503
|
+
latestStatus: AgentTaskStatus;
|
|
9504
|
+
}
|
|
9505
|
+
|
|
9097
9506
|
/**
|
|
9098
9507
|
* List the most recent sessions ordered by chain-wide latest activity.
|
|
9099
9508
|
* A "session" here is any task with `parentTaskId IS NULL` — its descendants
|
|
9100
9509
|
* (children, grand-children, …) are summarized via the recursive CTE.
|
|
9101
9510
|
*
|
|
9102
|
-
*
|
|
9103
|
-
*
|
|
9104
|
-
*
|
|
9511
|
+
* Single-pass CTE: seeds with root tasks matching the filter, walks the full
|
|
9512
|
+
* descendant tree once, then aggregates chainCount / lastActivityAt /
|
|
9513
|
+
* latestStatus in two lightweight non-recursive CTEs — replacing the original
|
|
9514
|
+
* pattern of 3 correlated subqueries each re-running the recursion per row.
|
|
9105
9515
|
*/
|
|
9106
|
-
|
|
9516
|
+
interface ListRecentSessionsOpts {
|
|
9107
9517
|
limit?: number;
|
|
9108
9518
|
offset?: number;
|
|
9109
9519
|
/** Filter to root tasks whose `source` is in this list. Empty/undefined → no source filter. */
|
|
@@ -9112,7 +9522,19 @@ export function listRecentSessions(opts?: {
|
|
|
9112
9522
|
q?: string;
|
|
9113
9523
|
/** When set, restrict to root tasks where `requestedByUserId` equals this value. NULL rows are excluded. */
|
|
9114
9524
|
requestedByUserId?: string;
|
|
9115
|
-
|
|
9525
|
+
/** When true, return slim `SessionListItemSummary` rows (default: full). */
|
|
9526
|
+
slim?: boolean;
|
|
9527
|
+
}
|
|
9528
|
+
|
|
9529
|
+
export function listRecentSessions(
|
|
9530
|
+
opts?: ListRecentSessionsOpts & { slim?: false },
|
|
9531
|
+
): SessionListItem[];
|
|
9532
|
+
export function listRecentSessions(
|
|
9533
|
+
opts: ListRecentSessionsOpts & { slim: true },
|
|
9534
|
+
): SessionListItemSummary[];
|
|
9535
|
+
export function listRecentSessions(
|
|
9536
|
+
opts?: ListRecentSessionsOpts,
|
|
9537
|
+
): SessionListItem[] | SessionListItemSummary[] {
|
|
9116
9538
|
const limit = opts?.limit ?? 25;
|
|
9117
9539
|
const offset = opts?.offset ?? 0;
|
|
9118
9540
|
const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
|
|
@@ -9141,48 +9563,55 @@ export function listRecentSessions(opts?: {
|
|
|
9141
9563
|
AgentTaskRow & { __chainCount: number; __lastActivityAt: string; __latestStatus: string },
|
|
9142
9564
|
typeof params
|
|
9143
9565
|
>(
|
|
9144
|
-
`
|
|
9566
|
+
`WITH RECURSIVE chain(root_id, id, lastUpdatedAt, status) AS (
|
|
9567
|
+
SELECT r.id, r.id, r.lastUpdatedAt, r.status
|
|
9568
|
+
FROM agent_tasks r
|
|
9569
|
+
WHERE ${conditions.join(" AND ")}
|
|
9570
|
+
UNION ALL
|
|
9571
|
+
SELECT c.root_id, t.id, t.lastUpdatedAt, t.status
|
|
9572
|
+
FROM agent_tasks t
|
|
9573
|
+
JOIN chain c ON t.parentTaskId = c.id
|
|
9574
|
+
),
|
|
9575
|
+
agg AS (
|
|
9576
|
+
SELECT
|
|
9577
|
+
root_id,
|
|
9578
|
+
COUNT(*) AS chainCount,
|
|
9579
|
+
MAX(lastUpdatedAt) AS lastActivityAt
|
|
9580
|
+
FROM chain
|
|
9581
|
+
GROUP BY root_id
|
|
9582
|
+
),
|
|
9583
|
+
latest_status AS (
|
|
9584
|
+
SELECT c.root_id, c.status AS latestStatus
|
|
9585
|
+
FROM chain c
|
|
9586
|
+
JOIN agg a ON c.root_id = a.root_id AND c.lastUpdatedAt = a.lastActivityAt
|
|
9587
|
+
GROUP BY c.root_id
|
|
9588
|
+
)
|
|
9589
|
+
SELECT
|
|
9145
9590
|
r.*,
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
SELECT r.id
|
|
9150
|
-
UNION ALL
|
|
9151
|
-
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
9152
|
-
)
|
|
9153
|
-
SELECT id FROM chain
|
|
9154
|
-
)
|
|
9155
|
-
) AS __chainCount,
|
|
9156
|
-
(SELECT MAX(d.lastUpdatedAt) FROM agent_tasks d
|
|
9157
|
-
WHERE d.id IN (
|
|
9158
|
-
WITH RECURSIVE chain(id) AS (
|
|
9159
|
-
SELECT r.id
|
|
9160
|
-
UNION ALL
|
|
9161
|
-
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
9162
|
-
)
|
|
9163
|
-
SELECT id FROM chain
|
|
9164
|
-
)
|
|
9165
|
-
) AS __lastActivityAt,
|
|
9166
|
-
(SELECT d.status FROM agent_tasks d
|
|
9167
|
-
WHERE d.id IN (
|
|
9168
|
-
WITH RECURSIVE chain(id) AS (
|
|
9169
|
-
SELECT r.id
|
|
9170
|
-
UNION ALL
|
|
9171
|
-
SELECT t.id FROM agent_tasks t JOIN chain c ON t.parentTaskId = c.id
|
|
9172
|
-
)
|
|
9173
|
-
SELECT id FROM chain
|
|
9174
|
-
)
|
|
9175
|
-
ORDER BY d.lastUpdatedAt DESC
|
|
9176
|
-
LIMIT 1
|
|
9177
|
-
) AS __latestStatus
|
|
9591
|
+
a.chainCount AS __chainCount,
|
|
9592
|
+
a.lastActivityAt AS __lastActivityAt,
|
|
9593
|
+
COALESCE(ls.latestStatus, r.status) AS __latestStatus
|
|
9178
9594
|
FROM agent_tasks r
|
|
9179
|
-
|
|
9180
|
-
|
|
9595
|
+
JOIN agg a ON a.root_id = r.id
|
|
9596
|
+
LEFT JOIN latest_status ls ON ls.root_id = r.id
|
|
9597
|
+
ORDER BY a.lastActivityAt DESC
|
|
9181
9598
|
LIMIT ? OFFSET ?`,
|
|
9182
9599
|
)
|
|
9183
9600
|
.all(...params);
|
|
9184
9601
|
|
|
9185
|
-
|
|
9602
|
+
if (opts?.slim) {
|
|
9603
|
+
return rootRows.map((row): SessionListItemSummary => {
|
|
9604
|
+
const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
|
|
9605
|
+
return {
|
|
9606
|
+
root: rowToAgentTaskSummary(taskRow as AgentTaskRow),
|
|
9607
|
+
chainTaskCount: __chainCount,
|
|
9608
|
+
lastActivityAt: __lastActivityAt ?? row.lastUpdatedAt,
|
|
9609
|
+
latestStatus: (__latestStatus as AgentTaskStatus) ?? row.status,
|
|
9610
|
+
};
|
|
9611
|
+
});
|
|
9612
|
+
}
|
|
9613
|
+
|
|
9614
|
+
return rootRows.map((row): SessionListItem => {
|
|
9186
9615
|
const { __chainCount, __lastActivityAt, __latestStatus, ...taskRow } = row;
|
|
9187
9616
|
return {
|
|
9188
9617
|
root: rowToAgentTask(taskRow as AgentTaskRow),
|
|
@@ -9193,6 +9622,43 @@ export function listRecentSessions(opts?: {
|
|
|
9193
9622
|
});
|
|
9194
9623
|
}
|
|
9195
9624
|
|
|
9625
|
+
/**
|
|
9626
|
+
* Filter-aware count of sessions (root tasks) matching the same `source` / `q`
|
|
9627
|
+
* / `requestedByUserId` filters as `listRecentSessions`. Powers a correct
|
|
9628
|
+
* `total` in the `/api/sessions` pager — a session is a root task, so this is
|
|
9629
|
+
* a plain count, no recursive chain walk needed.
|
|
9630
|
+
*/
|
|
9631
|
+
export function countSessions(
|
|
9632
|
+
opts?: Pick<ListRecentSessionsOpts, "source" | "q" | "requestedByUserId">,
|
|
9633
|
+
): number {
|
|
9634
|
+
const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
|
|
9635
|
+
const q = opts?.q?.trim();
|
|
9636
|
+
const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
|
|
9637
|
+
|
|
9638
|
+
const conditions: string[] = ["parentTaskId IS NULL"];
|
|
9639
|
+
const params: string[] = [];
|
|
9640
|
+
|
|
9641
|
+
if (sources.length > 0) {
|
|
9642
|
+
conditions.push(`source IN (${sources.map(() => "?").join(", ")})`);
|
|
9643
|
+
params.push(...sources);
|
|
9644
|
+
}
|
|
9645
|
+
if (q && q.length > 0) {
|
|
9646
|
+
conditions.push("lower(task) LIKE ?");
|
|
9647
|
+
params.push(`%${q.toLowerCase()}%`);
|
|
9648
|
+
}
|
|
9649
|
+
if (requestedByUserId) {
|
|
9650
|
+
conditions.push("requestedByUserId = ?");
|
|
9651
|
+
params.push(requestedByUserId);
|
|
9652
|
+
}
|
|
9653
|
+
|
|
9654
|
+
const row = getDb()
|
|
9655
|
+
.prepare<{ count: number }, string[]>(
|
|
9656
|
+
`SELECT COUNT(*) AS count FROM agent_tasks WHERE ${conditions.join(" AND ")}`,
|
|
9657
|
+
)
|
|
9658
|
+
.get(...params);
|
|
9659
|
+
return row?.count ?? 0;
|
|
9660
|
+
}
|
|
9661
|
+
|
|
9196
9662
|
// ============================================================================
|
|
9197
9663
|
// Budgets, daily-spend aggregation, and budget-refusal notifications (Phase 2)
|
|
9198
9664
|
// ----------------------------------------------------------------------------
|
|
@@ -9685,6 +10151,60 @@ export function getInstanceActivity(): {
|
|
|
9685
10151
|
};
|
|
9686
10152
|
}
|
|
9687
10153
|
|
|
10154
|
+
export interface SwarmMetrics {
|
|
10155
|
+
tasks: { total: number; by_status: Record<string, number> };
|
|
10156
|
+
agents: { total: number; by_status: Record<string, number> };
|
|
10157
|
+
workflows: { total: number; enabled: number };
|
|
10158
|
+
pages: { total: number };
|
|
10159
|
+
sessions: { active: number };
|
|
10160
|
+
skills: { total: number };
|
|
10161
|
+
}
|
|
10162
|
+
|
|
10163
|
+
/**
|
|
10164
|
+
* Lightweight swarm-wide counts for UI footers/sidebars and MCP context —
|
|
10165
|
+
* a single object so callers never have to fetch full list payloads just to
|
|
10166
|
+
* count. Pure `COUNT(*)` / `GROUP BY` queries; the `agent_tasks` status
|
|
10167
|
+
* grouping rides the indexes added in migration 069.
|
|
10168
|
+
*/
|
|
10169
|
+
export function getSwarmMetrics(): SwarmMetrics {
|
|
10170
|
+
const db = getDb();
|
|
10171
|
+
|
|
10172
|
+
const groupCounts = (table: string): { total: number; by_status: Record<string, number> } => {
|
|
10173
|
+
const rows = db
|
|
10174
|
+
.prepare<{ status: string; count: number }, []>(
|
|
10175
|
+
`SELECT status, COUNT(*) AS count FROM ${table} GROUP BY status`,
|
|
10176
|
+
)
|
|
10177
|
+
.all();
|
|
10178
|
+
const by_status: Record<string, number> = {};
|
|
10179
|
+
let total = 0;
|
|
10180
|
+
for (const r of rows) {
|
|
10181
|
+
by_status[r.status] = r.count;
|
|
10182
|
+
total += r.count;
|
|
10183
|
+
}
|
|
10184
|
+
return { total, by_status };
|
|
10185
|
+
};
|
|
10186
|
+
|
|
10187
|
+
const workflowRow = db
|
|
10188
|
+
.prepare<{ total: number; enabled: number }, []>(
|
|
10189
|
+
"SELECT COUNT(*) AS total, SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) AS enabled FROM workflows",
|
|
10190
|
+
)
|
|
10191
|
+
.get();
|
|
10192
|
+
const pagesRow = db.prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM pages").get();
|
|
10193
|
+
const sessionsRow = db
|
|
10194
|
+
.prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM active_sessions")
|
|
10195
|
+
.get();
|
|
10196
|
+
const skillsRow = db.prepare<{ count: number }, []>("SELECT COUNT(*) AS count FROM skills").get();
|
|
10197
|
+
|
|
10198
|
+
return {
|
|
10199
|
+
tasks: groupCounts("agent_tasks"),
|
|
10200
|
+
agents: groupCounts("agents"),
|
|
10201
|
+
workflows: { total: workflowRow?.total ?? 0, enabled: workflowRow?.enabled ?? 0 },
|
|
10202
|
+
pages: { total: pagesRow?.count ?? 0 },
|
|
10203
|
+
sessions: { active: sessionsRow?.count ?? 0 },
|
|
10204
|
+
skills: { total: skillsRow?.count ?? 0 },
|
|
10205
|
+
};
|
|
10206
|
+
}
|
|
10207
|
+
|
|
9688
10208
|
/**
|
|
9689
10209
|
* `first_task` milestone: true once any task has reached `status = 'completed'`.
|
|
9690
10210
|
* Cheap LIMIT 1 probe; the row's contents don't matter, only existence.
|