From 4b9f8165c5ce04ee697ccbc3d0e8e8e71e57a27d Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 1 Jul 2026 19:12:59 +0000 Subject: [PATCH] Require actionable work for timer heartbeats --- ...heartbeat-stale-queue-invalidation.test.ts | 20 +++++++++++++++++-- server/src/services/heartbeat.ts | 2 +- ui/src/lib/new-agent-runtime-config.test.ts | 2 ++ ui/src/lib/new-agent-runtime-config.ts | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts b/server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts index 9c0f2732e68..c613439b7bf 100644 --- a/server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts +++ b/server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts @@ -393,7 +393,7 @@ describeEmbeddedPostgres("heartbeat stale queued-run invalidation", () => { expect(countExecuteCallsForRun(run!.id)).toBe(1); }); - it("runs generic timer wakes by default for proactive agents without assigned issue work", async () => { + it("skips generic timer wakes by default when there is no assigned issue work", async () => { const { agentId } = await seedCompanyAndAgent({ heartbeatConfig: { enabled: true, @@ -405,9 +405,25 @@ describeEmbeddedPostgres("heartbeat stale queued-run invalidation", () => { triggerDetail: "schedule", }); + expect(run).toBeNull(); + expect(mockAdapterExecute).not.toHaveBeenCalled(); + }); + + it("allows explicit proactive generic timer wakes without assigned issue work", async () => { + const { agentId } = await seedCompanyAndAgent({ + heartbeatConfig: { + enabled: true, + skipTimerWhenNoActionableWork: false, + }, + }); + + const run = await heartbeat.wakeup(agentId, { + source: "timer", + triggerDetail: "schedule", + }); + expect(run).not.toBeNull(); await waitForCondition(async () => countExecuteCallsForRun(run!.id) > 0); - expect(countExecuteCallsForRun(run!.id)).toBe(1); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 5c6a2b17d15..a834cca2c19 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -7996,7 +7996,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) heartbeat.skipTimerWhenNoActionableWork ?? heartbeat.requireActionableTimerWork ?? heartbeat.issueOnlyTimer, - false, + true, ), maxDailyRuns: normalizeOptionalNonNegativeInteger( heartbeat.maxDailyRuns ?? heartbeat.dailyRunLimit ?? heartbeat.dailyRunCap ?? heartbeat.maxRunsPerDay, diff --git a/ui/src/lib/new-agent-runtime-config.test.ts b/ui/src/lib/new-agent-runtime-config.test.ts index 3a72943c3f2..bbac8c6eb27 100644 --- a/ui/src/lib/new-agent-runtime-config.test.ts +++ b/ui/src/lib/new-agent-runtime-config.test.ts @@ -10,6 +10,7 @@ describe("buildNewAgentRuntimeConfig", () => { enabled: false, intervalSec: 300, wakeOnDemand: true, + skipTimerWhenNoActionableWork: true, cooldownSec: 10, maxConcurrentRuns: AGENT_DEFAULT_MAX_CONCURRENT_RUNS, }, @@ -27,6 +28,7 @@ describe("buildNewAgentRuntimeConfig", () => { enabled: true, intervalSec: 3600, wakeOnDemand: true, + skipTimerWhenNoActionableWork: true, cooldownSec: 10, maxConcurrentRuns: AGENT_DEFAULT_MAX_CONCURRENT_RUNS, }, diff --git a/ui/src/lib/new-agent-runtime-config.ts b/ui/src/lib/new-agent-runtime-config.ts index 0e106b984d8..206a7797f86 100644 --- a/ui/src/lib/new-agent-runtime-config.ts +++ b/ui/src/lib/new-agent-runtime-config.ts @@ -12,6 +12,7 @@ export function buildNewAgentRuntimeConfig(input?: { enabled: input?.heartbeatEnabled ?? defaultCreateValues.heartbeatEnabled, intervalSec: input?.intervalSec ?? defaultCreateValues.intervalSec, wakeOnDemand: true, + skipTimerWhenNoActionableWork: true, cooldownSec: 10, maxConcurrentRuns: AGENT_DEFAULT_MAX_CONCURRENT_RUNS, },