From 31a73c8c9c508ba1f22080705d001d8efc97bcc4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 11 Apr 2026 21:57:06 -0500 Subject: [PATCH] fix: treat run summary comment as fallback-only --- .../heartbeat-comment-wake-batching.test.ts | 114 ++++++++++++++++++ server/src/services/heartbeat.ts | 9 +- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts index 91e1112bd8a..4f9678ef432 100644 --- a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts +++ b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts @@ -761,4 +761,118 @@ describe("heartbeat comment wake batching", () => { } }, 20_000); + it("treats the automatic run summary as fallback-only when the run already posted a comment", async () => { + const gateway = await createControlledGatewayServer(); + const companyId = randomUUID(); + const agentId = randomUUID(); + const issueId = randomUUID(); + const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; + const heartbeat = heartbeatService(db); + + try { + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "Gateway Agent", + role: "engineer", + status: "idle", + adapterType: "openclaw_gateway", + adapterConfig: { + url: gateway.url, + headers: { + "x-openclaw-token": "gateway-token", + }, + payloadTemplate: { + message: "wake now", + }, + waitTimeoutMs: 2_000, + }, + runtimeConfig: {}, + permissions: {}, + }); + + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Use existing comment", + status: "todo", + priority: "medium", + assigneeAgentId: agentId, + issueNumber: 1, + identifier: `${issuePrefix}-1`, + }); + + const firstRun = await heartbeat.wakeup(agentId, { + source: "assignment", + triggerDetail: "system", + reason: "issue_assigned", + payload: { issueId }, + contextSnapshot: { + issueId, + taskId: issueId, + wakeReason: "issue_assigned", + }, + requestedByActorType: "system", + requestedByActorId: null, + }); + + expect(firstRun).not.toBeNull(); + await waitFor(() => gateway.getAgentPayloads().length === 1); + + await db.insert(issueComments).values({ + companyId, + issueId, + authorAgentId: agentId, + authorUserId: null, + createdByRunId: firstRun!.id, + body: "Manual completion comment from the run.", + }); + + gateway.releaseFirstWait(); + + await waitFor(async () => { + const runs = await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.agentId, agentId)); + return runs.length === 1 && runs[0]?.status === "succeeded" && runs[0]?.issueCommentStatus === "satisfied"; + }); + + const runs = await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.agentId, agentId)); + + expect(runs).toHaveLength(1); + expect(runs[0]?.issueCommentStatus).toBe("satisfied"); + expect(runs[0]?.issueCommentSatisfiedByCommentId).not.toBeNull(); + + const comments = await db + .select() + .from(issueComments) + .where(eq(issueComments.issueId, issueId)) + .orderBy(asc(issueComments.createdAt)); + + expect(comments).toHaveLength(1); + expect(comments[0]?.body).toBe("Manual completion comment from the run."); + expect(comments[0]?.createdByRunId).toBe(firstRun?.id); + + const wakeups = await db + .select() + .from(agentWakeupRequests) + .where(and(eq(agentWakeupRequests.companyId, companyId), eq(agentWakeupRequests.agentId, agentId))); + + expect(wakeups).toHaveLength(1); + } finally { + gateway.releaseFirstWait(); + await gateway.close(); + } + }, 20_000); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 8287c5a02f2..cf621783e9a 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -3486,9 +3486,12 @@ export function heartbeatService(db: Db) { }); if (issueId && outcome === "succeeded") { try { - const issueComment = buildHeartbeatRunIssueComment(persistedResultJson); - if (issueComment) { - await issuesSvc.addComment(issueId, issueComment, { agentId: agent.id, runId: finalizedRun.id }); + const existingRunComment = await findRunIssueComment(finalizedRun.id, finalizedRun.companyId, issueId); + if (!existingRunComment) { + const issueComment = buildHeartbeatRunIssueComment(persistedResultJson); + if (issueComment) { + await issuesSvc.addComment(issueId, issueComment, { agentId: agent.id, runId: finalizedRun.id }); + } } } catch (err) { await onLog(