From 8e82ac7e38483e93b633458521fdf1ce425ac69a Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 12 Apr 2026 20:57:31 -0500 Subject: [PATCH] Handle harness checkout conflicts gracefully --- .../heartbeat-comment-wake-batching.test.ts | 1 + server/src/services/heartbeat.ts | 15 ++++++++++++--- 2 files changed, 13 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 2ed58443..3626c2f5 100644 --- a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts +++ b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts @@ -574,4 +574,5 @@ describe("heartbeat comment wake batching", () => { await gateway.close(); } }, 20_000); + }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 6fbcfe10..e0e0a317 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -17,7 +17,7 @@ import { projects, projectWorkspaces, } from "@paperclipai/db"; -import { conflict, notFound } from "../errors.js"; +import { conflict, HttpError, notFound } from "../errors.js"; import { logger } from "../middleware/logger.js"; import { publishLiveEvent } from "./live-events.js"; import { getRunLogStore, type RunLogHandle } from "./run-log-store.js"; @@ -787,6 +787,10 @@ function shouldAutoCheckoutIssueForWake(input: { return true; } +function isCheckoutConflictError(error: unknown): boolean { + return error instanceof HttpError && error.status === 409 && error.message === "Issue checkout conflict"; +} + function deriveCommentId( contextSnapshot: Record | null | undefined, payload: Record | null | undefined, @@ -2704,8 +2708,13 @@ export function heartbeatService(db: Db) { agentId: agent.id, }) ) { - await issuesSvc.checkout(issueId, agent.id, ["todo", "backlog", "blocked"], run.id); - context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = true; + try { + await issuesSvc.checkout(issueId, agent.id, ["todo", "backlog", "blocked"], run.id); + context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = true; + } catch (error) { + if (!isCheckoutConflictError(error)) throw error; + context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = false; + } issueContext = await getIssueExecutionContext(agent.companyId, issueId); } const issueAssigneeOverrides =