Skip to content

Commit 86ec92b

Browse files
nydamonclaude
andauthored
fix(heartbeat): board user wakeups bypass gate block backoff (paperclipai#221)
Board user comments are explicit instructions that should always reach the agent. The gate_block_backoff was unconditionally skipping wakeups after 3+ consecutive gate blocks, silently dropping board user comments. Now requestedByActorType=user bypasses the backoff and resets the gateBlockCount so the agent gets a fresh slate. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8febc80 commit 86ec92b

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

server/src/__tests__/gate-block-backoff.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,49 @@ describe("gate-block backoff", () => {
249249
expect(updated.status).toBe("in_review");
250250
});
251251

252+
it("board user wakeup bypasses gate block backoff", async () => {
253+
const { agentId, issueId } = await seedIssueWithGateBlocks({ gateBlockCount: 5 });
254+
255+
const heartbeat = heartbeatService(db);
256+
const result = await heartbeat.wakeup(agentId, {
257+
source: "automation",
258+
triggerDetail: "test",
259+
reason: "assignment",
260+
contextSnapshot: { issueId },
261+
requestedByActorType: "user",
262+
requestedByActorId: randomUUID(),
263+
});
264+
265+
// Board user wakeup should NOT be skipped despite high gate block count
266+
expect(result).not.toBeNull();
267+
268+
// Gate block counter should be reset to 0
269+
const [updated] = await db.select().from(issues).where(eq(issues.id, issueId));
270+
expect(updated.gateBlockCount).toBe(0);
271+
});
272+
273+
it("agent wakeup is still skipped by gate block backoff", async () => {
274+
const { agentId, issueId } = await seedIssueWithGateBlocks({ gateBlockCount: 5 });
275+
276+
const heartbeat = heartbeatService(db);
277+
const result = await heartbeat.wakeup(agentId, {
278+
source: "automation",
279+
triggerDetail: "test",
280+
reason: "assignment",
281+
contextSnapshot: { issueId },
282+
requestedByActorType: "agent",
283+
requestedByActorId: randomUUID(),
284+
});
285+
286+
// Agent-originated wakeup should still be skipped
287+
expect(result).toBeNull();
288+
289+
const wakeups = await db.select().from(agentWakeupRequests);
290+
const skipped = wakeups.find((w) => w.reason?.startsWith("gate_block_backoff"));
291+
expect(skipped).toBeTruthy();
292+
expect(skipped!.status).toBe("skipped");
293+
});
294+
252295
it("timer wakes bypass the issue-specific gate check (no issueId)", async () => {
253296
// Timer wakes go through the non-issue pathway in enqueueWakeup.
254297
// Even if the agent has issues with high gateBlockCount, timer wakes

server/src/services/heartbeat.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3613,8 +3613,11 @@ export function heartbeatService(db: Db) {
36133613
// Gate-block backoff: skip dispatch to issues that keep hitting gate blocks.
36143614
// Counter is incremented by the PATCH /issues/:id gate handlers and resets
36153615
// on status or assignee changes.
3616+
// Board user wakeups bypass backoff — a human comment is an explicit instruction
3617+
// that should always reach the agent regardless of prior gate failures.
36163618
const GATE_BLOCK_THRESHOLD = 3;
3617-
if (issue.gateBlockCount >= GATE_BLOCK_THRESHOLD) {
3619+
const isBoardUserWake = opts.requestedByActorType === "user";
3620+
if (issue.gateBlockCount >= GATE_BLOCK_THRESHOLD && !isBoardUserWake) {
36183621
await tx.insert(agentWakeupRequests).values({
36193622
companyId: agent.companyId,
36203623
agentId,
@@ -3631,6 +3634,14 @@ export function heartbeatService(db: Db) {
36313634
return { kind: "skipped" as const };
36323635
}
36333636

3637+
// Board user wakeup resets gate block counter so the agent gets a fresh slate
3638+
if (isBoardUserWake && issue.gateBlockCount > 0) {
3639+
await tx
3640+
.update(issues)
3641+
.set({ gateBlockCount: 0, updatedAt: new Date() })
3642+
.where(eq(issues.id, issueId));
3643+
}
3644+
36343645
let activeExecutionRun = issue.executionRunId
36353646
? await tx
36363647
.select()

0 commit comments

Comments
 (0)