From 4a9f38c4e1f5ad95294327382f5db488533c6131 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 6 Mar 2026 21:35:29 +0100 Subject: [PATCH] =?UTF-8?q?perf:=20replace=20O(N=C2=B7chunks)=20listForRad?= =?UTF-8?q?ar=20read=20path=20with=20O(N=C2=B7agents)=20metrics=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit listForRadar previously called findByAgentIds() and JSON-parsed every chunk to compute questionsCount, subagentsCount, and compactionsCount. Switch to findMetricsByAgentIds() which reads the pre-computed agent_metrics table, eliminating the chunk scan and per-row JSON.parse entirely. Add two new test cases: agent with no metrics row returns zero counts, and listForRadar response rows never carry chunk content. Co-Authored-By: Claude Sonnet 4.6 --- .../server/test/unit/radar-procedures.test.ts | 41 +++++++++++++++++++ apps/server/trpc/routers/agent.ts | 37 ++++------------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/apps/server/test/unit/radar-procedures.test.ts b/apps/server/test/unit/radar-procedures.test.ts index d7acba6..10eb878 100644 --- a/apps/server/test/unit/radar-procedures.test.ts +++ b/apps/server/test/unit/radar-procedures.test.ts @@ -325,6 +325,47 @@ describe('agent.listForRadar', () => { expect(row!.subagentsCount).toBe(0); expect(row!.compactionsCount).toBe(0); }); + + it('returns zero counts for agent with no metrics row', async () => { + const agents = new MockAgentManager(); + const ctx = makeCtx(agents); + + const now = new Date(); + // Agent with no log chunks at all — no agent_metrics row will exist + agents.addAgent({ id: 'agent-no-chunks', name: 'no-chunks-agent', status: 'running', createdAt: now }); + + const caller = createAgentCaller(ctx); + const result = await caller.listForRadar({ timeRange: 'all' }); + + const row = result.find(r => r.id === 'agent-no-chunks'); + expect(row).toBeDefined(); + expect(row!.questionsCount).toBe(0); + expect(row!.subagentsCount).toBe(0); + expect(row!.compactionsCount).toBe(0); + }); + + it('listForRadar response does not contain chunk content field', async () => { + const agents = new MockAgentManager(); + const ctx = makeCtx(agents); + const { logChunkRepo } = getRepos(ctx); + + const now = new Date(); + agents.addAgent({ id: 'agent-content', name: 'content-agent', status: 'running', createdAt: now }); + + await logChunkRepo.insertChunk({ + agentId: 'agent-content', + agentName: 'content-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent', input: { description: 'do stuff', prompt: 'some prompt' } }), + }); + + const caller = createAgentCaller(ctx); + const result = await caller.listForRadar({ timeRange: 'all' }); + + for (const row of result) { + expect(row).not.toHaveProperty('content'); + } + }); }); // ============================================================================= diff --git a/apps/server/trpc/routers/agent.ts b/apps/server/trpc/routers/agent.ts index 644b814..82d397d 100644 --- a/apps/server/trpc/routers/agent.ts +++ b/apps/server/trpc/routers/agent.ts @@ -475,8 +475,8 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { const uniqueTaskIds = [...new Set(filteredAgents.map(a => a.taskId).filter(Boolean) as string[])]; const uniqueInitiativeIds = [...new Set(filteredAgents.map(a => a.initiativeId).filter(Boolean) as string[])]; - const [chunks, messageCounts, taskResults, initiativeResults] = await Promise.all([ - logChunkRepo.findByAgentIds(matchingIds), + const [metrics, messageCounts, taskResults, initiativeResults] = await Promise.all([ + logChunkRepo.findMetricsByAgentIds(matchingIds), conversationRepo.countByFromAgentIds(matchingIds), Promise.all(uniqueTaskIds.map(id => taskRepo.findById(id))), Promise.all(uniqueInitiativeIds.map(id => initiativeRepo.findById(id))), @@ -486,37 +486,14 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { const taskMap = new Map(taskResults.filter(Boolean).map(t => [t!.id, t!.name])); const initiativeMap = new Map(initiativeResults.filter(Boolean).map(i => [i!.id, i!.name])); const messagesMap = new Map(messageCounts.map(m => [m.agentId, m.count])); - - // Group chunks by agentId - const chunksByAgent = new Map(); - for (const chunk of chunks) { - const existing = chunksByAgent.get(chunk.agentId); - if (existing) { - existing.push(chunk); - } else { - chunksByAgent.set(chunk.agentId, [chunk]); - } - } + const metricsMap = new Map(metrics.map(m => [m.agentId, m])); // Build result rows return filteredAgents.map(agent => { - const agentChunks = chunksByAgent.get(agent.id) ?? []; - let questionsCount = 0; - let subagentsCount = 0; - let compactionsCount = 0; - - for (const chunk of agentChunks) { - try { - const parsed = JSON.parse(chunk.content); - if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') { - questionsCount += parsed.input?.questions?.length ?? 0; - } else if (parsed.type === 'tool_use' && parsed.name === 'Agent') { - subagentsCount++; - } else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') { - compactionsCount++; - } - } catch { /* skip malformed */ } - } + const agentMetrics = metricsMap.get(agent.id); + const questionsCount = agentMetrics?.questionsCount ?? 0; + const subagentsCount = agentMetrics?.subagentsCount ?? 0; + const compactionsCount = agentMetrics?.compactionsCount ?? 0; return { id: agent.id,