mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
Address attention queue review feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
aa5aa8ec6d
commit
4148ed5ca7
3 files changed
+92
-15
No files matched your search
@@ -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();
|
||||
|
||||
@@ -253,6 +253,10 @@ function readRunIssueId(contextSnapshot: Record<string, unknown> | 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<AttentionFeed> => {
|
||||
@@ -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<Date | null>((earliest, row) => {
|
||||
if (earliest == null || row.createdAt.getTime() < earliest.getTime()) return row.createdAt;
|
||||
return earliest;
|
||||
}, null);
|
||||
const latestRunCreatedAtByPair = new Map<string, Date>();
|
||||
if (earliestFailedAt && failedAgentIds.length > 0) {
|
||||
const newerRunRows = await db
|
||||
.select({
|
||||
agentId: heartbeatRuns.agentId,
|
||||
issueKey: sql<string>`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}`;
|
||||
|
||||
@@ -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) });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user