From d82963ce87de8c1298ef28ced660d10009fb9ad9 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 10 Apr 2026 11:26:22 -0500 Subject: [PATCH] fix(agents): clear heartbeat refs before removal --- .../__tests__/cleanup-removal-service.test.ts | 27 +++++++++++++++++ server/src/services/agents.ts | 29 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/cleanup-removal-service.test.ts b/server/src/__tests__/cleanup-removal-service.test.ts index f74f3798f15..c5753afe610 100644 --- a/server/src/__tests__/cleanup-removal-service.test.ts +++ b/server/src/__tests__/cleanup-removal-service.test.ts @@ -7,6 +7,7 @@ import { companies, companySkills, createDb, + heartbeatRunEvents, heartbeatRuns, issueComments, issueExecutionDecisions, @@ -44,6 +45,7 @@ describeEmbeddedPostgres("cleanup removal services", () => { await db.delete(issueComments); await db.delete(issueExecutionDecisions); await db.delete(companySkills); + await db.delete(heartbeatRunEvents); await db.delete(heartbeatRuns); await db.delete(issues); await db.delete(agents); @@ -99,6 +101,15 @@ describeEmbeddedPostgres("cleanup removal services", () => { contextSnapshot: { issueId }, }); + await db + .update(issues) + .set({ + executionRunId: runId, + executionAgentNameKey: "codexcoder", + executionLockedAt: new Date(), + }) + .where(eq(issues.id, issueId)); + return { agentId, companyId, issueId, runId }; } @@ -137,13 +148,29 @@ describeEmbeddedPostgres("cleanup removal services", () => { createdByRunId: runId, }); + await db.insert(heartbeatRunEvents).values({ + companyId, + runId, + agentId, + seq: 1, + eventType: "status", + message: "Run finished", + }); + const removed = await agentService(db).remove(agentId); expect(removed?.id).toBe(agentId); await expect(db.select().from(agents).where(eq(agents.id, agentId))).resolves.toHaveLength(0); await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId))).resolves.toHaveLength(0); + await expect(db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, runId))).resolves.toHaveLength(0); await expect(db.select().from(issueComments).where(eq(issueComments.issueId, issueId))).resolves.toHaveLength(0); await expect(db.select().from(activityLog).where(eq(activityLog.companyId, companyId))).resolves.toHaveLength(0); + await expect( + db + .select({ executionRunId: issues.executionRunId }) + .from(issues) + .where(eq(issues.id, issueId)), + ).resolves.toEqual([{ executionRunId: null }]); }); it("removes issue read states and activity rows before deleting the company", async () => { diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index abba741491c..dbcdb509258 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -477,11 +477,38 @@ export function agentService(db: Db) { if (!existing) return null; return db.transaction(async (tx) => { + const agentRunIds = await tx + .select({ id: heartbeatRuns.id }) + .from(heartbeatRuns) + .where(eq(heartbeatRuns.agentId, id)) + .then((rows) => rows.map((row) => row.id)); + await tx.update(agents).set({ reportsTo: null }).where(eq(agents.reportsTo, id)); await tx .update(issues) - .set({ assigneeAgentId: null, createdByAgentId: null }) + .set({ + assigneeAgentId: null, + createdByAgentId: null, + ...(agentRunIds.length > 0 + ? { + executionRunId: null, + executionAgentNameKey: null, + executionLockedAt: null, + } + : {}), + }) .where(or(eq(issues.assigneeAgentId, id), eq(issues.createdByAgentId, id))); + if (agentRunIds.length > 0) { + await tx + .update(issues) + .set({ + executionRunId: null, + executionAgentNameKey: null, + executionLockedAt: null, + }) + .where(inArray(issues.executionRunId, agentRunIds)); + await tx.delete(heartbeatRunEvents).where(inArray(heartbeatRunEvents.runId, agentRunIds)); + } await tx.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.agentId, id)); await tx.delete(agentTaskSessions).where(eq(agentTaskSessions.agentId, id)); await tx.delete(activityLog).where(