diff --git a/apps/server/cli/index.ts b/apps/server/cli/index.ts index cda8766..1e37383 100644 --- a/apps/server/cli/index.ts +++ b/apps/server/cli/index.ts @@ -13,6 +13,8 @@ import { createDefaultTrpcClient } from './trpc-client.js'; import { createContainer } from '../container.js'; import { findWorkspaceRoot, writeCwrc, defaultCwConfig } from '../config/index.js'; import { createModuleLogger } from '../logger/index.js'; +import { backfillMetricsFromPath } from '../scripts/backfill-metrics.js'; +import { getDbPath } from '../db/index.js'; /** Environment variable for custom port */ const CW_PORT_ENV = 'CW_PORT'; @@ -134,6 +136,22 @@ export function createCli(serverHandler?: (port?: number) => Promise): Com } }); + // Backfill metrics command (standalone — no server, no tRPC) + program + .command('backfill-metrics') + .description('Populate agent_metrics table from existing agent_log_chunks (run once after upgrading)') + .option('--db ', 'Path to the SQLite database file (defaults to configured DB path)') + .action(async (options: { db?: string }) => { + const dbPath = options.db ?? getDbPath(); + console.log(`Backfilling metrics from ${dbPath}...`); + try { + await backfillMetricsFromPath(dbPath); + } catch (error) { + console.error('Backfill failed:', (error as Error).message); + process.exit(1); + } + }); + // Agent command group const agentCommand = program .command('agent') diff --git a/apps/server/db/repositories/drizzle/agent-metrics.test.ts b/apps/server/db/repositories/drizzle/agent-metrics.test.ts new file mode 100644 index 0000000..2753a03 --- /dev/null +++ b/apps/server/db/repositories/drizzle/agent-metrics.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { createTestDatabase } from './test-helpers.js'; +import { agentMetrics } from '../../schema.js'; + +describe('agentMetrics table', () => { + it('select from empty agentMetrics returns []', async () => { + const db = createTestDatabase(); + const rows = await db.select().from(agentMetrics); + expect(rows).toEqual([]); + }); + + it('insert and select a metrics row round-trips correctly', async () => { + const db = createTestDatabase(); + await db.insert(agentMetrics).values({ + agentId: 'agent-abc', + questionsCount: 3, + subagentsCount: 1, + compactionsCount: 0, + updatedAt: new Date('2024-01-01T00:00:00Z'), + }); + const rows = await db.select().from(agentMetrics); + expect(rows).toHaveLength(1); + expect(rows[0].agentId).toBe('agent-abc'); + expect(rows[0].questionsCount).toBe(3); + expect(rows[0].subagentsCount).toBe(1); + expect(rows[0].compactionsCount).toBe(0); + }); + + it('agentId is primary key — duplicate insert throws', async () => { + const db = createTestDatabase(); + await db.insert(agentMetrics).values({ + agentId: 'agent-dup', + questionsCount: 0, + subagentsCount: 0, + compactionsCount: 0, + updatedAt: new Date(), + }); + await expect( + db.insert(agentMetrics).values({ + agentId: 'agent-dup', + questionsCount: 1, + subagentsCount: 0, + compactionsCount: 0, + updatedAt: new Date(), + }) + ).rejects.toThrow(); + }); +}); diff --git a/apps/server/db/repositories/drizzle/log-chunk.test.ts b/apps/server/db/repositories/drizzle/log-chunk.test.ts new file mode 100644 index 0000000..0c48d6a --- /dev/null +++ b/apps/server/db/repositories/drizzle/log-chunk.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzleLogChunkRepository } from './log-chunk.js'; +import { createTestDatabase } from './test-helpers.js'; +import type { DrizzleDatabase } from '../../index.js'; + +describe('DrizzleLogChunkRepository', () => { + let db: DrizzleDatabase; + let repo: DrizzleLogChunkRepository; + const testAgentId = 'agent-test-001'; + + beforeEach(() => { + db = createTestDatabase(); + repo = new DrizzleLogChunkRepository(db); + }); + + it('AskUserQuestion chunk — questionsCount upserted correctly', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 2, + subagentsCount: 0, + compactionsCount: 0, + }]); + }); + + it('Agent tool chunk — subagentsCount incremented', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 0, + subagentsCount: 1, + compactionsCount: 0, + }]); + }); + + it('Compaction event — compactionsCount incremented', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'system', subtype: 'init', source: 'compact' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 0, + subagentsCount: 0, + compactionsCount: 1, + }]); + }); + + it('Irrelevant chunk type — no metrics row created', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'text', text: 'hello' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([]); + }); + + it('Malformed JSON chunk — chunk persisted, metrics row absent', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: 'not-valid-json', + }); + const chunks = await repo.findByAgentId(testAgentId); + expect(chunks).toHaveLength(1); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([]); + }); + + it('Multiple inserts, same agent — counts accumulate additively', async () => { + // 3 Agent tool chunks + for (let i = 0; i < 3; i++) { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent' }), + }); + } + // 1 AskUserQuestion with 2 questions + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 2, + subagentsCount: 3, + compactionsCount: 0, + }]); + }); + + it('findMetricsByAgentIds with empty array — returns []', async () => { + const metrics = await repo.findMetricsByAgentIds([]); + expect(metrics).toEqual([]); + }); + + it('findMetricsByAgentIds with agentId that has no metrics row — returns []', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'text', text: 'hello' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([]); + }); +}); diff --git a/apps/server/db/repositories/drizzle/log-chunk.ts b/apps/server/db/repositories/drizzle/log-chunk.ts index 9d4632b..5d30c0b 100644 --- a/apps/server/db/repositories/drizzle/log-chunk.ts +++ b/apps/server/db/repositories/drizzle/log-chunk.ts @@ -4,10 +4,10 @@ * Implements LogChunkRepository interface using Drizzle ORM. */ -import { eq, asc, max, inArray } from 'drizzle-orm'; +import { eq, asc, max, inArray, sql } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import type { DrizzleDatabase } from '../../index.js'; -import { agentLogChunks } from '../../schema.js'; +import { agentLogChunks, agentMetrics } from '../../schema.js'; import type { LogChunkRepository } from '../log-chunk-repository.js'; export class DrizzleLogChunkRepository implements LogChunkRepository { @@ -19,13 +19,58 @@ export class DrizzleLogChunkRepository implements LogChunkRepository { sessionNumber: number; content: string; }): Promise { - await this.db.insert(agentLogChunks).values({ - id: nanoid(), - agentId: data.agentId, - agentName: data.agentName, - sessionNumber: data.sessionNumber, - content: data.content, - createdAt: new Date(), + // better-sqlite3 is synchronous — transaction callback must be sync, use .run() not await + this.db.transaction((tx) => { + // 1. Always insert the chunk row first + tx.insert(agentLogChunks).values({ + id: nanoid(), + agentId: data.agentId, + agentName: data.agentName, + sessionNumber: data.sessionNumber, + content: data.content, + createdAt: new Date(), + }).run(); + + // 2. Parse content and determine metric increments + // Wrap only the parse + upsert block — chunk insert is not rolled back on parse failure + try { + const parsed = JSON.parse(data.content); + let deltaQuestions = 0; + let deltaSubagents = 0; + let deltaCompactions = 0; + + if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') { + deltaQuestions = parsed.input?.questions?.length ?? 0; + } else if (parsed.type === 'tool_use' && parsed.name === 'Agent') { + deltaSubagents = 1; + } else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') { + deltaCompactions = 1; + } + + // 3. Only upsert if there is something to increment + if (deltaQuestions > 0 || deltaSubagents > 0 || deltaCompactions > 0) { + tx.insert(agentMetrics) + .values({ + agentId: data.agentId, + questionsCount: deltaQuestions, + subagentsCount: deltaSubagents, + compactionsCount: deltaCompactions, + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: agentMetrics.agentId, + set: { + questionsCount: sql`${agentMetrics.questionsCount} + ${deltaQuestions}`, + subagentsCount: sql`${agentMetrics.subagentsCount} + ${deltaSubagents}`, + compactionsCount: sql`${agentMetrics.compactionsCount} + ${deltaCompactions}`, + updatedAt: new Date(), + }, + }) + .run(); + } + } catch { + // Malformed JSON — skip metric upsert, chunk insert already committed within transaction + } }); } @@ -69,4 +114,22 @@ export class DrizzleLogChunkRepository implements LogChunkRepository { return result[0]?.maxSession ?? 0; } + + async findMetricsByAgentIds(agentIds: string[]): Promise<{ + agentId: string; + questionsCount: number; + subagentsCount: number; + compactionsCount: number; + }[]> { + if (agentIds.length === 0) return []; + return this.db + .select({ + agentId: agentMetrics.agentId, + questionsCount: agentMetrics.questionsCount, + subagentsCount: agentMetrics.subagentsCount, + compactionsCount: agentMetrics.compactionsCount, + }) + .from(agentMetrics) + .where(inArray(agentMetrics.agentId, agentIds)); + } } diff --git a/apps/server/db/repositories/log-chunk-repository.ts b/apps/server/db/repositories/log-chunk-repository.ts index 0283a0b..1e1f8a2 100644 --- a/apps/server/db/repositories/log-chunk-repository.ts +++ b/apps/server/db/repositories/log-chunk-repository.ts @@ -27,4 +27,16 @@ export interface LogChunkRepository { deleteByAgentId(agentId: string): Promise; getSessionCount(agentId: string): Promise; + + /** + * Batch-fetch pre-computed metrics for multiple agent IDs. + * Returns one row per agent that has metrics. Agents with no + * matching row in agent_metrics are omitted (not returned as zeros). + */ + findMetricsByAgentIds(agentIds: string[]): Promise<{ + agentId: string; + questionsCount: number; + subagentsCount: number; + compactionsCount: number; + }[]>; } diff --git a/apps/server/db/schema.ts b/apps/server/db/schema.ts index ce35cec..cd0f1d3 100644 --- a/apps/server/db/schema.ts +++ b/apps/server/db/schema.ts @@ -513,6 +513,21 @@ export const agentLogChunks = sqliteTable('agent_log_chunks', { export type AgentLogChunk = InferSelectModel; export type NewAgentLogChunk = InferInsertModel; +// ============================================================================ +// AGENT METRICS +// ============================================================================ + +export const agentMetrics = sqliteTable('agent_metrics', { + agentId: text('agent_id').primaryKey(), + questionsCount: integer('questions_count').notNull().default(0), + subagentsCount: integer('subagents_count').notNull().default(0), + compactionsCount: integer('compactions_count').notNull().default(0), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), +}); + +export type AgentMetrics = InferSelectModel; +export type NewAgentMetrics = InferInsertModel; + // ============================================================================ // CONVERSATIONS (inter-agent communication) // ============================================================================ diff --git a/apps/server/drizzle/0037_eager_devos.sql b/apps/server/drizzle/0037_eager_devos.sql new file mode 100644 index 0000000..3b09557 --- /dev/null +++ b/apps/server/drizzle/0037_eager_devos.sql @@ -0,0 +1,7 @@ +CREATE TABLE `agent_metrics` ( + `agent_id` text PRIMARY KEY NOT NULL, + `questions_count` integer DEFAULT 0 NOT NULL, + `subagents_count` integer DEFAULT 0 NOT NULL, + `compactions_count` integer DEFAULT 0 NOT NULL, + `updated_at` integer NOT NULL +); diff --git a/apps/server/drizzle/meta/0035_snapshot.json b/apps/server/drizzle/meta/0035_snapshot.json index d735a97..dd693db 100644 --- a/apps/server/drizzle/meta/0035_snapshot.json +++ b/apps/server/drizzle/meta/0035_snapshot.json @@ -2,7 +2,7 @@ "version": "6", "dialect": "sqlite", "id": "c84e499f-7df8-4091-b2a5-6b12847898bd", - "prevId": "5fbe1151-1dfb-4b0c-a7fa-2177369543fd", + "prevId": "443071fe-31d6-498a-9f4a-4a3ff24a46fc", "tables": { "accounts": { "name": "accounts", diff --git a/apps/server/drizzle/meta/0036_snapshot.json b/apps/server/drizzle/meta/0036_snapshot.json index f60484b..5a2e6bc 100644 --- a/apps/server/drizzle/meta/0036_snapshot.json +++ b/apps/server/drizzle/meta/0036_snapshot.json @@ -1,8 +1,8 @@ { - "id": "f85b9df3-dead-4c46-90ac-cf36bcaa6eb4", - "prevId": "c0b6d7d3-c9da-440a-9fb8-9dd88df5672a", "version": "6", "dialect": "sqlite", + "id": "f85b9df3-dead-4c46-90ac-cf36bcaa6eb4", + "prevId": "c84e499f-7df8-4091-b2a5-6b12847898bd", "tables": { "accounts": { "name": "accounts", @@ -29,11 +29,18 @@ "autoincrement": false, "default": "'claude'" }, - "config_dir": { - "name": "config_dir", + "config_json": { + "name": "config_json", "type": "text", "primaryKey": false, - "notNull": true, + "notNull": false, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false, "autoincrement": false }, "is_exhausted": { @@ -87,6 +94,67 @@ "uniqueConstraints": {}, "checkConstraints": {} }, + "agent_log_chunks": { + "name": "agent_log_chunks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_number": { + "name": "session_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_log_chunks_agent_id_idx": { + "name": "agent_log_chunks_agent_id_idx", + "columns": [ + "agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "agents": { "name": "agents", "columns": { @@ -170,9 +238,9 @@ "notNull": false, "autoincrement": false }, - "prompt": { - "name": "prompt", - "type": "text", + "exit_code": { + "name": "exit_code", + "type": "integer", "primaryKey": false, "notNull": false, "autoincrement": false @@ -211,6 +279,20 @@ "primaryKey": false, "notNull": true, "autoincrement": false + }, + "user_dismissed_at": { + "name": "user_dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false } }, "indexes": { @@ -226,41 +308,715 @@ "agents_task_id_tasks_id_fk": { "name": "agents_task_id_tasks_id_fk", "tableFrom": "agents", + "tableTo": "tasks", "columnsFrom": [ "task_id" ], - "tableTo": "tasks", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" }, "agents_initiative_id_initiatives_id_fk": { "name": "agents_initiative_id_initiatives_id_fk", "tableFrom": "agents", + "tableTo": "initiatives", "columnsFrom": [ "initiative_id" ], - "tableTo": "initiatives", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" }, "agents_account_id_accounts_id_fk": { "name": "agents_account_id_accounts_id_fk", "tableFrom": "agents", + "tableTo": "accounts", "columnsFrom": [ "account_id" ], - "tableTo": "accounts", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_set_entries": { + "name": "change_set_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_state": { + "name": "previous_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_state": { + "name": "new_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_set_entries_change_set_id_idx": { + "name": "change_set_entries_change_set_id_idx", + "columns": [ + "change_set_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_set_entries_change_set_id_change_sets_id_fk": { + "name": "change_set_entries_change_set_id_change_sets_id_fk", + "tableFrom": "change_set_entries", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_sets": { + "name": "change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'applied'" + }, + "reverted_at": { + "name": "reverted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_sets_initiative_id_idx": { + "name": "change_sets_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_sets_agent_id_agents_id_fk": { + "name": "change_sets_agent_id_agents_id_fk", + "tableFrom": "change_sets", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "change_sets_initiative_id_initiatives_id_fk": { + "name": "change_sets_initiative_id_initiatives_id_fk", + "tableFrom": "change_sets", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_messages_session_id_idx": { + "name": "chat_messages_session_id_idx", + "columns": [ + "chat_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_messages_chat_session_id_chat_sessions_id_fk": { + "name": "chat_messages_chat_session_id_chat_sessions_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_change_set_id_change_sets_id_fk": { + "name": "chat_messages_change_set_id_change_sets_id_fk", + "tableFrom": "chat_messages", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_sessions": { + "name": "chat_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_sessions_target_idx": { + "name": "chat_sessions_target_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": false + }, + "chat_sessions_initiative_id_idx": { + "name": "chat_sessions_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_sessions_initiative_id_initiatives_id_fk": { + "name": "chat_sessions_initiative_id_initiatives_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_agent_id_agents_id_fk": { + "name": "chat_sessions_agent_id_agents_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "from_agent_id": { + "name": "from_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_agent_id": { + "name": "to_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "conversations_to_agent_status_idx": { + "name": "conversations_to_agent_status_idx", + "columns": [ + "to_agent_id", + "status" + ], + "isUnique": false + }, + "conversations_from_agent_idx": { + "name": "conversations_from_agent_idx", + "columns": [ + "from_agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "conversations_from_agent_id_agents_id_fk": { + "name": "conversations_from_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "from_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_to_agent_id_agents_id_fk": { + "name": "conversations_to_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "to_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_initiative_id_initiatives_id_fk": { + "name": "conversations_initiative_id_initiatives_id_fk", + "tableFrom": "conversations", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_phase_id_phases_id_fk": { + "name": "conversations_phase_id_phases_id_fk", + "tableFrom": "conversations", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_task_id_tasks_id_fk": { + "name": "conversations_task_id_tasks_id_fk", + "tableFrom": "conversations", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "errands": { + "name": "errands", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "errands_agent_id_agents_id_fk": { + "name": "errands_agent_id_agents_id_fk", + "tableFrom": "errands", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "errands_project_id_projects_id_fk": { + "name": "errands_project_id_projects_id_fk", + "tableFrom": "errands", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -313,28 +1069,28 @@ "initiative_projects_initiative_id_initiatives_id_fk": { "name": "initiative_projects_initiative_id_initiatives_id_fk", "tableFrom": "initiative_projects", + "tableTo": "initiatives", "columnsFrom": [ "initiative_id" ], - "tableTo": "initiatives", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "initiative_projects_project_id_projects_id_fk": { "name": "initiative_projects_project_id_projects_id_fk", "tableFrom": "initiative_projects", + "tableTo": "projects", "columnsFrom": [ "project_id" ], - "tableTo": "projects", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -358,13 +1114,6 @@ "notNull": true, "autoincrement": false }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, "status": { "name": "status", "type": "text", @@ -373,21 +1122,21 @@ "autoincrement": false, "default": "'active'" }, - "merge_requires_approval": { - "name": "merge_requires_approval", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "merge_target": { - "name": "merge_target", + "branch": { + "name": "branch", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false }, + "execution_mode": { + "name": "execution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'review_per_phase'" + }, "created_at": { "name": "created_at", "type": "integer", @@ -505,41 +1254,41 @@ "messages_sender_id_agents_id_fk": { "name": "messages_sender_id_agents_id_fk", "tableFrom": "messages", + "tableTo": "agents", "columnsFrom": [ "sender_id" ], - "tableTo": "agents", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" }, "messages_recipient_id_agents_id_fk": { "name": "messages_recipient_id_agents_id_fk", "tableFrom": "messages", + "tableTo": "agents", "columnsFrom": [ "recipient_id" ], - "tableTo": "agents", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" }, "messages_parent_message_id_messages_id_fk": { "name": "messages_parent_message_id_messages_id_fk", "tableFrom": "messages", + "tableTo": "messages", "columnsFrom": [ "parent_message_id" ], - "tableTo": "messages", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "set null" + "onDelete": "set null", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -612,28 +1361,28 @@ "pages_initiative_id_initiatives_id_fk": { "name": "pages_initiative_id_initiatives_id_fk", "tableFrom": "pages", + "tableTo": "initiatives", "columnsFrom": [ "initiative_id" ], - "tableTo": "initiatives", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "pages_parent_page_id_pages_id_fk": { "name": "pages_parent_page_id_pages_id_fk", "tableFrom": "pages", + "tableTo": "pages", "columnsFrom": [ "parent_page_id" ], - "tableTo": "pages", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -677,28 +1426,28 @@ "phase_dependencies_phase_id_phases_id_fk": { "name": "phase_dependencies_phase_id_phases_id_fk", "tableFrom": "phase_dependencies", + "tableTo": "phases", "columnsFrom": [ "phase_id" ], - "tableTo": "phases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "phase_dependencies_depends_on_phase_id_phases_id_fk": { "name": "phase_dependencies_depends_on_phase_id_phases_id_fk", "tableFrom": "phase_dependencies", + "tableTo": "phases", "columnsFrom": [ "depends_on_phase_id" ], - "tableTo": "phases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -722,13 +1471,6 @@ "notNull": true, "autoincrement": false }, - "number": { - "name": "number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "name": { "name": "name", "type": "text", @@ -736,8 +1478,8 @@ "notNull": true, "autoincrement": false }, - "description": { - "name": "description", + "content": { + "name": "content", "type": "text", "primaryKey": false, "notNull": false, @@ -751,6 +1493,13 @@ "autoincrement": false, "default": "'pending'" }, + "merge_base": { + "name": "merge_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "integer", @@ -771,96 +1520,15 @@ "phases_initiative_id_initiatives_id_fk": { "name": "phases_initiative_id_initiatives_id_fk", "tableFrom": "phases", + "tableTo": "initiatives", "columnsFrom": [ "initiative_id" ], - "tableTo": "initiatives", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "plans": { - "name": "plans", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "phase_id": { - "name": "phase_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "number": { - "name": "number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'pending'" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "plans_phase_id_phases_id_fk": { - "name": "plans_phase_id_phases_id_fk", - "tableFrom": "plans", - "columnsFrom": [ - "phase_id" - ], - "tableTo": "phases", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -891,6 +1559,21 @@ "notNull": true, "autoincrement": false }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "last_fetched_at": { + "name": "last_fetched_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "integer", @@ -927,6 +1610,137 @@ "uniqueConstraints": {}, "checkConstraints": {} }, + "review_comments": { + "name": "review_comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_type": { + "name": "line_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'you'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved": { + "name": "resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "review_comments_phase_id_idx": { + "name": "review_comments_phase_id_idx", + "columns": [ + "phase_id" + ], + "isUnique": false + }, + "review_comments_parent_id_idx": { + "name": "review_comments_parent_id_idx", + "columns": [ + "parent_comment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_comments_phase_id_phases_id_fk": { + "name": "review_comments_phase_id_phases_id_fk", + "tableFrom": "review_comments", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_comments_parent_comment_id_review_comments_id_fk": { + "name": "review_comments_parent_comment_id_review_comments_id_fk", + "tableFrom": "review_comments", + "tableTo": "review_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "task_dependencies": { "name": "task_dependencies", "columns": { @@ -964,28 +1778,28 @@ "task_dependencies_task_id_tasks_id_fk": { "name": "task_dependencies_task_id_tasks_id_fk", "tableFrom": "task_dependencies", + "tableTo": "tasks", "columnsFrom": [ "task_id" ], - "tableTo": "tasks", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "task_dependencies_depends_on_task_id_tasks_id_fk": { "name": "task_dependencies_depends_on_task_id_tasks_id_fk", "tableFrom": "task_dependencies", + "tableTo": "tasks", "columnsFrom": [ "depends_on_task_id" ], - "tableTo": "tasks", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1002,13 +1816,6 @@ "notNull": true, "autoincrement": false }, - "plan_id": { - "name": "plan_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, "phase_id": { "name": "phase_id", "type": "text", @@ -1023,6 +1830,13 @@ "notNull": false, "autoincrement": false }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "name": { "name": "name", "type": "text", @@ -1069,15 +1883,23 @@ "autoincrement": false, "default": "'pending'" }, - "requires_approval": { - "name": "requires_approval", + "order": { + "name": "order", "type": "integer", "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, "notNull": false, "autoincrement": false }, - "order": { - "name": "order", + "retry_count": { + "name": "retry_count", "type": "integer", "primaryKey": false, "notNull": true, @@ -1101,44 +1923,44 @@ }, "indexes": {}, "foreignKeys": { - "tasks_plan_id_plans_id_fk": { - "name": "tasks_plan_id_plans_id_fk", - "tableFrom": "tasks", - "columnsFrom": [ - "plan_id" - ], - "tableTo": "plans", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, "tasks_phase_id_phases_id_fk": { "name": "tasks_phase_id_phases_id_fk", "tableFrom": "tasks", + "tableTo": "phases", "columnsFrom": [ "phase_id" ], - "tableTo": "phases", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" }, "tasks_initiative_id_initiatives_id_fk": { "name": "tasks_initiative_id_initiatives_id_fk", "tableFrom": "tasks", + "tableTo": "initiatives", "columnsFrom": [ "initiative_id" ], - "tableTo": "initiatives", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_parent_task_id_tasks_id_fk": { + "name": "tasks_parent_task_id_tasks_id_fk", + "tableFrom": "tasks", + "tableTo": "tasks", + "columnsFrom": [ + "parent_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -1149,9 +1971,9 @@ "views": {}, "enums": {}, "_meta": { - "columns": {}, "schemas": {}, - "tables": {} + "tables": {}, + "columns": {} }, "internal": { "indexes": {} diff --git a/apps/server/drizzle/meta/0037_snapshot.json b/apps/server/drizzle/meta/0037_snapshot.json new file mode 100644 index 0000000..c46274d --- /dev/null +++ b/apps/server/drizzle/meta/0037_snapshot.json @@ -0,0 +1,2029 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "eb30417e-d030-457f-911e-6566dce54fc9", + "prevId": "f85b9df3-dead-4c46-90ac-cf36bcaa6eb4", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_exhausted": { + "name": "is_exhausted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "exhausted_until": { + "name": "exhausted_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_log_chunks": { + "name": "agent_log_chunks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_number": { + "name": "session_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_log_chunks_agent_id_idx": { + "name": "agent_log_chunks_agent_id_idx", + "columns": [ + "agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_metrics": { + "name": "agent_metrics", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "questions_count": { + "name": "questions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "subagents_count": { + "name": "subagents_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "compactions_count": { + "name": "compactions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents": { + "name": "agents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'execute'" + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_file_path": { + "name": "output_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_questions": { + "name": "pending_questions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_dismissed_at": { + "name": "user_dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_name_unique": { + "name": "agents_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agents_task_id_tasks_id_fk": { + "name": "agents_task_id_tasks_id_fk", + "tableFrom": "agents", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_initiative_id_initiatives_id_fk": { + "name": "agents_initiative_id_initiatives_id_fk", + "tableFrom": "agents", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_account_id_accounts_id_fk": { + "name": "agents_account_id_accounts_id_fk", + "tableFrom": "agents", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_set_entries": { + "name": "change_set_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_state": { + "name": "previous_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_state": { + "name": "new_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_set_entries_change_set_id_idx": { + "name": "change_set_entries_change_set_id_idx", + "columns": [ + "change_set_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_set_entries_change_set_id_change_sets_id_fk": { + "name": "change_set_entries_change_set_id_change_sets_id_fk", + "tableFrom": "change_set_entries", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_sets": { + "name": "change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'applied'" + }, + "reverted_at": { + "name": "reverted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_sets_initiative_id_idx": { + "name": "change_sets_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_sets_agent_id_agents_id_fk": { + "name": "change_sets_agent_id_agents_id_fk", + "tableFrom": "change_sets", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "change_sets_initiative_id_initiatives_id_fk": { + "name": "change_sets_initiative_id_initiatives_id_fk", + "tableFrom": "change_sets", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_messages_session_id_idx": { + "name": "chat_messages_session_id_idx", + "columns": [ + "chat_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_messages_chat_session_id_chat_sessions_id_fk": { + "name": "chat_messages_chat_session_id_chat_sessions_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_change_set_id_change_sets_id_fk": { + "name": "chat_messages_change_set_id_change_sets_id_fk", + "tableFrom": "chat_messages", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_sessions": { + "name": "chat_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_sessions_target_idx": { + "name": "chat_sessions_target_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": false + }, + "chat_sessions_initiative_id_idx": { + "name": "chat_sessions_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_sessions_initiative_id_initiatives_id_fk": { + "name": "chat_sessions_initiative_id_initiatives_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_agent_id_agents_id_fk": { + "name": "chat_sessions_agent_id_agents_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "from_agent_id": { + "name": "from_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_agent_id": { + "name": "to_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "conversations_to_agent_status_idx": { + "name": "conversations_to_agent_status_idx", + "columns": [ + "to_agent_id", + "status" + ], + "isUnique": false + }, + "conversations_from_agent_idx": { + "name": "conversations_from_agent_idx", + "columns": [ + "from_agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "conversations_from_agent_id_agents_id_fk": { + "name": "conversations_from_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "from_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_to_agent_id_agents_id_fk": { + "name": "conversations_to_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "to_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_initiative_id_initiatives_id_fk": { + "name": "conversations_initiative_id_initiatives_id_fk", + "tableFrom": "conversations", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_phase_id_phases_id_fk": { + "name": "conversations_phase_id_phases_id_fk", + "tableFrom": "conversations", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_task_id_tasks_id_fk": { + "name": "conversations_task_id_tasks_id_fk", + "tableFrom": "conversations", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "errands": { + "name": "errands", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "errands_agent_id_agents_id_fk": { + "name": "errands_agent_id_agents_id_fk", + "tableFrom": "errands", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "errands_project_id_projects_id_fk": { + "name": "errands_project_id_projects_id_fk", + "tableFrom": "errands", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "initiative_projects": { + "name": "initiative_projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "initiative_project_unique": { + "name": "initiative_project_unique", + "columns": [ + "initiative_id", + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "initiative_projects_initiative_id_initiatives_id_fk": { + "name": "initiative_projects_initiative_id_initiatives_id_fk", + "tableFrom": "initiative_projects", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "initiative_projects_project_id_projects_id_fk": { + "name": "initiative_projects_project_id_projects_id_fk", + "tableFrom": "initiative_projects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "initiatives": { + "name": "initiatives", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_mode": { + "name": "execution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'review_per_phase'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sender_type": { + "name": "sender_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_type": { + "name": "recipient_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'info'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requires_response": { + "name": "requires_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "messages_sender_id_agents_id_fk": { + "name": "messages_sender_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_recipient_id_agents_id_fk": { + "name": "messages_recipient_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_parent_message_id_messages_id_fk": { + "name": "messages_parent_message_id_messages_id_fk", + "tableFrom": "messages", + "tableTo": "messages", + "columnsFrom": [ + "parent_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pages": { + "name": "pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_page_id": { + "name": "parent_page_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "pages_initiative_id_initiatives_id_fk": { + "name": "pages_initiative_id_initiatives_id_fk", + "tableFrom": "pages", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pages_parent_page_id_pages_id_fk": { + "name": "pages_parent_page_id_pages_id_fk", + "tableFrom": "pages", + "tableTo": "pages", + "columnsFrom": [ + "parent_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "phase_dependencies": { + "name": "phase_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "depends_on_phase_id": { + "name": "depends_on_phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "phase_dependencies_phase_id_phases_id_fk": { + "name": "phase_dependencies_phase_id_phases_id_fk", + "tableFrom": "phase_dependencies", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "phase_dependencies_depends_on_phase_id_phases_id_fk": { + "name": "phase_dependencies_depends_on_phase_id_phases_id_fk", + "tableFrom": "phase_dependencies", + "tableTo": "phases", + "columnsFrom": [ + "depends_on_phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "phases": { + "name": "phases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "merge_base": { + "name": "merge_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "phases_initiative_id_initiatives_id_fk": { + "name": "phases_initiative_id_initiatives_id_fk", + "tableFrom": "phases", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "last_fetched_at": { + "name": "last_fetched_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_name_unique": { + "name": "projects_name_unique", + "columns": [ + "name" + ], + "isUnique": true + }, + "projects_url_unique": { + "name": "projects_url_unique", + "columns": [ + "url" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "review_comments": { + "name": "review_comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_type": { + "name": "line_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'you'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved": { + "name": "resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "review_comments_phase_id_idx": { + "name": "review_comments_phase_id_idx", + "columns": [ + "phase_id" + ], + "isUnique": false + }, + "review_comments_parent_id_idx": { + "name": "review_comments_parent_id_idx", + "columns": [ + "parent_comment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_comments_phase_id_phases_id_fk": { + "name": "review_comments_phase_id_phases_id_fk", + "tableFrom": "review_comments", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_comments_parent_comment_id_review_comments_id_fk": { + "name": "review_comments_parent_comment_id_review_comments_id_fk", + "tableFrom": "review_comments", + "tableTo": "review_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_dependencies": { + "name": "task_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "depends_on_task_id": { + "name": "depends_on_task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_dependencies_task_id_tasks_id_fk": { + "name": "task_dependencies_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_dependencies_depends_on_task_id_tasks_id_fk": { + "name": "task_dependencies_depends_on_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "tasks", + "columnsFrom": [ + "depends_on_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'execute'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_phase_id_phases_id_fk": { + "name": "tasks_phase_id_phases_id_fk", + "tableFrom": "tasks", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_initiative_id_initiatives_id_fk": { + "name": "tasks_initiative_id_initiatives_id_fk", + "tableFrom": "tasks", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_parent_task_id_tasks_id_fk": { + "name": "tasks_parent_task_id_tasks_id_fk", + "tableFrom": "tasks", + "tableTo": "tasks", + "columnsFrom": [ + "parent_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index a58f2cf..0580df0 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1772798869413, "tag": "0036_icy_silvermane", "breakpoints": true + }, + { + "idx": 37, + "version": "6", + "when": 1772828694292, + "tag": "0037_eager_devos", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/apps/server/scripts/backfill-metrics.test.ts b/apps/server/scripts/backfill-metrics.test.ts new file mode 100644 index 0000000..1dbb966 --- /dev/null +++ b/apps/server/scripts/backfill-metrics.test.ts @@ -0,0 +1,131 @@ +/** + * Tests for the backfill-metrics script. + * + * Uses an in-memory test database to verify that backfillMetrics correctly + * accumulates counts from agent_log_chunks and upserts into agent_metrics. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js'; +import type { DrizzleDatabase } from '../db/index.js'; +import { agentLogChunks, agentMetrics } from '../db/index.js'; +import { backfillMetrics } from './backfill-metrics.js'; +import { nanoid } from 'nanoid'; +import { eq } from 'drizzle-orm'; + +async function insertChunk(db: DrizzleDatabase, agentId: string, content: object | string) { + await db.insert(agentLogChunks).values({ + id: nanoid(), + agentId, + agentName: 'test-agent', + sessionNumber: 1, + content: typeof content === 'string' ? content : JSON.stringify(content), + createdAt: new Date(), + }); +} + +describe('backfillMetrics', () => { + let db: DrizzleDatabase; + + beforeEach(() => { + db = createTestDatabase(); + }); + + it('AskUserQuestion chunks — questionsCount correct', async () => { + await insertChunk(db, 'agent-a', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }); + await insertChunk(db, 'agent-a', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}] } }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-a')); + expect(rows).toHaveLength(1); + expect(rows[0].questionsCount).toBe(3); + expect(rows[0].subagentsCount).toBe(0); + expect(rows[0].compactionsCount).toBe(0); + }); + + it('Agent tool chunks — subagentsCount correct', async () => { + await insertChunk(db, 'agent-b', { type: 'tool_use', name: 'Agent' }); + await insertChunk(db, 'agent-b', { type: 'tool_use', name: 'Agent' }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-b')); + expect(rows).toHaveLength(1); + expect(rows[0].questionsCount).toBe(0); + expect(rows[0].subagentsCount).toBe(2); + expect(rows[0].compactionsCount).toBe(0); + }); + + it('Compaction chunks — compactionsCount correct', async () => { + await insertChunk(db, 'agent-c', { type: 'system', subtype: 'init', source: 'compact' }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-c')); + expect(rows).toHaveLength(1); + expect(rows[0].questionsCount).toBe(0); + expect(rows[0].subagentsCount).toBe(0); + expect(rows[0].compactionsCount).toBe(1); + }); + + it('Irrelevant chunk type — no metrics row created', async () => { + await insertChunk(db, 'agent-d', { type: 'text', text: 'hello' }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-d')); + expect(rows).toEqual([]); + }); + + it('Malformed JSON chunk — skipped, no crash', async () => { + await insertChunk(db, 'agent-e', 'not-valid-json'); + await insertChunk(db, 'agent-e', { type: 'tool_use', name: 'Agent' }); + + await expect(backfillMetrics(db)).resolves.not.toThrow(); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-e')); + expect(rows).toHaveLength(1); + expect(rows[0].subagentsCount).toBe(1); + }); + + it('Multiple agents — counts isolated per agent', async () => { + await insertChunk(db, 'agent-f', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}, {}] } }); + await insertChunk(db, 'agent-f', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}, {}] } }); + await insertChunk(db, 'agent-g', { type: 'tool_use', name: 'Agent' }); + + await backfillMetrics(db); + + const rowsF = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-f')); + expect(rowsF).toHaveLength(1); + expect(rowsF[0].questionsCount).toBe(6); + expect(rowsF[0].subagentsCount).toBe(0); + expect(rowsF[0].compactionsCount).toBe(0); + + const rowsG = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-g')); + expect(rowsG).toHaveLength(1); + expect(rowsG[0].questionsCount).toBe(0); + expect(rowsG[0].subagentsCount).toBe(1); + expect(rowsG[0].compactionsCount).toBe(0); + }); + + it('Empty database — completes without error', async () => { + await expect(backfillMetrics(db)).resolves.not.toThrow(); + + const rows = await db.select().from(agentMetrics); + expect(rows).toEqual([]); + }); + + it('Re-run idempotency note — second run doubles counts', async () => { + // Documented behavior: run only once against a fresh agent_metrics table + await insertChunk(db, 'agent-h', { type: 'tool_use', name: 'Agent' }); + + await backfillMetrics(db); + const rowsAfterFirst = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-h')); + expect(rowsAfterFirst[0].subagentsCount).toBe(1); + + await backfillMetrics(db); + const rowsAfterSecond = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-h')); + expect(rowsAfterSecond[0].subagentsCount).toBe(2); + }); +}); diff --git a/apps/server/scripts/backfill-metrics.ts b/apps/server/scripts/backfill-metrics.ts new file mode 100644 index 0000000..42dfe25 --- /dev/null +++ b/apps/server/scripts/backfill-metrics.ts @@ -0,0 +1,128 @@ +/** + * Backfill script for agent_metrics table. + * + * Reads all existing agent_log_chunks rows and populates agent_metrics with + * accumulated counts of questions, subagent spawns, and compaction events. + * + * Intended to be run once per production database after applying the migration + * that introduces the agent_metrics table. + * + * Idempotency note: Uses ON CONFLICT DO UPDATE with additive increments to match + * the ongoing insertChunk write-path behavior. Running against an empty + * agent_metrics table is fully safe. Running a second time will double-count — + * only run this script once per database, immediately after applying the migration. + */ + +import { asc, sql } from 'drizzle-orm'; +import { createDatabase, DrizzleDatabase, agentLogChunks, agentMetrics } from '../db/index.js'; + +const BATCH_SIZE = 500; +const LOG_EVERY = 1000; + +/** + * Core backfill function. Accepts a DrizzleDatabase for testability. + */ +export async function backfillMetrics(db: DrizzleDatabase): Promise { + const accumulator = new Map(); + let offset = 0; + let totalChunks = 0; + let malformedCount = 0; + + while (true) { + const batch = await db + .select({ agentId: agentLogChunks.agentId, content: agentLogChunks.content }) + .from(agentLogChunks) + .orderBy(asc(agentLogChunks.createdAt)) + .limit(BATCH_SIZE) + .offset(offset); + + if (batch.length === 0) break; + + for (const chunk of batch) { + let parsed: unknown; + try { + parsed = JSON.parse(chunk.content); + } catch { + malformedCount++; + totalChunks++; + if (totalChunks % LOG_EVERY === 0) { + console.log(`Processed ${totalChunks} chunks...`); + } + continue; + } + + if (typeof parsed !== 'object' || parsed === null) { + totalChunks++; + if (totalChunks % LOG_EVERY === 0) { + console.log(`Processed ${totalChunks} chunks...`); + } + continue; + } + + const obj = parsed as Record; + const type = obj['type']; + const name = obj['name']; + + if (type === 'tool_use' && name === 'AskUserQuestion') { + const input = obj['input'] as Record | undefined; + const questions = input?.['questions']; + const count = Array.isArray(questions) ? questions.length : 0; + if (count > 0) { + const entry = accumulator.get(chunk.agentId) ?? { questionsCount: 0, subagentsCount: 0, compactionsCount: 0 }; + entry.questionsCount += count; + accumulator.set(chunk.agentId, entry); + } + } else if (type === 'tool_use' && name === 'Agent') { + const entry = accumulator.get(chunk.agentId) ?? { questionsCount: 0, subagentsCount: 0, compactionsCount: 0 }; + entry.subagentsCount += 1; + accumulator.set(chunk.agentId, entry); + } else if (type === 'system' && obj['subtype'] === 'init' && obj['source'] === 'compact') { + const entry = accumulator.get(chunk.agentId) ?? { questionsCount: 0, subagentsCount: 0, compactionsCount: 0 }; + entry.compactionsCount += 1; + accumulator.set(chunk.agentId, entry); + } + + totalChunks++; + if (totalChunks % LOG_EVERY === 0) { + console.log(`Processed ${totalChunks} chunks...`); + } + } + + offset += BATCH_SIZE; + } + + // Upsert accumulated counts into agent_metrics. + // Uses additive ON CONFLICT DO UPDATE to match the ongoing insertChunk behavior. + for (const [agentId, counts] of accumulator) { + await db + .insert(agentMetrics) + .values({ + agentId, + questionsCount: counts.questionsCount, + subagentsCount: counts.subagentsCount, + compactionsCount: counts.compactionsCount, + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: agentMetrics.agentId, + set: { + questionsCount: sql`${agentMetrics.questionsCount} + ${counts.questionsCount}`, + subagentsCount: sql`${agentMetrics.subagentsCount} + ${counts.subagentsCount}`, + compactionsCount: sql`${agentMetrics.compactionsCount} + ${counts.compactionsCount}`, + updatedAt: new Date(), + }, + }); + } + + console.log( + `Backfill complete: ${accumulator.size} agents updated, ${totalChunks} chunks processed, ${malformedCount} malformed chunks skipped` + ); +} + +/** + * CLI wrapper — opens a database from a path, then delegates to backfillMetrics. + */ +export async function backfillMetricsFromPath(dbPath: string): Promise { + const db = createDatabase(dbPath); + await backfillMetrics(db); +} diff --git a/apps/server/test/unit/radar-procedures.test.ts b/apps/server/test/unit/radar-procedures.test.ts index 01d642e..dacea69 100644 --- a/apps/server/test/unit/radar-procedures.test.ts +++ b/apps/server/test/unit/radar-procedures.test.ts @@ -326,6 +326,47 @@ describe('agent.listForRadar', () => { expect(row!.subagentsCount).toBe(0); expect(row!.compactionsCount).toBe(0); }); + + it('returns zero counts for agent with no metrics row', async () => { + const agents = new MockAgentManager(); + const ctx = makeCtx(agents); + + const now = new Date(); + // Agent with no log chunks at all — no agent_metrics row will exist + agents.addAgent({ id: 'agent-no-chunks', name: 'no-chunks-agent', status: 'running', createdAt: now }); + + const caller = createAgentCaller(ctx); + const result = await caller.listForRadar({ timeRange: 'all' }); + + const row = result.find(r => r.id === 'agent-no-chunks'); + expect(row).toBeDefined(); + expect(row!.questionsCount).toBe(0); + expect(row!.subagentsCount).toBe(0); + expect(row!.compactionsCount).toBe(0); + }); + + it('listForRadar response does not contain chunk content field', async () => { + const agents = new MockAgentManager(); + const ctx = makeCtx(agents); + const { logChunkRepo } = getRepos(ctx); + + const now = new Date(); + agents.addAgent({ id: 'agent-content', name: 'content-agent', status: 'running', createdAt: now }); + + await logChunkRepo.insertChunk({ + agentId: 'agent-content', + agentName: 'content-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent', input: { description: 'do stuff', prompt: 'some prompt' } }), + }); + + const caller = createAgentCaller(ctx); + const result = await caller.listForRadar({ timeRange: 'all' }); + + for (const row of result) { + expect(row).not.toHaveProperty('content'); + } + }); }); // ============================================================================= diff --git a/apps/server/trpc/routers/agent.ts b/apps/server/trpc/routers/agent.ts index 644b814..82d397d 100644 --- a/apps/server/trpc/routers/agent.ts +++ b/apps/server/trpc/routers/agent.ts @@ -475,8 +475,8 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { const uniqueTaskIds = [...new Set(filteredAgents.map(a => a.taskId).filter(Boolean) as string[])]; const uniqueInitiativeIds = [...new Set(filteredAgents.map(a => a.initiativeId).filter(Boolean) as string[])]; - const [chunks, messageCounts, taskResults, initiativeResults] = await Promise.all([ - logChunkRepo.findByAgentIds(matchingIds), + const [metrics, messageCounts, taskResults, initiativeResults] = await Promise.all([ + logChunkRepo.findMetricsByAgentIds(matchingIds), conversationRepo.countByFromAgentIds(matchingIds), Promise.all(uniqueTaskIds.map(id => taskRepo.findById(id))), Promise.all(uniqueInitiativeIds.map(id => initiativeRepo.findById(id))), @@ -486,37 +486,14 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { const taskMap = new Map(taskResults.filter(Boolean).map(t => [t!.id, t!.name])); const initiativeMap = new Map(initiativeResults.filter(Boolean).map(i => [i!.id, i!.name])); const messagesMap = new Map(messageCounts.map(m => [m.agentId, m.count])); - - // Group chunks by agentId - const chunksByAgent = new Map(); - for (const chunk of chunks) { - const existing = chunksByAgent.get(chunk.agentId); - if (existing) { - existing.push(chunk); - } else { - chunksByAgent.set(chunk.agentId, [chunk]); - } - } + const metricsMap = new Map(metrics.map(m => [m.agentId, m])); // Build result rows return filteredAgents.map(agent => { - const agentChunks = chunksByAgent.get(agent.id) ?? []; - let questionsCount = 0; - let subagentsCount = 0; - let compactionsCount = 0; - - for (const chunk of agentChunks) { - try { - const parsed = JSON.parse(chunk.content); - if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') { - questionsCount += parsed.input?.questions?.length ?? 0; - } else if (parsed.type === 'tool_use' && parsed.name === 'Agent') { - subagentsCount++; - } else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') { - compactionsCount++; - } - } catch { /* skip malformed */ } - } + const agentMetrics = metricsMap.get(agent.id); + const questionsCount = agentMetrics?.questionsCount ?? 0; + const subagentsCount = agentMetrics?.subagentsCount ?? 0; + const compactionsCount = agentMetrics?.compactionsCount ?? 0; return { id: agent.id, diff --git a/docs/database-migrations.md b/docs/database-migrations.md index 603d49e..3e6d3ae 100644 --- a/docs/database-migrations.md +++ b/docs/database-migrations.md @@ -55,3 +55,27 @@ Migrations 0000–0007 were generated by `drizzle-kit generate`. Migrations 0008 - **Migration files are immutable.** Once committed, never edit them. Make a new migration instead. - **Keep schema.ts in sync.** The schema file is the source of truth for TypeScript types; migrations are the source of truth for database DDL. Both must reflect the same structure. - **Test with `npm test`** after generating migrations to verify they work with in-memory databases. + +## Post-migration backfill scripts + +Some schema additions require a one-time data backfill because SQLite migrations cannot execute Node.js logic (e.g., JSON parsing). In these cases, the migration creates the table structure, and a separate Node.js script populates it from existing data. + +### agent_metrics backfill + +**When to run:** After deploying the migration that creates the `agent_metrics` table (introduced in the Radar Screen Performance initiative). Run this once per production database after upgrading. + +**Command:** +```sh +cw backfill-metrics +# Or with a custom DB path: +cw backfill-metrics --db /path/to/codewalkers.db +``` + +**What it does:** +- Reads all existing `agent_log_chunks` rows in batches of 500 (ordered by `createdAt ASC`) +- Parses each chunk's `content` JSON to count `AskUserQuestion` tool calls, `Agent` spawns, and compaction events +- Upserts the accumulated counts into `agent_metrics` using additive conflict resolution + +**Idempotency:** The script uses `ON CONFLICT DO UPDATE` with additive increments, matching the ongoing write-path behavior. Running it against an empty `agent_metrics` table is fully safe. Running it a second time will double-count — only run it once per database, immediately after applying the migration. + +**Batch size:** 500 rows per query, to avoid loading the full `agent_log_chunks` table into memory. Progress is logged every 1,000 chunks.