diff --git a/server/src/__tests__/heartbeat-dependency-scheduling.test.ts b/server/src/__tests__/heartbeat-dependency-scheduling.test.ts index 8e9d2245c82..6327c84c897 100644 --- a/server/src/__tests__/heartbeat-dependency-scheduling.test.ts +++ b/server/src/__tests__/heartbeat-dependency-scheduling.test.ts @@ -2,9 +2,11 @@ import { randomUUID } from "node:crypto"; import { and, eq, sql } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { + activityLog, agents, agentRuntimeState, agentWakeupRequests, + companySkills, companies, createDb, documentRevisions, @@ -95,6 +97,8 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () = afterEach(async () => { vi.clearAllMocks(); runningProcesses.clear(); + await db.delete(activityLog); + await db.delete(companySkills); await db.delete(issueComments); await db.delete(issueDocuments); await db.delete(documentRevisions); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index a85761e4e62..5e4d14ff50d 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -3129,6 +3129,7 @@ export function heartbeatService(db: Db) { const dependencyReadiness = await issuesSvc.listDependencyReadiness(run.companyId, [issueId]); const unresolvedBlockerCount = dependencyReadiness.get(issueId)?.unresolvedBlockerCount ?? 0; if (unresolvedBlockerCount > 0) { + logger.debug({ runId: run.id, issueId, unresolvedBlockerCount }, "claimQueuedRun: skipping blocked run"); return null; } } @@ -3965,7 +3966,7 @@ export function heartbeatService(db: Db) { if (run.status === "queued") { const claimed = await claimQueuedRun(run); if (!claimed) { - // Another worker has already claimed or finalized this run. + // claimQueuedRun can also leave the run queued when dependencies are unresolved. return; } run = claimed; diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index ebbd958f365..67c262311d9 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -241,6 +241,8 @@ async function listIssueDependencyReadinessMap( for (const row of blockerRows) { const current = readinessMap.get(row.issueId) ?? createIssueDependencyReadiness(row.issueId); current.blockerIssueIds.push(row.blockerIssueId); + // Only done blockers resolve dependents; cancelled blockers stay unresolved + // until an operator removes or replaces the blocker relationship explicitly. if (row.blockerStatus !== "done") { current.unresolvedBlockerIssueIds.push(row.blockerIssueId); current.unresolvedBlockerCount += 1; @@ -267,6 +269,7 @@ async function listUnresolvedBlockerIssueIds( and( eq(issues.companyId, companyId), inArray(issues.id, uniqueBlockerIssueIds), + // Cancelled blockers intentionally remain unresolved until the relation changes. ne(issues.status, "done"), ), )