@desplega.ai/agent-swarm 1.52.1 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/openapi.json +1517 -488
  2. package/package.json +5 -2
  3. package/src/be/db.ts +530 -0
  4. package/src/be/events.ts +322 -0
  5. package/src/be/migrations/021_events.sql +24 -0
  6. package/src/be/migrations/022_context_usage.sql +34 -0
  7. package/src/be/migrations/023_mcp_servers.sql +44 -0
  8. package/src/commands/runner.ts +348 -1
  9. package/src/http/context.ts +118 -0
  10. package/src/http/events.ts +188 -0
  11. package/src/http/index.ts +6 -0
  12. package/src/http/mcp-servers.ts +364 -0
  13. package/src/http/tasks.ts +33 -0
  14. package/src/linear/outbound.ts +8 -1
  15. package/src/linear/sync.ts +3 -0
  16. package/src/oauth/ensure-token.ts +50 -0
  17. package/src/prompts/base-prompt.ts +7 -0
  18. package/src/providers/claude-adapter.ts +156 -15
  19. package/src/providers/pi-mono-adapter.ts +68 -0
  20. package/src/providers/pi-mono-extension.ts +56 -2
  21. package/src/providers/pi-mono-mcp-client.ts +10 -1
  22. package/src/providers/types.ts +14 -1
  23. package/src/server.ts +19 -0
  24. package/src/tests/context-window.test.ts +66 -0
  25. package/src/tests/ensure-token.test.ts +170 -0
  26. package/src/tests/events-db.test.ts +314 -0
  27. package/src/tests/events-http.test.ts +267 -0
  28. package/src/tests/prompt-template-remaining.test.ts +5 -5
  29. package/src/tests/tool-annotations.test.ts +2 -2
  30. package/src/tests/vcs-tracking.test.ts +176 -0
  31. package/src/tests/workflow-executors.test.ts +8 -1
  32. package/src/tools/mcp-servers/index.ts +7 -0
  33. package/src/tools/mcp-servers/mcp-server-create.ts +138 -0
  34. package/src/tools/mcp-servers/mcp-server-delete.ts +72 -0
  35. package/src/tools/mcp-servers/mcp-server-get.ts +80 -0
  36. package/src/tools/mcp-servers/mcp-server-install.ts +110 -0
  37. package/src/tools/mcp-servers/mcp-server-list.ts +67 -0
  38. package/src/tools/mcp-servers/mcp-server-uninstall.ts +71 -0
  39. package/src/tools/mcp-servers/mcp-server-update.ts +120 -0
  40. package/src/tools/tool-config.ts +9 -0
  41. package/src/types.ts +153 -0
  42. package/src/utils/context-window.ts +41 -0
  43. package/src/workflows/executors/base.ts +9 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.52.1",
3
+ "version": "1.53.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -79,10 +79,13 @@
79
79
  "pm2-stop": "pm2 stop ecosystem.config.cjs",
80
80
  "pm2-restart": "pm2 restart ecosystem.config.cjs",
81
81
  "pm2-logs": "pm2 logs swarm-api swarm-ui swarm-lead swarm-worker",
82
- "pm2-status": "pm2 status swarm-api swarm-ui swarm-lead swarm-worker"
82
+ "pm2-status": "pm2 status swarm-api swarm-ui swarm-lead swarm-worker",
83
+ "seed": "bun scripts/seed.ts",
84
+ "seed:clean": "bun scripts/seed.ts --clean"
83
85
  },
84
86
  "devDependencies": {
85
87
  "@biomejs/biome": "^2.3.10",
88
+ "@faker-js/faker": "^10.4.0",
86
89
  "@types/bun": "latest"
87
90
  },
