Cancel stale queued heartbeats when issue graph changes (PAP-2314) (#4534)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-04-26 21:17:38 -05:00 committed by GitHub
parent 868d08903e
commit 82e257c7ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1991 additions and 238 deletions

View file

@ -1,10 +1,11 @@
import { Buffer } from "node:buffer";
import { and, asc, desc, eq, gt, inArray, isNull, lt, ne, or, sql } from "drizzle-orm";
import { and, asc, desc, eq, gt, inArray, isNull, lt, ne, notInArray, or, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
activityLog,
agentWakeupRequests,
agents,
approvals,
assets,
companies,
companyMemberships,
@ -12,6 +13,7 @@ import {
goals,
heartbeatRuns,
executionWorkspaces,
issueApprovals,
issueAttachments,
issueInboxArchives,
issueLabels,
@ -19,6 +21,7 @@ import {
issueComments,
issueDocuments,
issueReadStates,
issueThreadInteractions,
issues,
labels,
projectWorkspaces,
@ -660,6 +663,10 @@ async function withIssueLabels(dbOrTx: any, rows: IssueRow[]): Promise<IssueWith
const ACTIVE_RUN_STATUSES = ["queued", "running"];
const BLOCKER_ATTENTION_ACTIVE_RUN_STATUSES = ["queued", "running"];
const BLOCKER_ATTENTION_ACTIVE_WAKE_STATUSES = ["queued", "deferred_issue_execution"];
const BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES = ["pending"];
const BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES = ["pending", "revision_requested"];
const BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND = "harness_liveness_escalation";
const BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES = ["done", "cancelled"];
const BLOCKER_ATTENTION_MAX_DEPTH = 8;
const BLOCKER_ATTENTION_MAX_NODES = 2000;
const BLOCKER_ATTENTION_INVOKABLE_AGENT_STATUSES = new Set(["active", "idle", "running", "error"]);
@ -742,8 +749,10 @@ function createIssueBlockerAttention(input: Partial<IssueBlockerAttention> = {})
reason: input.reason ?? null,
unresolvedBlockerCount: input.unresolvedBlockerCount ?? 0,
coveredBlockerCount: input.coveredBlockerCount ?? 0,
stalledBlockerCount: input.stalledBlockerCount ?? 0,
attentionBlockerCount: input.attentionBlockerCount ?? 0,
sampleBlockerIdentifier: input.sampleBlockerIdentifier ?? null,
sampleStalledBlockerIdentifier: input.sampleStalledBlockerIdentifier ?? null,
};
}
@ -1026,6 +1035,55 @@ async function listIssueBlockerAttentionMap(
}
}
const reviewNodeIds = [...nodesById.values()]
.filter((node) => node.status === "in_review")
.map((node) => node.id);
const explicitWaitingIssueIds = new Set<string>();
if (reviewNodeIds.length > 0) {
for (const chunk of chunkList(reviewNodeIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
const interactionRows: Array<{ issueId: string }> = await dbOrTx
.select({ issueId: issueThreadInteractions.issueId })
.from(issueThreadInteractions)
.where(
and(
eq(issueThreadInteractions.companyId, companyId),
inArray(issueThreadInteractions.status, BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES),
inArray(issueThreadInteractions.issueId, chunk),
),
);
for (const row of interactionRows) explicitWaitingIssueIds.add(row.issueId);
const approvalRows: Array<{ issueId: string }> = await dbOrTx
.select({ issueId: issueApprovals.issueId })
.from(issueApprovals)
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
.where(
and(
eq(issueApprovals.companyId, companyId),
inArray(approvals.status, BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES),
inArray(issueApprovals.issueId, chunk),
),
);
for (const row of approvalRows) explicitWaitingIssueIds.add(row.issueId);
const recoveryRows: Array<{ originId: string | null }> = await dbOrTx
.select({ originId: issues.originId })
.from(issues)
.where(
and(
eq(issues.companyId, companyId),
eq(issues.originKind, BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND),
isNull(issues.hiddenAt),
inArray(issues.originId, chunk),
notInArray(issues.status, BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES),
),
);
for (const row of recoveryRows) {
if (row.originId) explicitWaitingIssueIds.add(row.originId);
}
}
}
const agentRows: IssueBlockerAttentionAgentRow[] = agentIds.size > 0
? await dbOrTx
.select({
@ -1038,39 +1096,83 @@ async function listIssueBlockerAttentionMap(
: [];
const agentsById = new Map(agentRows.map((agent) => [agent.id, agent]));
type PathClassification = { covered: boolean; sampleBlockerIdentifier: string | null };
type PathClassification = {
covered: boolean;
stalled: boolean;
sampleBlockerIdentifier: string | null;
sampleStalledBlockerIdentifier: string | null;
};
const classifyPath = (
nodeId: string,
seen: Set<string>,
): PathClassification => {
if (truncated || seen.has(nodeId)) return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(nodesById.get(nodeId)) };
const sample = blockerSampleIdentifier(nodesById.get(nodeId));
if (truncated || seen.has(nodeId)) {
return { covered: false, stalled: false, sampleBlockerIdentifier: sample, sampleStalledBlockerIdentifier: null };
}
const node = nodesById.get(nodeId);
if (!node || node.companyId !== companyId) return { covered: false, sampleBlockerIdentifier: nodeId };
if (node.status === "done") return { covered: true, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
if (activeIssueIds.has(node.id)) return { covered: true, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
if (node.status === "cancelled") return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
if (!node || node.companyId !== companyId) {
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeId, sampleStalledBlockerIdentifier: null };
}
const nodeSample = blockerSampleIdentifier(node);
if (node.status === "done") {
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
}
if (node.status === "in_review") {
const hasWaitingPath = activeIssueIds.has(node.id) || Boolean(node.assigneeUserId) || explicitWaitingIssueIds.has(node.id);
if (hasWaitingPath) {
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
}
return { covered: false, stalled: true, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: nodeSample };
}
if (activeIssueIds.has(node.id)) {
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
}
if (node.status === "cancelled") {
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
}
const downstream = (edgesByIssueId.get(node.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
if (downstream.length > 0) {
const nextSeen = new Set(seen);
nextSeen.add(nodeId);
const classified = downstream.map((edge) => classifyPath(edge.blockerIssueId, nextSeen));
const attention = classified.find((result) => !result.covered);
if (attention) return attention;
const stalledChild = classified.find((result) => result.stalled || result.sampleStalledBlockerIdentifier);
const sampleStalled = stalledChild?.sampleStalledBlockerIdentifier ?? null;
const hardAttention = classified.find((result) => !result.covered && !result.stalled);
if (hardAttention) {
return {
covered: false,
stalled: false,
sampleBlockerIdentifier: hardAttention.sampleBlockerIdentifier,
sampleStalledBlockerIdentifier: sampleStalled,
};
}
const stalledEntry = classified.find((result) => result.stalled);
if (stalledEntry) {
return {
covered: false,
stalled: true,
sampleBlockerIdentifier: stalledEntry.sampleBlockerIdentifier,
sampleStalledBlockerIdentifier: sampleStalled,
};
}
return {
covered: true,
sampleBlockerIdentifier: classified[0]?.sampleBlockerIdentifier ?? blockerSampleIdentifier(node),
stalled: false,
sampleBlockerIdentifier: classified[0]?.sampleBlockerIdentifier ?? nodeSample,
sampleStalledBlockerIdentifier: null,
};
}
if (node.assigneeAgentId) {
const assignee = agentsById.get(node.assigneeAgentId);
if (!assignee || assignee.companyId !== companyId || !BLOCKER_ATTENTION_INVOKABLE_AGENT_STATUSES.has(assignee.status)) {
return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
}
}
return { covered: false, sampleBlockerIdentifier: blockerSampleIdentifier(node) };
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
};
for (const root of roots) {
@ -1088,22 +1190,41 @@ async function listIssueBlockerAttentionMap(
result: classifyPath(edge.blockerIssueId, new Set([root.id])),
}));
const coveredBlockerCount = classified.filter((entry) => entry.result.covered).length;
const attentionBlockerCount = classified.length - coveredBlockerCount;
const attentionEntry = classified.find((entry) => !entry.result.covered);
const sampleEntry = attentionEntry ?? classified[0] ?? null;
const stalledBlockerCount = classified.filter((entry) => entry.result.stalled).length;
const attentionBlockerCount = classified.length - coveredBlockerCount - stalledBlockerCount;
const hardAttentionEntry = classified.find((entry) => !entry.result.covered && !entry.result.stalled);
const stalledEntry = classified.find((entry) => entry.result.stalled);
const sampleEntry = hardAttentionEntry ?? stalledEntry ?? classified[0] ?? null;
const sampleNode = sampleEntry ? nodesById.get(sampleEntry.edge.blockerIssueId) : null;
const sampleStalledFromChain = classified
.map((entry) => entry.result.sampleStalledBlockerIdentifier)
.find((value) => value);
let state: IssueBlockerAttention["state"];
let reason: IssueBlockerAttention["reason"];
if (attentionBlockerCount > 0) {
state = "needs_attention";
reason = "attention_required";
} else if (stalledBlockerCount > 0) {
state = "stalled";
reason = "stalled_review";
} else {
state = "covered";
reason = topLevelEdges.every((edge) => nodesById.get(edge.blockerIssueId)?.parentId === root.id)
? "active_child"
: "active_dependency";
}
attentionMap.set(root.id, createIssueBlockerAttention({
state: attentionBlockerCount === 0 ? "covered" : "needs_attention",
reason: attentionBlockerCount === 0
? topLevelEdges.every((edge) => nodesById.get(edge.blockerIssueId)?.parentId === root.id)
? "active_child"
: "active_dependency"
: "attention_required",
state,
reason,
unresolvedBlockerCount: topLevelEdges.length,
coveredBlockerCount,
stalledBlockerCount,
attentionBlockerCount,
sampleBlockerIdentifier: sampleEntry?.result.sampleBlockerIdentifier ?? blockerSampleIdentifier(sampleNode),
sampleStalledBlockerIdentifier:
stalledEntry?.result.sampleStalledBlockerIdentifier ?? sampleStalledFromChain ?? null,
}));
}