fix: add executionAgentNameKey to execution lock clears (Greptile)

Issue 1: add executionAgentNameKey = null alongside executionRunId in
Fix B (status change, reassignment) and Fix C (staleness clear UPDATE),
matching the existing pattern used everywhere else in the codebase.

Issue 2: wrap Fix C staleness pre-check in a db.transaction with
SELECT ... FOR UPDATE to make the read + conditional clear atomic,
consistent with the enqueueWakeup() pattern.
This commit is contained in:
chrisschwer 2026-04-03 15:11:42 +02:00
parent 65e0d3d672
commit 72408642b1

View file

@ -1293,6 +1293,7 @@ export function issueService(db: Db) {
patch.checkoutRunId = null; patch.checkoutRunId = null;
// Fix B: also clear the execution lock when leaving in_progress // Fix B: also clear the execution lock when leaving in_progress
patch.executionRunId = null; patch.executionRunId = null;
patch.executionAgentNameKey = null;
patch.executionLockedAt = null; patch.executionLockedAt = null;
} }
if ( if (
@ -1302,6 +1303,7 @@ export function issueService(db: Db) {
patch.checkoutRunId = null; patch.checkoutRunId = null;
// Fix B: clear execution lock on reassignment, matching checkoutRunId clear // Fix B: clear execution lock on reassignment, matching checkoutRunId clear
patch.executionRunId = null; patch.executionRunId = null;
patch.executionAgentNameKey = null;
patch.executionLockedAt = null; patch.executionLockedAt = null;
} }
@ -1387,21 +1389,27 @@ export function issueService(db: Db) {
// Fix C: staleness detection — if executionRunId references a run that is no // Fix C: staleness detection — if executionRunId references a run that is no
// longer queued or running, clear it before applying the execution lock condition // longer queued or running, clear it before applying the execution lock condition
// so a dead lock can't produce a spurious 409. // so a dead lock can't produce a spurious 409.
const preCheckRow = await db // Wrapped in a transaction with SELECT FOR UPDATE to make the read + clear atomic,
.select({ executionRunId: issues.executionRunId }) // matching the existing pattern in enqueueWakeup().
.from(issues) await db.transaction(async (tx) => {
.where(eq(issues.id, id)) await tx.execute(
.then((rows) => rows[0] ?? null); sql`select id from issues where id = ${id} for update`,
if (preCheckRow?.executionRunId) { );
const lockRun = await db const preCheckRow = await tx
.select({ executionRunId: issues.executionRunId })
.from(issues)
.where(eq(issues.id, id))
.then((rows) => rows[0] ?? null);
if (!preCheckRow?.executionRunId) return;
const lockRun = await tx
.select({ id: heartbeatRuns.id, status: heartbeatRuns.status }) .select({ id: heartbeatRuns.id, status: heartbeatRuns.status })
.from(heartbeatRuns) .from(heartbeatRuns)
.where(eq(heartbeatRuns.id, preCheckRow.executionRunId)) .where(eq(heartbeatRuns.id, preCheckRow.executionRunId))
.then((rows) => rows[0] ?? null); .then((rows) => rows[0] ?? null);
if (!lockRun || (lockRun.status !== "queued" && lockRun.status !== "running")) { if (!lockRun || (lockRun.status !== "queued" && lockRun.status !== "running")) {
await db await tx
.update(issues) .update(issues)
.set({ executionRunId: null, executionLockedAt: null, updatedAt: now }) .set({ executionRunId: null, executionAgentNameKey: null, executionLockedAt: null, updatedAt: now })
.where( .where(
and( and(
eq(issues.id, id), eq(issues.id, id),
@ -1409,7 +1417,7 @@ export function issueService(db: Db) {
), ),
); );
} }
} });
const sameRunAssigneeCondition = checkoutRunId const sameRunAssigneeCondition = checkoutRunId
? and( ? and(