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.
249 lines
8.9 KiB
TypeScript
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,
|
|
};
|
|
}),
|
|
};
|
|
}
|