From 4148ed5ca7622fadd0b3fe7093459c6457e00275 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 9 Jul 2026 18:43:27 +0000 Subject: [PATCH] Address attention queue review feedback Co-Authored-By: Paperclip --- .../src/__tests__/attention-service.test.ts | 55 +++++++++++++++++++ server/src/services/attention.ts | 51 ++++++++++++----- ui/src/hooks/useInboxBadge.ts | 1 + 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/server/src/__tests__/attention-service.test.ts b/server/src/__tests__/attention-service.test.ts index c731b3ca56c..1aad6dcfaf9 100644 --- a/server/src/__tests__/attention-service.test.ts +++ b/server/src/__tests__/attention-service.test.ts @@ -569,6 +569,61 @@ describeEmbeddedPostgres("attention service", () => { )).toBe(false); }); + it("suppresses exhausted failed runs when a newer run exists for the same agent and issue", async () => { + const { companyId, workerId } = await seedCompany("ATF"); + const issueId = await insertIssue({ + companyId, + identifier: "ATF-1", + title: "Retry target", + status: "in_progress", + assigneeAgentId: workerId, + updatedAt: new Date("2026-07-09T12:00:00.000Z"), + }); + const exhaustedRunId = randomUUID(); + const newerRunId = randomUUID(); + await db.insert(heartbeatRuns).values([ + { + id: exhaustedRunId, + companyId, + agentId: workerId, + invocationSource: "automation", + status: "failed", + error: "adapter failed", + errorCode: "adapter_failed", + contextSnapshot: { issueId }, + createdAt: new Date("2026-07-09T12:01:00.000Z"), + updatedAt: new Date("2026-07-09T12:01:00.000Z"), + finishedAt: new Date("2026-07-09T12:01:00.000Z"), + }, + { + id: newerRunId, + companyId, + agentId: workerId, + invocationSource: "automation", + status: "succeeded", + contextSnapshot: { issueId }, + createdAt: new Date("2026-07-09T12:02:00.000Z"), + updatedAt: new Date("2026-07-09T12:02:00.000Z"), + finishedAt: new Date("2026-07-09T12:02:00.000Z"), + }, + ]); + await db.insert(heartbeatRunEvents).values({ + companyId, + runId: exhaustedRunId, + agentId: workerId, + seq: 1, + eventType: "lifecycle", + message: "Bounded retry exhausted after 4 scheduled attempts; no further automatic retry will be queued", + payload: { retryReason: "transient_failure", maxAttempts: 4 }, + createdAt: new Date("2026-07-09T12:01:01.000Z"), + }); + + const feed = await attentionService(db).list(companyId, { userId: "board-user" }); + + expect(feed.countsBySourceKind.failed_run).toBe(0); + expect(feed.items.some((item) => item.sourceKind === "failed_run" && item.subject.id === exhaustedRunId)).toBe(false); + }); + it("uses inbox_dismissals with attention-prefixed dedup keys and resurfaces newer activity", async () => { const { companyId } = await seedCompany("ATD"); const approvalId = randomUUID(); diff --git a/server/src/services/attention.ts b/server/src/services/attention.ts index ef240dedb9b..5bbfd06cbd4 100644 --- a/server/src/services/attention.ts +++ b/server/src/services/attention.ts @@ -253,6 +253,10 @@ function readRunIssueId(contextSnapshot: Record | null) { return typeof issueId === "string" && issueId.length > 0 ? issueId : null; } +function runAttentionPairKey(agentId: string, issueId: string | null | undefined) { + return `${agentId}:${issueId ?? ""}`; +} + export function attentionService(db: Db) { return { list: async (companyId: string, options: AttentionListOptions = {}): Promise => { @@ -666,24 +670,41 @@ export function attentionService(db: Db) { if (!latestExhaustedByRunId.has(row.id)) latestExhaustedByRunId.set(row.id, row); } const failedRows = [...latestExhaustedByRunId.values()]; - const failedIssueMap = await issueSummaryMap( - db, - companyId, - failedRows.map((row) => readRunIssueId(row.contextSnapshot)), - ); - for (const run of failedRows) { - const issueId = readRunIssueId(run.contextSnapshot); - const newer = await db - .select({ id: heartbeatRuns.id }) + const failedIssueIdByRunId = new Map(failedRows.map((row) => [row.id, readRunIssueId(row.contextSnapshot)])); + const failedIssueMap = await issueSummaryMap(db, companyId, [...failedIssueIdByRunId.values()]); + const failedAgentIds = [...new Set(failedRows.map((row) => row.agentId))]; + const earliestFailedAt = failedRows.reduce((earliest, row) => { + if (earliest == null || row.createdAt.getTime() < earliest.getTime()) return row.createdAt; + return earliest; + }, null); + const latestRunCreatedAtByPair = new Map(); + if (earliestFailedAt && failedAgentIds.length > 0) { + const newerRunRows = await db + .select({ + agentId: heartbeatRuns.agentId, + issueKey: sql`coalesce(${heartbeatRuns.contextSnapshot} ->> 'issueId', ${heartbeatRuns.contextSnapshot} ->> 'taskId', '')`, + createdAt: heartbeatRuns.createdAt, + }) .from(heartbeatRuns) .where(and( eq(heartbeatRuns.companyId, companyId), - eq(heartbeatRuns.agentId, run.agentId), - gt(heartbeatRuns.createdAt, run.createdAt), - sql`coalesce(${heartbeatRuns.contextSnapshot} ->> 'issueId', ${heartbeatRuns.contextSnapshot} ->> 'taskId', '') = ${issueId ?? ""}`, - )) - .limit(1); - if (newer.length > 0) continue; + inArray(heartbeatRuns.agentId, failedAgentIds), + gt(heartbeatRuns.createdAt, earliestFailedAt), + )); + + for (const row of newerRunRows) { + const key = runAttentionPairKey(row.agentId, row.issueKey); + const current = latestRunCreatedAtByPair.get(key); + if (!current || row.createdAt.getTime() > current.getTime()) { + latestRunCreatedAtByPair.set(key, row.createdAt); + } + } + } + + for (const run of failedRows) { + const issueId = failedIssueIdByRunId.get(run.id) ?? null; + const latestRunCreatedAt = latestRunCreatedAtByPair.get(runAttentionPairKey(run.agentId, issueId)); + if (latestRunCreatedAt && latestRunCreatedAt.getTime() > run.createdAt.getTime()) continue; const issue = issueId ? failedIssueMap.get(issueId) ?? null : null; const dedupKey = `run:${run.id}`; diff --git a/ui/src/hooks/useInboxBadge.ts b/ui/src/hooks/useInboxBadge.ts index 22aaacf855e..77572370cff 100644 --- a/ui/src/hooks/useInboxBadge.ts +++ b/ui/src/hooks/useInboxBadge.ts @@ -90,6 +90,7 @@ export function useInboxDismissals(companyId: string | null | undefined) { if (!companyId) return; queryClient.invalidateQueries({ queryKey }); queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(companyId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.attention(companyId) }); }, });