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.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 22:39:43 -04:00
1 parent 4342cf3e2f
commit 9874d825b6
5 files changed
+146 -94

No files matched your search

+10 -94
View File
@@ -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<DirectReportRow>(
`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<MyTaskRow>(
`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<ReportTaskRow>(
`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<ReportStatusRow>(
`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<TaskCommentRow>(
`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<ApprovalRow>(
`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<ApprovalRow>(
`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<string, string>([
+6
View File
@@ -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); }
+46
View File
@@ -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
+44
View File
@@ -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.
+40
View File
@@ -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.