Context
Heartbeat has binary either/or logic: if ANY agent has explicit heartbeat config, ONLY those get heartbeat; otherwise ONLY the default agent gets heartbeat. Cron's resolveCronAgent() silently falls back when specified agent not in config — a typo in agentId: "helpr" runs as default agent.
Depends on #1575 (sole-agent auto-selection).
What
Heartbeat
- Rewrite
isHeartbeatEnabledForAgent() — agents.defaults.heartbeat applies to ALL agents as baseline
- Per-agent
heartbeat config overrides the defaults
- No agents → skip heartbeat entirely
Cron
resolveCronAgent() must throw/warn when specified agent not in config
assertMainSessionAgentId() uses sole-agent logic instead of default-agent
- Session reaper uses explicit agent from job, not default fallback
Acceptance Criteria
Files
src/infra/heartbeat-runner.ts (6 call sites)
src/gateway/server-cron.ts — resolveCronAgent at line 165, plus standalone call at line 223 (2 production call sites total)
src/cron/service/jobs.ts (assertMainSessionAgentId)
src/cron/service/timer.ts (session reaper)
src/cron/isolated-agent/run.ts (three-level cascade)
Note on heartbeat-runner.ts
src/web/auto-reply/heartbeat-runner.ts does NOT use resolveDefaultAgentId in production code (test mock only at src/web/auto-reply/heartbeat-runner.test.ts:53).
Context
Heartbeat has binary either/or logic: if ANY agent has explicit heartbeat config, ONLY those get heartbeat; otherwise ONLY the default agent gets heartbeat. Cron's
resolveCronAgent()silently falls back when specified agent not in config — a typo inagentId: "helpr"runs as default agent.Depends on #1575 (sole-agent auto-selection).
What
Heartbeat
isHeartbeatEnabledForAgent()—agents.defaults.heartbeatapplies to ALL agents as baselineheartbeatconfig overrides the defaultsCron
resolveCronAgent()must throw/warn when specified agent not in configassertMainSessionAgentId()uses sole-agent logic instead of default-agentAcceptance Criteria
agents.defaults.heartbeatconfigured → ALL agents get heartbeat (not just default)agentId→ error/warning (not silent fallback)assertMainSessionAgentId()validates against sole agentFiles
src/infra/heartbeat-runner.ts(6 call sites)src/gateway/server-cron.ts—resolveCronAgentat line 165, plus standalone call at line 223 (2 production call sites total)src/cron/service/jobs.ts(assertMainSessionAgentId)src/cron/service/timer.ts(session reaper)src/cron/isolated-agent/run.ts(three-level cascade)Note on heartbeat-runner.ts
src/web/auto-reply/heartbeat-runner.tsdoes NOT useresolveDefaultAgentIdin production code (test mock only atsrc/web/auto-reply/heartbeat-runner.test.ts:53).