From 9874d825b66c2db537c7ad777bc9a96c70238265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 22:39:43 -0400 Subject: [PATCH] refactor(api-server): move agents-standup SQL into db domain methods Replace the six inline queryAll calls in routes-agents-standup.ts with typed domain methods: db.agents gains listDirectReportsForStandup and listReportStatusesForStandup (LEFT JOIN aggregation), db.tasks gains listAssigneeTasksForStandup and listTasksForAssignees (safe on empty assignee list), db.approvals gains listAgentPendingApprovals and listAgentResolvedApprovals. Shared listRecentTaskComments is reused. --- src/main/api-server/routes-agents-standup.ts | 104 ++----------------- src/main/database-base.ts | 6 ++ src/main/database/agents-hierarchy.ts | 46 ++++++++ src/main/database/approvals.ts | 44 ++++++++ src/main/database/tasks-crud.ts | 40 +++++++ 5 files changed, 146 insertions(+), 94 deletions(-) diff --git a/src/main/api-server/routes-agents-standup.ts b/src/main/api-server/routes-agents-standup.ts index a51e2958..2544a7df 100644 --- a/src/main/api-server/routes-agents-standup.ts +++ b/src/main/api-server/routes-agents-standup.ts @@ -1,34 +1,5 @@ import type { AgentApiServer } from "./server-core"; -type DirectReportRow = { - id: string; name: string; role: string; status: string; -}; - -type MyTaskRow = { - id: string; title: string; status: string; priority: string; -}; - -type ReportTaskRow = { - id: string; title: string; status: string; priority: string; - assigneeAgentId: string; -}; - -type ApprovalRow = { - id: string; type: string; state: string; - payloadSummary: string | null; decisionNote: string | null; -}; - -type TaskCommentRow = { - id: string; taskId: string; authorName: string; - body: string; createdAt: string; -}; - -type ReportStatusRow = { - id: string; name: string; role: string; status: string; - tasksInProgress: number; tasksInReview: number; - tasksBlocked: number; tasksDone: number; -}; - export function registerAgentStandupRoute(server: AgentApiServer): void { const db = server.db; server.addRoute("GET", "/api/agents/me/standup", (_req, res, _params, token) => { @@ -38,49 +9,17 @@ export function registerAgentStandupRoute(server: AgentApiServer): void { return; } - const directReports = db.queryAll( - `select id, name, role, status from agents - where company_id = ? and reports_to = ? - order by created_at asc`, - token.companyId, - token.agentId, - ); + const directReports = db.listDirectReportsForStandup(token.companyId, token.agentId); - const myTasks = db.queryAll( - `select id, title, status, priority from tasks - where company_id = ? and assignee_agent_id = ? - order by created_at desc`, - token.companyId, - token.agentId, - ); + const myTasks = db.listAssigneeTasksForStandup(token.companyId, token.agentId); - const reportTasks = directReports.length > 0 - ? db.queryAll( - `select id, title, status, priority, assignee_agent_id as assigneeAgentId - from tasks - where company_id = ? - and assignee_agent_id in (${directReports.map(() => "?").join(",")}) - order by created_at desc`, - token.companyId, - ...directReports.map((r) => r.id), - ) - : []; + const reportTasks = db.listTasksForAssignees( + token.companyId, + directReports.map((r) => r.id), + ); const reportStatusRows = directReports.length > 0 - ? db.queryAll( - `select agents.id as id, agents.name as name, agents.role as role, agents.status as status, - coalesce(sum(case when tasks.status = 'in_progress' then 1 else 0 end), 0) as tasksInProgress, - coalesce(sum(case when tasks.status = 'in_review' then 1 else 0 end), 0) as tasksInReview, - coalesce(sum(case when tasks.status = 'blocked' then 1 else 0 end), 0) as tasksBlocked, - coalesce(sum(case when tasks.status = 'done' then 1 else 0 end), 0) as tasksDone - from agents - left join tasks on tasks.assignee_agent_id = agents.id and tasks.company_id = agents.company_id - where agents.company_id = ? and agents.reports_to = ? - group by agents.id, agents.name, agents.role, agents.status - order by agents.created_at asc`, - token.companyId, - token.agentId, - ) + ? db.listReportStatusesForStandup(token.companyId, token.agentId) : []; const tasksAwaitingMyReview = reportTasks.filter((t) => t.status === "in_review"); @@ -94,33 +33,10 @@ export function registerAgentStandupRoute(server: AgentApiServer): void { ...reportTasks.map((t) => t.id), ]); const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); - const recentComments = allRelevantTaskIds.size > 0 - ? db.queryAll( - `select id, task_id as taskId, author_name as authorName, body, created_at as createdAt - from task_comments - where task_id in (${[...allRelevantTaskIds].map(() => "?").join(",")}) - and created_at > ? - order by created_at desc limit 20`, - ...[...allRelevantTaskIds], twoHoursAgo, - ) - : []; + const recentComments = db.listRecentTaskComments([...allRelevantTaskIds], twoHoursAgo, 20); - const myPendingApprovals = db.queryAll( - `select id, type, state, payload_summary as payloadSummary, decision_note as decisionNote - from approvals - where company_id = ? and requested_by_agent_id = ? and state = 'pending' - order by created_at desc`, - token.companyId, - token.agentId, - ); - const myResolvedApprovals = db.queryAll( - `select id, type, state, payload_summary as payloadSummary, decision_note as decisionNote - from approvals - where company_id = ? and requested_by_agent_id = ? and state != 'pending' - order by created_at desc limit 5`, - token.companyId, - token.agentId, - ); + const myPendingApprovals = db.listAgentPendingApprovals(token.companyId, token.agentId); + const myResolvedApprovals = db.listAgentResolvedApprovals(token.companyId, token.agentId, 5); const reportNameMap = new Map(directReports.map((a) => [a.id, a.name])); const taskTitleMap = new Map([ diff --git a/src/main/database-base.ts b/src/main/database-base.ts index f3101474..798e1dfe 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -213,6 +213,8 @@ export class AppDatabaseBase { listActiveAgentsForCompany(companyId: string) { return this.agents.listActiveAgentsForCompany(companyId); } listAgentsForApi(companyId: string) { return this.agents.listAgentsForApi(companyId); } listAgentsForStandup(companyId: string) { return this.agents.listAgentsForStandup(companyId); } + listDirectReportsForStandup(companyId: string, managerId: string) { return this.agents.listDirectReportsForStandup(companyId, managerId); } + listReportStatusesForStandup(companyId: string, managerId: string) { return this.agents.listReportStatusesForStandup(companyId, managerId); } listExistingAgentsForHireCheck(companyId: string) { return this.agents.listExistingAgentsForHireCheck(companyId); } hasAgentAssignedTasks(agentId: string, companyId: string) { return this.agents.hasAgentAssignedTasks(agentId, companyId); } updateAgentStatus(agentId: string, status: string) { return this.agents.updateAgentStatus(agentId, status); } @@ -248,6 +250,8 @@ export class AppDatabaseBase { listAgentTasks(agentId: string, companyId: string, statuses: string[]) { return this.tasks.listAgentTasks(agentId, companyId, statuses); } listTasksByStatuses(companyId: string, statuses: string[]) { return this.tasks.listTasksByStatuses(companyId, statuses); } listTasksForStandup(companyId: string) { return this.tasks.listTasksForStandup(companyId); } + listAssigneeTasksForStandup(companyId: string, agentId: string) { return this.tasks.listAssigneeTasksForStandup(companyId, agentId); } + listTasksForAssignees(companyId: string, assigneeIds: string[]) { return this.tasks.listTasksForAssignees(companyId, assigneeIds); } findReusableHeartbeatTask(agentId: string, companyId: string) { return this.tasks.findReusableHeartbeatTask(agentId, companyId); } acquireHeartbeatTask(input: { agentId: string; companyId: string; runId: string; workspaceId: string | null; title: string; description: string; priority: string }) { return this.tasks.acquireHeartbeatTask(input); } cleanupDuplicateHeartbeatTasks(companyId: string) { return this.tasks.cleanupDuplicateHeartbeatTasks(companyId); } @@ -303,6 +307,8 @@ export class AppDatabaseBase { listTasksForApproval(approvalId: string) { return this.approvalsMod.listTasksForApproval(approvalId); } linkManyTasksToApproval(approvalId: string, taskIds: string[], companyId: string, linkedByAgentId?: string | null) { this.approvalsMod.linkManyTasksToApproval(approvalId, taskIds, companyId, linkedByAgentId); } listApprovalsForStandup(companyId: string) { return this.approvalsMod.listApprovalsForStandup(companyId); } + listAgentPendingApprovals(companyId: string, agentId: string) { return this.approvalsMod.listAgentPendingApprovals(companyId, agentId); } + listAgentResolvedApprovals(companyId: string, agentId: string, limit?: number) { return this.approvalsMod.listAgentResolvedApprovals(companyId, agentId, limit); } listPendingHireApprovalPayloads(companyId: string) { return this.approvalsMod.listPendingHireApprovalPayloads(companyId); } listApprovalsForApi(companyId: string, filters: { requestedByAgentId?: string; relatedTaskId?: string; statuses?: string[] | null }) { return this.approvalsMod.listApprovalsForApi(companyId, filters); } diff --git a/src/main/database/agents-hierarchy.ts b/src/main/database/agents-hierarchy.ts index a1debf5f..33b710b6 100644 --- a/src/main/database/agents-hierarchy.ts +++ b/src/main/database/agents-hierarchy.ts @@ -139,6 +139,52 @@ export function buildAgentHierarchyMethods( ); }, + /** + * Returns the slim direct-reports projection used by + * GET /api/agents/me/standup — only (id, name, role, status). + */ + listDirectReportsForStandup( + companyId: string, + managerId: string, + ): Array<{ id: string; name: string; role: string; status: string }> { + return readAll( + "select id, name, role, status from agents where company_id = ? and reports_to = ? order by created_at asc", + companyId, + managerId, + ); + }, + + /** + * Returns per-report task-status counts for the standup aggregation. + * Joins agents with their tasks and aggregates statuses (in_progress, + * in_review, blocked, done). + */ + listReportStatusesForStandup(companyId: string, managerId: string): Array<{ + id: string; + name: string; + role: string; + status: string; + tasksInProgress: number; + tasksInReview: number; + tasksBlocked: number; + tasksDone: number; + }> { + return readAll( + `select agents.id as id, agents.name as name, agents.role as role, agents.status as status, + coalesce(sum(case when tasks.status = 'in_progress' then 1 else 0 end), 0) as tasksInProgress, + coalesce(sum(case when tasks.status = 'in_review' then 1 else 0 end), 0) as tasksInReview, + coalesce(sum(case when tasks.status = 'blocked' then 1 else 0 end), 0) as tasksBlocked, + coalesce(sum(case when tasks.status = 'done' then 1 else 0 end), 0) as tasksDone + from agents + left join tasks on tasks.assignee_agent_id = agents.id and tasks.company_id = agents.company_id + where agents.company_id = ? and agents.reports_to = ? + group by agents.id, agents.name, agents.role, agents.status + order by agents.created_at asc`, + companyId, + managerId, + ); + }, + /** * Returns the duplicate-detection projection used by * POST /api/companies/:id/agents to reject hires with a name that diff --git a/src/main/database/approvals.ts b/src/main/database/approvals.ts index 3cfb85c5..e3c791a7 100644 --- a/src/main/database/approvals.ts +++ b/src/main/database/approvals.ts @@ -130,6 +130,50 @@ export function buildApprovalMethods( getDb().prepare("delete from approvals where id = ? and company_id = ?").run(id, companyId); }, + /** + * Returns the requesting agent's pending approvals for standup + * aggregation (id, type, state, payloadSummary, decisionNote). + */ + listAgentPendingApprovals( + companyId: string, + agentId: string, + ): Array<{ + id: string; + type: string; + state: string; + payloadSummary: string | null; + decisionNote: string | null; + }> { + return readAll( + "select id, type, state, payload_summary as payloadSummary, decision_note as decisionNote from approvals where company_id = ? and requested_by_agent_id = ? and state = 'pending' order by created_at desc", + companyId, + agentId, + ); + }, + + /** + * Returns up to `limit` of the requesting agent's resolved approvals + * for standup aggregation (state != 'pending'). + */ + listAgentResolvedApprovals( + companyId: string, + agentId: string, + limit = 5, + ): Array<{ + id: string; + type: string; + state: string; + payloadSummary: string | null; + decisionNote: string | null; + }> { + return readAll( + "select id, type, state, payload_summary as payloadSummary, decision_note as decisionNote from approvals where company_id = ? and requested_by_agent_id = ? and state != 'pending' order by created_at desc limit ?", + companyId, + agentId, + limit, + ); + }, + /** * Returns the slim approval projection used by * GET /api/agents/me/standup. Ordered by created_at desc. diff --git a/src/main/database/tasks-crud.ts b/src/main/database/tasks-crud.ts index 04c79862..8f279914 100644 --- a/src/main/database/tasks-crud.ts +++ b/src/main/database/tasks-crud.ts @@ -190,6 +190,46 @@ export function buildTaskCrudMethods( ); }, + /** + * Returns the slim task projection (id, title, status, priority) for a + * single assignee, ordered by created_at desc. Used by the alternate + * me/standup builder in routes-agents-standup.ts. + */ + listAssigneeTasksForStandup( + companyId: string, + agentId: string, + ): Array<{ id: string; title: string; status: string; priority: string }> { + return readAll( + "select id, title, status, priority from tasks where company_id = ? and assignee_agent_id = ? order by created_at desc", + companyId, + agentId, + ); + }, + + /** + * Returns the slim task projection for a set of assignees. Safe on + * an empty assignee list (returns []). Used by the alternate + * me/standup builder to fetch reports' tasks in one query. + */ + listTasksForAssignees( + companyId: string, + assigneeIds: string[], + ): Array<{ + id: string; + title: string; + status: string; + priority: string; + assigneeAgentId: string; + }> { + if (assigneeIds.length === 0) return []; + const placeholders = assigneeIds.map(() => "?").join(","); + return readAll( + `select id, title, status, priority, assignee_agent_id as assigneeAgentId from tasks where company_id = ? and assignee_agent_id in (${placeholders}) order by created_at desc`, + companyId, + ...assigneeIds, + ); + }, + /** * Returns the slim task projection used by the me/standup aggregation. * Ordered by created_at desc.