Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions server/src/services/heartbeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@ export function heartbeatService(db: Db) {
intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
skipIfNoAssignments: asBoolean(heartbeat.skipIfNoAssignments, false),
};
}

Expand Down Expand Up @@ -1668,6 +1669,29 @@ export function heartbeatService(db: Db) {
return null;
}

if (source === "timer" && policy.skipIfNoAssignments) {
const assignedCount = await db
.select({ count: sql<number>`count(*)` })
.from(issues)
.where(
and(
eq(issues.companyId, agent.companyId),
eq(issues.assigneeAgentId, agentId),
inArray(issues.status, ["todo", "in_progress", "blocked"]),
),
)
.then(([row]) => Number(row?.count ?? 0));
if (assignedCount === 0) {
await writeSkippedRequest("heartbeat.skipIfNoAssignments");
// Reset the interval baseline so tickTimers doesn't fire again immediately.
await db
.update(agents)
.set({ lastHeartbeatAt: new Date() })
.where(eq(agents.id, agentId));
return null;
}
Comment on lines +1684 to +1692

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip does not reset lastHeartbeatAt, causing per-tick flooding

writeSkippedRequest writes to agentWakeupRequests and returns null, but it never updates agent.lastHeartbeatAt. The interval timer in tickTimers (line 2234-2236) uses lastHeartbeatAt as its baseline:

const baseline = new Date(agent.lastHeartbeatAt ?? agent.createdAt).getTime();
const elapsedMs = now.getTime() - baseline;
if (elapsedMs < policy.intervalSec * 1000) continue;

Once elapsedMs >= intervalSec * 1000, every single tickTimers invocation will call enqueueWakeup for that agent, because lastHeartbeatAt is only ever updated inside finalizeAgentStatus (called on run completion). With skipIfNoAssignments: true and no assignments, no run ever starts, so lastHeartbeatAt is never refreshed.

Result: instead of one skipped row per interval, you get one skipped row per tickTimers tick — potentially dozens per minute — the exact "wakeup log noise" the PR is trying to eliminate.

The fix is to update lastHeartbeatAt on a skip (or update the tickTimers baseline) so the interval correctly resets:

if (assignedCount === 0) {
  await writeSkippedRequest("heartbeat.skipIfNoAssignments");
  // Reset the heartbeat baseline so the interval is respected
  await db
    .update(agents)
    .set({ lastHeartbeatAt: new Date(), updatedAt: new Date() })
    .where(eq(agents.id, agentId));
  return null;
}

}

const bypassIssueExecutionLock =
reason === "issue_comment_mentioned" ||
readNonEmptyString(enrichedContextSnapshot.wakeReason) === "issue_comment_mentioned";
Expand Down
Loading