Skip to content

Commit 3ae9680

Browse files
author
Test
committed
fix: separate execution locks from heartbeat monitors
1 parent b4baa9e commit 3ae9680

3 files changed

Lines changed: 52 additions & 17 deletions

File tree

server/src/__tests__/execution-lock-acquisition-helper.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import { describe, expect, it } from "vitest";
22
import { readFile } from "node:fs/promises";
33
import path from "node:path";
4+
import { executionLockAcquisitionFields } from "../services/issues.js";
45

56
describe("execution lock acquisition helper usage", () => {
7+
it("does not create a scheduled review monitor when acquiring an execution lock", () => {
8+
const now = new Date("2026-08-08T03:15:00.000Z");
9+
10+
expect(executionLockAcquisitionFields("run-1", now)).toEqual({
11+
executionRunId: "run-1",
12+
executionLockedAt: now,
13+
});
14+
});
15+
616
it("keeps production execution lock acquisition writes centralized", async () => {
717
const root = process.cwd();
818
const productionFiles = [

server/src/services/heartbeat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13617,7 +13617,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
1361713617
})
1361813618
) {
1361913619
try {
13620-
await issuesSvc.checkout(issueId, agent.id, ["todo", "backlog", "blocked"], run.id);
13620+
await issuesSvc.checkout(issueId, agent.id, ["todo", "backlog", "blocked"], run.id, { scheduleMonitor: false });
1362113621
context[PAPERCLIP_HARNESS_CHECKOUT_KEY] = true;
1362213622
} catch (error) {
1362313623
if (!isCheckoutConflictError(error)) throw error;

server/src/services/issues.ts

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -780,19 +780,28 @@ function issueExecutionLockMonitorNextCheckAt(now: Date) {
780780
return new Date(now.getTime() + ISSUE_EXECUTION_LOCK_TTL_MS);
781781
}
782782

783-
export function executionLockAcquisitionFields(runId: string, now: Date) {
784-
const nextCheckAt = issueExecutionLockMonitorNextCheckAt(now);
783+
export function executionLockAcquisitionFields(
784+
runId: string,
785+
now: Date,
786+
options: { scheduleMonitor?: boolean } = {},
787+
) {
788+
const monitorFields = options.scheduleMonitor
789+
? {
790+
monitorNextCheckAt: sql<Date | null>`case
791+
when ${issues.monitorNextCheckAt} is null
792+
and ${issues.monitorLastTriggeredAt} is null
793+
and ${issues.monitorAttemptCount} = 0
794+
then ${issueExecutionLockMonitorNextCheckAt(now).toISOString()}::timestamptz
795+
else ${issues.monitorNextCheckAt}
796+
end`,
797+
monitorWakeRequestedAt: null,
798+
}
799+
: {};
800+
785801
return {
786802
executionRunId: runId,
787803
executionLockedAt: now,
788-
monitorNextCheckAt: sql<Date | null>`case
789-
when ${issues.monitorNextCheckAt} is null
790-
and ${issues.monitorLastTriggeredAt} is null
791-
and ${issues.monitorAttemptCount} = 0
792-
then ${nextCheckAt.toISOString()}::timestamptz
793-
else ${issues.monitorNextCheckAt}
794-
end`,
795-
monitorWakeRequestedAt: null,
804+
...monitorFields,
796805
};
797806
}
798807

@@ -5102,6 +5111,7 @@ export function issueService(db: Db) {
51025111
actorAgentId: string;
51035112
actorRunId: string;
51045113
expectedCheckoutRunId: string;
5114+
scheduleMonitor: boolean;
51055115
}) {
51065116
return db.transaction(async (tx) => {
51075117
const lockedIssue = await tx
@@ -5159,7 +5169,7 @@ export function issueService(db: Db) {
51595169
.update(issues)
51605170
.set({
51615171
checkoutRunId: input.actorRunId,
5162-
...executionLockAcquisitionFields(input.actorRunId, now),
5172+
...executionLockAcquisitionFields(input.actorRunId, now, { scheduleMonitor: input.scheduleMonitor }),
51635173
updatedAt: now,
51645174
})
51655175
.where(
@@ -5201,6 +5211,7 @@ export function issueService(db: Db) {
52015211
issueId: string;
52025212
actorAgentId: string;
52035213
actorRunId: string;
5214+
scheduleMonitor: boolean;
52045215
}) {
52055216
return db.transaction(async (tx) => {
52065217
await tx.execute(
@@ -5218,7 +5229,7 @@ export function issueService(db: Db) {
52185229
.update(issues)
52195230
.set({
52205231
checkoutRunId: input.actorRunId,
5221-
...executionLockAcquisitionFields(input.actorRunId, now),
5232+
...executionLockAcquisitionFields(input.actorRunId, now, { scheduleMonitor: input.scheduleMonitor }),
52225233
updatedAt: now,
52235234
})
52245235
.where(
@@ -7976,7 +7987,13 @@ export function issueService(db: Db) {
79767987
return enriched;
79777988
}),
79787989

7979-
checkout: async (id: string, agentId: string, expectedStatuses: string[], checkoutRunId: string | null) => {
7990+
checkout: async (
7991+
id: string,
7992+
agentId: string,
7993+
expectedStatuses: string[],
7994+
checkoutRunId: string | null,
7995+
options: { scheduleMonitor?: boolean } = {},
7996+
) => {
79807997
const issueCompany = await db
79817998
.select({ companyId: issues.companyId })
79827999
.from(issues)
@@ -7986,6 +8003,7 @@ export function issueService(db: Db) {
79868003
await assertAssignableAgent(db, issueCompany.companyId, agentId, { kind: "work" });
79878004

79888005
const now = new Date();
8006+
const scheduleMonitor = options.scheduleMonitor ?? true;
79898007
const activePauseHold = await treeControlSvc.getActivePauseHoldGate(issueCompany.companyId, id);
79908008
if (
79918009
activePauseHold &&
@@ -8034,7 +8052,9 @@ export function issueService(db: Db) {
80348052
assigneeAgentId: agentId,
80358053
assigneeUserId: null,
80368054
checkoutRunId,
8037-
...(checkoutRunId ? executionLockAcquisitionFields(checkoutRunId, now) : { executionRunId: null }),
8055+
...(checkoutRunId
8056+
? executionLockAcquisitionFields(checkoutRunId, now, { scheduleMonitor })
8057+
: { executionRunId: null }),
80388058
status: "in_progress",
80398059
startedAt: now,
80408060
updatedAt: now,
@@ -8080,7 +8100,9 @@ export function issueService(db: Db) {
80808100
.update(issues)
80818101
.set({
80828102
checkoutRunId,
8083-
...(checkoutRunId ? executionLockAcquisitionFields(checkoutRunId, now) : { executionRunId: null }),
8103+
...(checkoutRunId
8104+
? executionLockAcquisitionFields(checkoutRunId, now, { scheduleMonitor })
8105+
: { executionRunId: null }),
80848106
updatedAt: new Date(),
80858107
})
80868108
.where(
@@ -8109,6 +8131,7 @@ export function issueService(db: Db) {
81098131
actorAgentId: agentId,
81108132
actorRunId: checkoutRunId,
81118133
expectedCheckoutRunId: current.checkoutRunId,
8134+
scheduleMonitor,
81128135
});
81138136
if (staleAdoption.adopted) {
81148137
const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0] ?? null);
@@ -8133,7 +8156,7 @@ export function issueService(db: Db) {
81338156
const adoptionSet: Record<string, unknown> = {
81348157
assigneeAgentId: agentId,
81358158
checkoutRunId,
8136-
...executionLockAcquisitionFields(checkoutRunId, now),
8159+
...executionLockAcquisitionFields(checkoutRunId, now, { scheduleMonitor }),
81378160
executionAgentNameKey: null,
81388161
status: "in_progress",
81398162
updatedAt: now,
@@ -8248,6 +8271,7 @@ export function issueService(db: Db) {
82488271
issueId: id,
82498272
actorAgentId,
82508273
actorRunId: actorRunId!,
8274+
scheduleMonitor: true,
82518275
});
82528276

82538277
if (adopted) {
@@ -8274,6 +8298,7 @@ export function issueService(db: Db) {
82748298
actorAgentId,
82758299
actorRunId,
82768300
expectedCheckoutRunId: previousCheckoutRunId,
8301+
scheduleMonitor: true,
82778302
});
82788303

82798304
if (staleAdoption.adopted) {

0 commit comments

Comments
 (0)