88
91
  "peerDependencies": {
package/src/be/db.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  Agent,
6
6
  AgentLog,
7
7
  AgentLogEventType,
8
+ AgentMcpServer,
8
9
  AgentMemory,
9
10
  AgentMemoryScope,
10
11
  AgentMemorySource,
@@ -18,6 +19,8 @@ import type {
18
19
  Channel,
19
20
  ChannelMessage,
20
21
  ChannelType,
22
+ ContextSnapshot,
23
+ ContextSnapshotEventType,
21
24
  ContextVersion,
22
25
  CooldownConfig,
23
26
  Epic,
@@ -26,6 +29,10 @@ import type {
26
29
  InboxMessage,
27
30
  InboxMessageStatus,
28
31
  InputValue,
32
+ McpServer,
33
+ McpServerScope,
34
+ McpServerTransport,
35
+ McpServerWithInstallInfo,
29
36
  PromptTemplate,
30
37
  PromptTemplateHistory,
31
38
  ScheduledTask,
@@ -717,6 +724,10 @@ type AgentTaskRow = {
717
724
  failureReason: string | null;
718
725
  output: string | null;
719
726
  progress: string | null;
727
+ compactionCount: number | null;
728
+ peakContextPercent: number | null;
729
+ totalContextTokensUsed: number | null;
730
+ contextWindowSize: number | null;
720
731
  };
721
732
 
722
733
  function rowToAgentTask(row: AgentTaskRow): AgentTask {
@@ -759,6 +770,10 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
759
770
  workflowRunId: row.workflowRunId ?? undefined,
760
771
  workflowRunStepId: row.workflowRunStepId ?? undefined,
761
772
  outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
773
+ compactionCount: row.compactionCount ?? undefined,
774
+ peakContextPercent: row.peakContextPercent ?? undefined,
775
+ totalContextTokensUsed: row.totalContextTokensUsed ?? undefined,
776
+ contextWindowSize: row.contextWindowSize ?? undefined,
762
777
  createdAt: row.createdAt,
763
778
  lastUpdatedAt: row.lastUpdatedAt,
764
779
  finishedAt: row.finishedAt ?? undefined,
@@ -936,6 +951,25 @@ export function updateTaskClaudeSessionId(
936
951
  return row ? rowToAgentTask(row) : null;
937
952
  }
938
953
 
954
+ export function updateTaskVcs(
955
+ taskId: string,
956
+ vcs: {
957
+ vcsProvider: "github" | "gitlab";
958
+ vcsRepo: string;
959
+ vcsNumber: number;
960
+ vcsUrl: string;
961
+ },
962
+ ): AgentTask | null {
963
+ const row = getDb()
964
+ .prepare<AgentTaskRow, [string, string, number, string, string, string]>(
965
+ `UPDATE agent_tasks
966
+ SET vcsProvider = ?, vcsRepo = ?, vcsNumber = ?, vcsUrl = ?, lastUpdatedAt = ?
967
+ WHERE id = ? RETURNING *`,
968
+ )
969
+ .get(vcs.vcsProvider, vcs.vcsRepo, vcs.vcsNumber, vcs.vcsUrl, new Date().toISOString(), taskId);
970
+ return row ? rowToAgentTask(row) : null;
971
+ }
972
+
939
973
  export function getTasksByAgentId(agentId: string): AgentTask[] {
940
974
  return taskQueries.getByAgentId().all(agentId).map(rowToAgentTask);
941
975
  }
@@ -7391,3 +7425,499 @@ export function toggleAgentSkill(agentId: string, skillId: string, isActive: boo
7391
7425
  .run(isActive ? 1 : 0, agentId, skillId);
7392
7426
  return result.changes > 0;
7393
7427
  }
7428
+
7429
+ // ── MCP Servers ──────────────────────────────────────────────────────────
7430
+
7431
+ type McpServerRow = {
7432
+ id: string;
7433
+ name: string;
7434
+ description: string | null;
7435
+ scope: string;
7436
+ ownerAgentId: string | null;
7437
+ transport: string;
7438
+ command: string | null;
7439
+ args: string | null;
7440
+ url: string | null;
7441
+ headers: string | null;
7442
+ envConfigKeys: string | null;
7443
+ headerConfigKeys: string | null;
7444
+ isEnabled: number;
7445
+ version: number;
7446
+ createdAt: string;
7447
+ lastUpdatedAt: string;
7448
+ };
7449
+
7450
+ type AgentMcpServerRow = {
7451
+ id: string;
7452
+ agentId: string;
7453
+ mcpServerId: string;
7454
+ isActive: number;
7455
+ installedAt: string;
7456
+ };
7457
+
7458
+ type McpServerWithInstallRow = McpServerRow & { isActive: number; installedAt: string };
7459
+
7460
+ function rowToMcpServer(row: McpServerRow): McpServer {
7461
+ return {
7462
+ id: row.id,
7463
+ name: row.name,
7464
+ description: row.description,
7465
+ scope: row.scope as McpServerScope,
7466
+ ownerAgentId: row.ownerAgentId,
7467
+ transport: row.transport as McpServerTransport,
7468
+ command: row.command,
7469
+ args: row.args,
7470
+ url: row.url,
7471
+ headers: row.headers,
7472
+ envConfigKeys: row.envConfigKeys,
7473
+ headerConfigKeys: row.headerConfigKeys,
7474
+ isEnabled: row.isEnabled === 1,
7475
+ version: row.version,
7476
+ createdAt: row.createdAt,
7477
+ lastUpdatedAt: row.lastUpdatedAt,
7478
+ };
7479
+ }
7480
+
7481
+ function rowToAgentMcpServer(row: AgentMcpServerRow): AgentMcpServer {
7482
+ return {
7483
+ id: row.id,
7484
+ agentId: row.agentId,
7485
+ mcpServerId: row.mcpServerId,
7486
+ isActive: row.isActive === 1,
7487
+ installedAt: row.installedAt,
7488
+ };
7489
+ }
7490
+
7491
+ function rowToMcpServerWithInstall(row: McpServerWithInstallRow): McpServerWithInstallInfo {
7492
+ return {
7493
+ ...rowToMcpServer(row),
7494
+ isActive: row.isActive === 1,
7495
+ installedAt: row.installedAt,
7496
+ };
7497
+ }
7498
+
7499
+ export interface McpServerInsert {
7500
+ name: string;
7501
+ transport: McpServerTransport;
7502
+ description?: string;
7503
+ scope?: McpServerScope;
7504
+ ownerAgentId?: string;
7505
+ command?: string;
7506
+ args?: string;
7507
+ url?: string;
7508
+ headers?: string;
7509
+ envConfigKeys?: string;
7510
+ headerConfigKeys?: string;
7511
+ }
7512
+
7513
+ export function createMcpServer(data: McpServerInsert): McpServer {
7514
+ const id = crypto.randomUUID();
7515
+ const now = new Date().toISOString();
7516
+
7517
+ const row = getDb()
7518
+ .prepare<McpServerRow, (string | number | null)[]>(
7519
+ `INSERT INTO mcp_servers (
7520
+ id, name, description, scope, ownerAgentId, transport,
7521
+ command, args, url, headers,
7522
+ envConfigKeys, headerConfigKeys,
7523
+ isEnabled, version, createdAt, lastUpdatedAt
7524
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?) RETURNING *`,
7525
+ )
7526
+ .get(
7527
+ id,
7528
+ data.name,
7529
+ data.description ?? null,
7530
+ data.scope ?? "agent",
7531
+ data.ownerAgentId ?? null,
7532
+ data.transport,
7533
+ data.command ?? null,
7534
+ data.args ?? null,
7535
+ data.url ?? null,
7536
+ data.headers ?? null,
7537
+ data.envConfigKeys ?? null,
7538
+ data.headerConfigKeys ?? null,
7539
+ now,
7540
+ now,
7541
+ );
7542
+
7543
+ if (!row) throw new Error("Failed to create MCP server");
7544
+ return rowToMcpServer(row);
7545
+ }
7546
+
7547
+ export function updateMcpServer(
7548
+ id: string,
7549
+ updates: Partial<McpServerInsert> & { isEnabled?: boolean },
7550
+ ): McpServer | null {
7551
+ const existing = getMcpServerById(id);
7552
+ if (!existing) return null;
7553
+
7554
+ const now = new Date().toISOString();
7555
+ const sets: string[] = ["lastUpdatedAt = ?"];
7556
+ const params: (string | number | null)[] = [now];
7557
+
7558
+ if (updates.name !== undefined) {
7559
+ sets.push("name = ?");
7560
+ params.push(updates.name);
7561
+ }
7562
+ if (updates.description !== undefined) {
7563
+ sets.push("description = ?");
7564
+ params.push(updates.description ?? null);
7565
+ }
7566
+ if (updates.scope !== undefined) {
7567
+ sets.push("scope = ?");
7568
+ params.push(updates.scope);
7569
+ }
7570
+ if (updates.transport !== undefined) {
7571
+ sets.push("transport = ?");
7572
+ params.push(updates.transport);
7573
+ }
7574
+ if (updates.command !== undefined) {
7575
+ sets.push("command = ?");
7576
+ params.push(updates.command ?? null);
7577
+ }
7578
+ if (updates.args !== undefined) {
7579
+ sets.push("args = ?");
7580
+ params.push(updates.args ?? null);
7581
+ }
7582
+ if (updates.url !== undefined) {
7583
+ sets.push("url = ?");
7584
+ params.push(updates.url ?? null);
7585
+ }
7586
+ if (updates.headers !== undefined) {
7587
+ sets.push("headers = ?");
7588
+ params.push(updates.headers ?? null);
7589
+ }
7590
+ if (updates.envConfigKeys !== undefined) {
7591
+ sets.push("envConfigKeys = ?");
7592
+ params.push(updates.envConfigKeys ?? null);
7593
+ }
7594
+ if (updates.headerConfigKeys !== undefined) {
7595
+ sets.push("headerConfigKeys = ?");
7596
+ params.push(updates.headerConfigKeys ?? null);
7597
+ }
7598
+ if (updates.isEnabled !== undefined) {
7599
+ sets.push("isEnabled = ?");
7600
+ params.push(updates.isEnabled ? 1 : 0);
7601
+ }
7602
+ if (updates.ownerAgentId !== undefined) {
7603
+ sets.push("ownerAgentId = ?");
7604
+ params.push(updates.ownerAgentId ?? null);
7605
+ }
7606
+
7607
+ // Bump version on config changes
7608
+ const configFields = [
7609
+ "command",
7610
+ "args",
7611
+ "url",
7612
+ "headers",
7613
+ "envConfigKeys",
7614
+ "headerConfigKeys",
7615
+ "transport",
7616
+ ];
7617
+ if (configFields.some((f) => (updates as Record<string, unknown>)[f] !== undefined)) {
7618
+ sets.push("version = version + 1");
7619
+ }
7620
+
7621
+ params.push(id);
7622
+ const row = getDb()
7623
+ .prepare<McpServerRow, (string | number | null)[]>(
7624
+ `UPDATE mcp_servers SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
7625
+ )
7626
+ .get(...params);
7627
+
7628
+ return row ? rowToMcpServer(row) : null;
7629
+ }
7630
+
7631
+ export function deleteMcpServer(id: string): boolean {
7632
+ const result = getDb().prepare("DELETE FROM mcp_servers WHERE id = ?").run(id);
7633
+ return result.changes > 0;
7634
+ }
7635
+
7636
+ export function getMcpServerById(id: string): McpServer | null {
7637
+ const row = getDb()
7638
+ .prepare<McpServerRow, [string]>("SELECT * FROM mcp_servers WHERE id = ?")
7639
+ .get(id);
7640
+ return row ? rowToMcpServer(row) : null;
7641
+ }
7642
+
7643
+ export function getMcpServerByName(
7644
+ name: string,
7645
+ scope: McpServerScope,
7646
+ ownerAgentId: string | null,
7647
+ ): McpServer | null {
7648
+ const row = getDb()
7649
+ .prepare<McpServerRow, [string, string, string]>(
7650
+ "SELECT * FROM mcp_servers WHERE name = ? AND scope = ? AND COALESCE(ownerAgentId, '') = ?",
7651
+ )
7652
+ .get(name, scope, ownerAgentId ?? "");
7653
+ return row ? rowToMcpServer(row) : null;
7654
+ }
7655
+
7656
+ export interface McpServerFilters {
7657
+ scope?: McpServerScope;
7658
+ ownerAgentId?: string;
7659
+ transport?: McpServerTransport;
7660
+ isEnabled?: boolean;
7661
+ search?: string;
7662
+ }
7663
+
7664
+ export function listMcpServers(filters?: McpServerFilters): McpServer[] {
7665
+ let query = "SELECT * FROM mcp_servers WHERE 1=1";
7666
+ const params: (string | number)[] = [];
7667
+
7668
+ if (filters?.scope) {
7669
+ query += " AND scope = ?";
7670
+ params.push(filters.scope);
7671
+ }
7672
+ if (filters?.ownerAgentId) {
7673
+ query += " AND ownerAgentId = ?";
7674
+ params.push(filters.ownerAgentId);
7675
+ }
7676
+ if (filters?.transport) {
7677
+ query += " AND transport = ?";
7678
+ params.push(filters.transport);
7679
+ }
7680
+ if (filters?.isEnabled !== undefined) {
7681
+ query += " AND isEnabled = ?";
7682
+ params.push(filters.isEnabled ? 1 : 0);
7683
+ }
7684
+ if (filters?.search) {
7685
+ query += " AND (name LIKE ? OR description LIKE ?)";
7686
+ const term = `%${filters.search}%`;
7687
+ params.push(term, term);
7688
+ }
7689
+
7690
+ query += " ORDER BY name ASC";
7691
+
7692
+ return getDb()
7693
+ .prepare<McpServerRow, (string | number)[]>(query)
7694
+ .all(...params)
7695
+ .map(rowToMcpServer);
7696
+ }
7697
+
7698
+ export function installMcpServer(agentId: string, mcpServerId: string): AgentMcpServer {
7699
+ const id = crypto.randomUUID();
7700
+ const now = new Date().toISOString();
7701
+
7702
+ const row = getDb()
7703
+ .prepare<AgentMcpServerRow, [string, string, string, string]>(
7704
+ `INSERT INTO agent_mcp_servers (id, agentId, mcpServerId, isActive, installedAt)
7705
+ VALUES (?, ?, ?, 1, ?)
7706
+ ON CONFLICT(agentId, mcpServerId) DO UPDATE SET isActive = 1
7707
+ RETURNING *`,
7708
+ )
7709
+ .get(id, agentId, mcpServerId, now);
7710
+
7711
+ if (!row) throw new Error("Failed to install MCP server");
7712
+ return rowToAgentMcpServer(row);
7713
+ }
7714
+
7715
+ export function uninstallMcpServer(agentId: string, mcpServerId: string): boolean {
7716
+ const result = getDb()
7717
+ .prepare("DELETE FROM agent_mcp_servers WHERE agentId = ? AND mcpServerId = ?")
7718
+ .run(agentId, mcpServerId);
7719
+ return result.changes > 0;
7720
+ }
7721
+
7722
+ export function getAgentMcpServers(agentId: string, activeOnly = true): McpServerWithInstallInfo[] {
7723
+ const query = `
7724
+ SELECT ms.*, ams.isActive, ams.installedAt
7725
+ FROM mcp_servers ms
7726
+ JOIN agent_mcp_servers ams ON ms.id = ams.mcpServerId
7727
+ WHERE ams.agentId = ?
7728
+ ${activeOnly ? "AND ams.isActive = 1" : ""}
7729
+ AND ms.isEnabled = 1
7730
+ ORDER BY ms.name ASC
7731
+ `;
7732
+
7733
+ return getDb()
7734
+ .prepare<McpServerWithInstallRow, [string]>(query)
7735
+ .all(agentId)
7736
+ .map(rowToMcpServerWithInstall);
7737
+ }
7738
+
7739
+ // ============================================================================
7740
+ // Context Usage Snapshots
7741
+ // ============================================================================
7742
+
7743
+ type ContextSnapshotRow = {
7744
+ id: string;
7745
+ taskId: string;
7746
+ agentId: string;
7747
+ sessionId: string;
7748
+ contextUsedTokens: number | null;
7749
+ contextTotalTokens: number | null;
7750
+ contextPercent: number | null;
7751
+ eventType: ContextSnapshotEventType;
7752
+ compactTrigger: string | null;
7753
+ preCompactTokens: number | null;
7754
+ cumulativeInputTokens: number;
7755
+ cumulativeOutputTokens: number;
7756
+ createdAt: string;
7757
+ };
7758
+
7759
+ function rowToContextSnapshot(row: ContextSnapshotRow): ContextSnapshot {
7760
+ return {
7761
+ id: row.id,
7762
+ taskId: row.taskId,
7763
+ agentId: row.agentId,
7764
+ sessionId: row.sessionId,
7765
+ contextUsedTokens: row.contextUsedTokens ?? undefined,
7766
+ contextTotalTokens: row.contextTotalTokens ?? undefined,
7767
+ contextPercent: row.contextPercent ?? undefined,
7768
+ eventType: row.eventType,
7769
+ compactTrigger: (row.compactTrigger as "auto" | "manual" | null) ?? undefined,
7770
+ preCompactTokens: row.preCompactTokens ?? undefined,
7771
+ cumulativeInputTokens: row.cumulativeInputTokens,
7772
+ cumulativeOutputTokens: row.cumulativeOutputTokens,
7773
+ createdAt: row.createdAt,
7774
+ };
7775
+ }
7776
+
7777
+ const contextSnapshotQueries = {
7778
+ insert: () =>
7779
+ getDb().prepare<
7780
+ ContextSnapshotRow,
7781
+ [
7782
+ string,
7783
+ string,
7784
+ string,
7785
+ string,
7786
+ number | null,
7787
+ number | null,
7788
+ number | null,
7789
+ string,
7790
+ string | null,
7791
+ number | null,
7792
+ number,
7793
+ number,
7794
+ string,
7795
+ ]
7796
+ >(
7797
+ `INSERT INTO task_context_snapshots (id, taskId, agentId, sessionId, contextUsedTokens, contextTotalTokens, contextPercent, eventType, compactTrigger, preCompactTokens, cumulativeInputTokens, cumulativeOutputTokens, createdAt)
7798
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
7799
+ ),
7800
+
7801
+ getByTaskId: () =>
7802
+ getDb().prepare<ContextSnapshotRow, [string, number]>(
7803
+ "SELECT * FROM task_context_snapshots WHERE taskId = ? ORDER BY createdAt ASC LIMIT ?",
7804
+ ),
7805
+
7806
+ getBySessionId: () =>
7807
+ getDb().prepare<ContextSnapshotRow, [string, number]>(
7808
+ "SELECT * FROM task_context_snapshots WHERE sessionId = ? ORDER BY createdAt ASC LIMIT ?",
7809
+ ),
7810
+ };
7811
+
7812
+ export interface CreateContextSnapshotInput {
7813
+ taskId: string;
7814
+ agentId: string;
7815
+ sessionId: string;
7816
+ contextUsedTokens?: number;
7817
+ contextTotalTokens?: number;
7818
+ contextPercent?: number;
7819
+ eventType: ContextSnapshotEventType;
7820
+ compactTrigger?: "auto" | "manual";
7821
+ preCompactTokens?: number;
7822
+ cumulativeInputTokens?: number;
7823
+ cumulativeOutputTokens?: number;
7824
+ }
7825
+
7826
+ export function createContextSnapshot(input: CreateContextSnapshotInput): ContextSnapshot {
7827
+ const id = crypto.randomUUID();
7828
+ const now = new Date().toISOString();
7829
+
7830
+ contextSnapshotQueries
7831
+ .insert()
7832
+ .run(
7833
+ id,
7834
+ input.taskId,
7835
+ input.agentId,
7836
+ input.sessionId,
7837
+ input.contextUsedTokens ?? null,
7838
+ input.contextTotalTokens ?? null,
7839
+ input.contextPercent ?? null,
7840
+ input.eventType,
7841
+ input.compactTrigger ?? null,
7842
+ input.preCompactTokens ?? null,
7843
+ input.cumulativeInputTokens ?? 0,
7844
+ input.cumulativeOutputTokens ?? 0,
7845
+ now,
7846
+ );
7847
+
7848
+ // Update aggregate columns on agent_tasks
7849
+ if (input.contextPercent != null) {
7850
+ getDb()
7851
+ .prepare(
7852
+ `UPDATE agent_tasks SET peakContextPercent = MAX(COALESCE(peakContextPercent, 0), ?)
7853
+ WHERE id = ?`,
7854
+ )
7855
+ .run(input.contextPercent, input.taskId);
7856
+ }
7857
+
7858
+ if (input.eventType === "compaction") {
7859
+ getDb()
7860
+ .prepare(
7861
+ "UPDATE agent_tasks SET compactionCount = COALESCE(compactionCount, 0) + 1 WHERE id = ?",
7862
+ )
7863
+ .run(input.taskId);
7864
+ }
7865
+
7866
+ if (input.eventType === "completion") {
7867
+ getDb()
7868
+ .prepare(
7869
+ `UPDATE agent_tasks SET totalContextTokensUsed = ?, contextWindowSize = ?
7870
+ WHERE id = ?`,
7871
+ )
7872
+ .run(input.contextUsedTokens ?? null, input.contextTotalTokens ?? null, input.taskId);
7873
+ }
7874
+
7875
+ return {
7876
+ id,
7877
+ taskId: input.taskId,
7878
+ agentId: input.agentId,
7879
+ sessionId: input.sessionId,
7880
+ contextUsedTokens: input.contextUsedTokens,
7881
+ contextTotalTokens: input.contextTotalTokens,
7882
+ contextPercent: input.contextPercent,
7883
+ eventType: input.eventType,
7884
+ compactTrigger: input.compactTrigger,
7885
+ preCompactTokens: input.preCompactTokens,
7886
+ cumulativeInputTokens: input.cumulativeInputTokens ?? 0,
7887
+ cumulativeOutputTokens: input.cumulativeOutputTokens ?? 0,
7888
+ createdAt: now,
7889
+ };
7890
+ }
7891
+
7892
+ export function getContextSnapshotsByTaskId(taskId: string, limit = 500): ContextSnapshot[] {
7893
+ return contextSnapshotQueries.getByTaskId().all(taskId, limit).map(rowToContextSnapshot);
7894
+ }
7895
+
7896
+ export function getContextSnapshotsBySessionId(sessionId: string, limit = 500): ContextSnapshot[] {
7897
+ return contextSnapshotQueries.getBySessionId().all(sessionId, limit).map(rowToContextSnapshot);
7898
+ }
7899
+
7900
+ export interface ContextSummary {
7901
+ compactionCount: number;
7902
+ peakContextPercent: number | null;
7903
+ totalContextTokensUsed: number | null;
7904
+ contextWindowSize: number | null;
7905
+ snapshotCount: number;
7906
+ }
7907
+
7908
+ export function getContextSummaryByTaskId(taskId: string): ContextSummary {
7909
+ const task = getTaskById(taskId);
7910
+ const countRow = getDb()
7911
+ .prepare<{ cnt: number }, [string]>(
7912
+ "SELECT COUNT(*) as cnt FROM task_context_snapshots WHERE taskId = ?",
7913
+ )
7914
+ .get(taskId);
7915
+
7916
+ return {
7917
+ compactionCount: task?.compactionCount ?? 0,
7918
+ peakContextPercent: task?.peakContextPercent ?? null,
7919
+ totalContextTokensUsed: task?.totalContextTokensUsed ?? null,
7920
+ contextWindowSize: task?.contextWindowSize ?? null,
7921
+ snapshotCount: countRow?.cnt ?? 0,
7922
+ };
7923
+ }