diff --git a/src/main/database/company-settings.ts b/src/main/database/company-settings.ts index c38d962a..e2d3c7bb 100644 --- a/src/main/database/company-settings.ts +++ b/src/main/database/company-settings.ts @@ -2,6 +2,7 @@ import type { DatabaseSync } from "node:sqlite"; import type { CompanyMetrics, AgentMetrics, + InboxItem, ProfileSnapshot, RunStatus, StandupReport, @@ -21,104 +22,164 @@ export function buildCompanySettingsMethods( ) { return { getCompanyMetrics(companyId: string, days: number = 30): CompanyMetrics { - const snapshot = deps.listSnapshot({ companyId }); - const agents = snapshot.agents; - const tasks = snapshot.tasks; - const runs = snapshot.runs; - const costs = snapshot.costs; - const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); - const recentRuns = runs.filter((r) => r.createdAt > cutoff); - const recentTasks = tasks.filter((t) => t.status === "done" && t.updatedAt > cutoff); + const db = getDb(); - const successfulRuns = recentRuns.filter((r) => r.status === "succeeded").length; - const failedRuns = recentRuns.filter( - (r) => r.status === "failed" || r.status === "timed_out", + const agentRows = readAll<{ + id: string; name: string; status: string; + }>( + `select id, name, status from agents where company_id = ? order by created_at asc`, + companyId, + ); + const activeAgentRows = agentRows.filter( + (a) => a.status !== "pending_approval" && a.status !== "terminated", + ); + + const totalAgents = activeAgentRows.length; + const activeAgents = agentRows.filter( + (a) => a.status === "active" || a.status === "running", ).length; - const totalCost = costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0); - const completedTasks = tasks.filter((t) => t.status === "done").length; - const agentMetrics: AgentMetrics[] = agents - .filter((a) => a.status !== "pending_approval" && a.status !== "terminated") - .map((agent) => { - const agentTasks = tasks.filter((t) => t.assigneeAgentId === agent.id); - const agentRuns = runs.filter((r) => r.agentId === agent.id); - const agentRecentRuns = agentRuns.filter((r) => r.createdAt > cutoff); - const agentCosts = costs.filter((c) => c.agentId === agent.id); + const totalTasks = (db.prepare( + "select count(*) as c from tasks where company_id = ?", + ).get(companyId) as { c: number }).c; + const completedTasks = (db.prepare( + "select count(*) as c from tasks where company_id = ? and status = 'done'", + ).get(companyId) as { c: number }).c; + const blockedTasks = (db.prepare( + "select count(*) as c from tasks where company_id = ? and status = 'blocked'", + ).get(companyId) as { c: number }).c; - const agentSucceeded = agentRecentRuns.filter((r) => r.status === "succeeded").length; - const agentFailed = agentRecentRuns.filter( - (r) => r.status === "failed" || r.status === "timed_out", - ).length; + const recentRunStats = db.prepare( + `select + count(*) as totalRuns, + sum(case when status = 'succeeded' then 1 else 0 end) as successful, + sum(case when status = 'failed' or status = 'timed_out' then 1 else 0 end) as failed + from runs where company_id = ? and created_at > ?`, + ).get(companyId, cutoff) as { totalRuns: number; successful: number | null; failed: number | null }; + const totalRuns = recentRunStats.totalRuns ?? 0; + const successfulRuns = recentRunStats.successful ?? 0; + const failedRuns = recentRunStats.failed ?? 0; - // Avg run duration - const finishedRuns = agentRuns.filter( - (r): r is typeof r & { startedAt: string; finishedAt: string } => - Boolean(r.startedAt && r.finishedAt), - ); - const avgDuration = - finishedRuns.length > 0 - ? finishedRuns.reduce( - (sum, r) => - sum + - (new Date(r.finishedAt).getTime() - new Date(r.startedAt).getTime()) / 1000, - 0, - ) / finishedRuns.length - : 0; + const totalCost = (db.prepare( + "select coalesce(sum(amount_usd), 0) as s from cost_entries where company_id = ?", + ).get(companyId) as { s: number }).s; - // Review cycles: count task status transitions by this agent - // Actual action strings: "task.status_changed" (api-server), "task.updated" (index.ts), "task.auto_assigned" (database) - const reviewCycles = snapshot.activity.filter( - (a) => - a.companyId === companyId && - a.actor === agent.id && - (a.action === "task.status_changed" || - a.action === "task.updated" || - a.action === "task.auto_assigned"), - ).length; + const recentTasksDone = (db.prepare( + "select count(*) as c from tasks where company_id = ? and status = 'done' and updated_at > ?", + ).get(companyId, cutoff) as { c: number }).c; - const lastRun = agentRuns.sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - )[0]; + const taskAggRows = readAll<{ + agentId: string; + done: number; + assigned: number; + cancelled: number; + }>( + `select assignee_agent_id as agentId, + sum(case when status = 'done' then 1 else 0 end) as done, + sum(case when status != 'done' and status != 'cancelled' then 1 else 0 end) as assigned, + sum(case when status = 'cancelled' then 1 else 0 end) as cancelled + from tasks + where company_id = ? and assignee_agent_id is not null + group by assignee_agent_id`, + companyId, + ); + const taskAggMap = new Map(taskAggRows.map((row) => [row.agentId, row])); - return { - agentId: agent.id, - agentName: agent.name, - tasksCompleted: agentTasks.filter((t) => t.status === "done").length, - tasksAssigned: agentTasks.filter( - (t) => t.status !== "done" && t.status !== "cancelled", - ).length, - tasksFailed: agentTasks.filter((t) => t.status === "cancelled").length, - successRate: - agentRecentRuns.length > 0 - ? Math.round((agentSucceeded / agentRecentRuns.length) * 100) - : 0, - avgRunDurationSec: Math.round(avgDuration), - totalCostUsd: agentCosts.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), - runsTotal: agentRecentRuns.length, - runsSucceeded: agentSucceeded, - runsFailed: agentFailed, - reviewCycles, - lastActiveAt: lastRun?.createdAt ?? null, - }; - }); + const runRecentAggRows = readAll<{ + agentId: string; + total: number; + succeeded: number; + failed: number; + }>( + `select agent_id as agentId, + count(*) as total, + sum(case when status = 'succeeded' then 1 else 0 end) as succeeded, + sum(case when status = 'failed' or status = 'timed_out' then 1 else 0 end) as failed + from runs + where company_id = ? and created_at > ? + group by agent_id`, + companyId, + cutoff, + ); + const runRecentAggMap = new Map(runRecentAggRows.map((row) => [row.agentId, row])); + + const runDurationRows = readAll<{ + agentId: string; + avgDurationSec: number | null; + lastActiveAt: string | null; + }>( + `select agent_id as agentId, + avg((julianday(finished_at) - julianday(started_at)) * 86400.0) as avgDurationSec, + max(created_at) as lastActiveAt + from runs + where company_id = ? and started_at is not null and finished_at is not null + group by agent_id`, + companyId, + ); + const runDurationMap = new Map(runDurationRows.map((row) => [row.agentId, row])); + + const lastActiveRows = readAll<{ agentId: string; lastActiveAt: string }>( + `select agent_id as agentId, max(created_at) as lastActiveAt + from runs where company_id = ? group by agent_id`, + companyId, + ); + const lastActiveMap = new Map(lastActiveRows.map((row) => [row.agentId, row.lastActiveAt])); + + const costAggRows = readAll<{ agentId: string; totalCost: number }>( + `select agent_id as agentId, coalesce(sum(amount_usd), 0) as totalCost + from cost_entries where company_id = ? and agent_id is not null + group by agent_id`, + companyId, + ); + const costAggMap = new Map(costAggRows.map((row) => [row.agentId, row.totalCost])); + + const reviewCycleRows = readAll<{ actor: string; cycles: number }>( + `select actor, count(*) as cycles + from activity + where company_id = ? + and action in ('task.status_changed', 'task.updated', 'task.auto_assigned') + group by actor`, + companyId, + ); + const reviewCycleMap = new Map(reviewCycleRows.map((row) => [row.actor, row.cycles])); + + const agentMetrics: AgentMetrics[] = activeAgentRows.map((agent) => { + const taskAgg = taskAggMap.get(agent.id); + const runRecent = runRecentAggMap.get(agent.id); + const runDur = runDurationMap.get(agent.id); + const succeeded = runRecent?.succeeded ?? 0; + const total = runRecent?.total ?? 0; + return { + agentId: agent.id, + agentName: agent.name, + tasksCompleted: taskAgg?.done ?? 0, + tasksAssigned: taskAgg?.assigned ?? 0, + tasksFailed: taskAgg?.cancelled ?? 0, + successRate: total > 0 ? Math.round((succeeded / total) * 100) : 0, + avgRunDurationSec: Math.round(runDur?.avgDurationSec ?? 0), + totalCostUsd: costAggMap.get(agent.id) ?? 0, + runsTotal: total, + runsSucceeded: succeeded, + runsFailed: runRecent?.failed ?? 0, + reviewCycles: reviewCycleMap.get(agent.id) ?? 0, + lastActiveAt: lastActiveMap.get(agent.id) ?? null, + }; + }); return { - totalAgents: agents.filter( - (a) => a.status !== "pending_approval" && a.status !== "terminated", - ).length, - activeAgents: agents.filter((a) => a.status === "active" || a.status === "running").length, - totalTasks: tasks.length, + totalAgents, + activeAgents, + totalTasks, completedTasks, - blockedTasks: tasks.filter((t) => t.status === "blocked").length, - totalRuns: recentRuns.length, + blockedTasks, + totalRuns, successfulRuns, failedRuns, - overallSuccessRate: - recentRuns.length > 0 ? Math.round((successfulRuns / recentRuns.length) * 100) : 0, + overallSuccessRate: totalRuns > 0 ? Math.round((successfulRuns / totalRuns) * 100) : 0, totalCostUsd: totalCost, avgCostPerTask: completedTasks > 0 ? totalCost / completedTasks : 0, - throughputTasksPerDay: days > 0 ? recentTasks.length / days : 0, + throughputTasksPerDay: days > 0 ? recentTasksDone / days : 0, agentMetrics, }; },