Files
Codewalkers/apps/server/trpc/routers/headquarters.ts
Lukas May 1e16ad82e8 fix: Show conflict resolution agents in HQ dashboard
getHeadquartersDashboard had no section for active conflict agents,
so initiatives with a running conflict-* agent disappeared from all
HQ sections. Add resolvingConflicts array to surface them.
2026-03-06 16:39:48 +01:00

249 lines
8.9 KiB
TypeScript

/**
* Headquarters Router
*
* Provides the composite dashboard query for the Headquarters page,
* aggregating all action items that require user intervention.
*/
import type { ProcedureBuilder } from '../trpc.js';
import type { Phase } from '../../db/schema.js';
import {
requireAgentManager,
requireInitiativeRepository,
requirePhaseRepository,
} from './_helpers.js';
export function headquartersProcedures(publicProcedure: ProcedureBuilder) {
return {
getHeadquartersDashboard: publicProcedure.query(async ({ ctx }) => {
const initiativeRepo = requireInitiativeRepository(ctx);
const phaseRepo = requirePhaseRepository(ctx);
const agentManager = requireAgentManager(ctx);
const [allInitiatives, allAgents] = await Promise.all([
initiativeRepo.findAll(),
agentManager.list(),
]);
// Relevant initiatives: status in ['active', 'pending_review']
const relevantInitiatives = allInitiatives.filter(
(i) => i.status === 'active' || i.status === 'pending_review',
);
// Non-dismissed agents only
const activeAgents = allAgents.filter((a) => !a.userDismissedAt);
// Fast lookup map: initiative id → initiative
const initiativeMap = new Map(relevantInitiatives.map((i) => [i.id, i]));
// Batch-fetch all phases for relevant initiatives in parallel
const phasesByInitiative = new Map<string, Phase[]>();
await Promise.all(
relevantInitiatives.map(async (init) => {
const phases = await phaseRepo.findByInitiativeId(init.id);
phasesByInitiative.set(init.id, phases);
}),
);
// -----------------------------------------------------------------------
// Section 1: waitingForInput
// -----------------------------------------------------------------------
const waitingAgents = activeAgents.filter((a) => a.status === 'waiting_for_input');
const pendingQuestionsResults = await Promise.all(
waitingAgents.map((a) => agentManager.getPendingQuestions(a.id)),
);
const waitingForInput = waitingAgents
.map((agent, i) => {
const initiative = agent.initiativeId ? initiativeMap.get(agent.initiativeId) : undefined;
return {
agentId: agent.id,
agentName: agent.name,
initiativeId: agent.initiativeId,
initiativeName: initiative?.name ?? null,
questionText: pendingQuestionsResults[i]?.questions[0]?.question ?? '',
waitingSince: agent.updatedAt.toISOString(),
};
})
.sort((a, b) => a.waitingSince.localeCompare(b.waitingSince));
// -----------------------------------------------------------------------
// Section 2a: pendingReviewInitiatives
// -----------------------------------------------------------------------
const pendingReviewInitiatives = relevantInitiatives
.filter((i) => i.status === 'pending_review')
.map((i) => ({
initiativeId: i.id,
initiativeName: i.name,
since: i.updatedAt.toISOString(),
}))
.sort((a, b) => a.since.localeCompare(b.since));
// -----------------------------------------------------------------------
// Section 2b: pendingReviewPhases
// -----------------------------------------------------------------------
const pendingReviewPhases: Array<{
initiativeId: string;
initiativeName: string;
phaseId: string;
phaseName: string;
since: string;
}> = [];
for (const [initiativeId, phases] of phasesByInitiative) {
const initiative = initiativeMap.get(initiativeId)!;
for (const phase of phases) {
if (phase.status === 'pending_review') {
pendingReviewPhases.push({
initiativeId,
initiativeName: initiative.name,
phaseId: phase.id,
phaseName: phase.name,
since: phase.updatedAt.toISOString(),
});
}
}
}
pendingReviewPhases.sort((a, b) => a.since.localeCompare(b.since));
// -----------------------------------------------------------------------
// Section 3: planningInitiatives
// -----------------------------------------------------------------------
const planningInitiatives: Array<{
initiativeId: string;
initiativeName: string;
pendingPhaseCount: number;
since: string;
}> = [];
for (const initiative of relevantInitiatives) {
if (initiative.status !== 'active') continue;
const phases = phasesByInitiative.get(initiative.id) ?? [];
if (phases.length === 0) continue;
const allPending = phases.every((p) => p.status === 'pending');
if (!allPending) continue;
const hasActiveAgent = activeAgents.some(
(a) =>
a.initiativeId === initiative.id &&
(a.status === 'running' || a.status === 'waiting_for_input'),
);
if (hasActiveAgent) continue;
const sortedByCreatedAt = [...phases].sort(
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
);
planningInitiatives.push({
initiativeId: initiative.id,
initiativeName: initiative.name,
pendingPhaseCount: phases.length,
since: sortedByCreatedAt[0].createdAt.toISOString(),
});
}
planningInitiatives.sort((a, b) => a.since.localeCompare(b.since));
// -----------------------------------------------------------------------
// Section 4: resolvingConflicts
// -----------------------------------------------------------------------
const resolvingConflicts: Array<{
initiativeId: string;
initiativeName: string;
agentId: string;
agentName: string;
agentStatus: string;
since: string;
}> = [];
for (const agent of activeAgents) {
if (
agent.name?.startsWith('conflict-') &&
(agent.status === 'running' || agent.status === 'waiting_for_input') &&
agent.initiativeId
) {
const initiative = initiativeMap.get(agent.initiativeId);
if (initiative) {
resolvingConflicts.push({
initiativeId: initiative.id,
initiativeName: initiative.name,
agentId: agent.id,
agentName: agent.name,
agentStatus: agent.status,
since: agent.updatedAt.toISOString(),
});
}
}
}
resolvingConflicts.sort((a, b) => a.since.localeCompare(b.since));
// -----------------------------------------------------------------------
// Section 5: blockedPhases
// -----------------------------------------------------------------------
const blockedPhases: Array<{
initiativeId: string;
initiativeName: string;
phaseId: string;
phaseName: string;
lastMessage: string | null;
since: string;
}> = [];
for (const initiative of relevantInitiatives) {
if (initiative.status !== 'active') continue;
const phases = phasesByInitiative.get(initiative.id) ?? [];
for (const phase of phases) {
if (phase.status !== 'blocked') continue;
let lastMessage: string | null = null;
try {
if (ctx.taskRepository && ctx.messageRepository) {
const taskRepo = ctx.taskRepository;
const messageRepo = ctx.messageRepository;
const tasks = await taskRepo.findByPhaseId(phase.id);
const phaseAgentIds = allAgents
.filter((a) => tasks.some((t) => t.id === a.taskId))
.map((a) => a.id);
if (phaseAgentIds.length > 0) {
const messageLists = await Promise.all(
phaseAgentIds.map((id) => messageRepo.findBySender('agent', id)),
);
const allMessages = messageLists
.flat()
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
if (allMessages.length > 0) {
lastMessage = allMessages[0].content.slice(0, 160);
}
}
}
} catch {
// Non-critical: message retrieval failure does not crash the dashboard
}
blockedPhases.push({
initiativeId: initiative.id,
initiativeName: initiative.name,
phaseId: phase.id,
phaseName: phase.name,
lastMessage,
since: phase.updatedAt.toISOString(),
});
}
}
blockedPhases.sort((a, b) => a.since.localeCompare(b.since));
return {
waitingForInput,
pendingReviewInitiatives,
pendingReviewPhases,
planningInitiatives,
resolvingConflicts,
blockedPhases,
};
}),
};
}