Skip to content

fix(swarm): stop treating silent idle agents as heartbeat failures - #4320

Merged
ruvnet merged 1 commit into
ruvnet:mainfrom
Tong-bit-art:fix/idle-agent-heartbeat
Oct 20, 2026
Merged

ruvnet merged 1 commit into
ruvnet:mainfrom
Tong-bit-art:fix/idle-agent-heartbeat

Conversation

@Tong-bit-art

Copy link
Copy Markdown
Contributor

Problem and change

UnifiedSwarmCoordinator.checkHeartbeats() marks every agent error once now - agent.lastHeartbeat > heartbeatIntervalMs * 3 (15s with the default 5s interval). Idle agents have no heartbeat producer and no in-flight work, so after registration they receive no messages and never refresh lastHeartbeat. Healthy idle workers therefore:

  • flip to error after ~15s;
  • disappear from getAvailableAgents() (which only returns status === 'idle'), so generic task dispatch stops using them;
  • remain there until autoRecovery decays health to 0.2 (~75s) and resets them, after which the cycle repeats.

The coordinator only treats terminated as a non-live state. This change skips idle agents that also have no currentTask:

if (agent.status === 'terminated' || (agent.status === 'idle' && !agent.currentTask)) {
  continue;
}

Busy agents keep the existing liveness timeout: an agent that is actually working must still report a heartbeat or completion. The behavior for error/completed/other states is unchanged.

Validation

  • 2 new regressions in v3/@claude-flow/swarm/__tests__/idle-agent-heartbeat.test.ts:
    • a silent idle agent stays idle with health 1 after a simulated timeout;
    • a silent busy agent is still marked error.
  • Against main 19fb5c6e7: 1/2 fail (the idle case becomes error; the busy control passes).
  • Candidate: 2/2 pass; full @claude-flow/swarm suite 346/346, 31/31 files; tsc --noEmit 0 errors; git diff --check clean.

Scope / known limitations

Compatibility

  • Only a private liveness check changes; no method signatures, events, persistence formats, dependencies, or lockfiles.
  • Agents with an active task still use the same 3-interval timeout and auto-recovery path.

UnifiedSwarmCoordinator.checkHeartbeats() marked every agent error after 3 heartbeat intervals when no message arrived. Idle agents have no heartbeat producer and no in-flight work, so healthy workers flapped to error (and back through auto-recovery), and getAvailableAgents() stopped offering them. Skip idle agents with no currentTask; busy agents keep the existing liveness timeout.
@Tong-bit-art
Tong-bit-art requested a review from ruvnet as a code owner October 20, 2026 12:13
@ruvnet

ruvnet commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Verified the mechanism directly rather than taking the write-up at face value: the old `checkHeartbeats()` loop flagged any non-terminated agent as `error` purely on heartbeat silence, with no distinction for agents that have no heartbeat producer to begin with (idle, no current task). The fix's skip condition (`agent.status === 'terminated' || (agent.status === 'idle' && !agent.currentTask)`) is correctly scoped — it only exempts truly-idle agents, and a busy agent that goes silent still hits the existing 3-interval timeout path unchanged (confirmed in the diff: the busy-agent branch is untouched below the new early-`continue`).

The two new tests precisely isolate the two cases (idle+silent stays healthy; busy+silent still flips to `error`), which is exactly what's needed to pin this behavior down. Small, surgical, low blast radius — no public API/signature changes, one private liveness check.

Will merge once CI is green.

@ruvnet
ruvnet merged commit ab40a99 into ruvnet:main Oct 20, 2026
110 checks passed
@ruvnet ruvnet mentioned this pull request Oct 20, 2026
meefs pushed a commit to meefs/claude-code-flow that referenced this pull request Oct 20, 2026
Minor release (new backward-compatible flags: Grok Build CLI host
surface ruvnet#3512, memory list/search tag filter ruvnet#3526) bundled with a
round of correctness fixes merged since 3.58.1: memory delete-sync
(ruvnet#4138), real AgentDB HNSWIndex replacing the dead getController('hnsw')
path with safe exact-search fallback (ruvnet#4249/ruvnet#4322), harness live-process
reporting (ruvnet#3521), RaBitQ on-demand index build (ruvnet#4318/ruvnet#4319),
metaharness ADR-152 cost-dimension and verdict-band fix (ruvnet#3520), swarm
idle-agent heartbeat false-positive (ruvnet#4320), ephemeral-agent termination
idempotency (ruvnet#4323), status/task_list field-mapping and total-count
fixes (ruvnet#4321/ruvnet#4324).

Also fixes the actual cause of broken images/links on
npmjs.com/package/ruflo: `ruflo/package.json`'s `repository.directory:
"ruflo"` field added an extra path layer on top of the README content
(the root README.md, copied in verbatim by `prepublishOnly` at publish
time, which is already written with repo-root-relative paths) — every
relative image/link resolved to e.g. `raw/main/ruflo/ruflo/assets/...`
instead of `raw/main/ruflo/assets/...`. Removed the `directory` field to
match claude-flow's own (directory-less) repository entry. A prior PR
(ruvnet#4325) fixed `ruflo/README.md`'s own relative paths for anyone browsing
that file directly on GitHub, but that file is overwritten by the root
README at publish time and was never the actual npm-rendered content.

Published to npm: @claude-flow/cli@3.59.0, claude-flow@3.59.0,
ruflo@3.59.0, and standalone @claude-flow/memory@3.0.8 (the ruvnet#4249 fix —
not part of the 3-package train, published separately per its own
leaf's npm presence). All three umbrella packages' latest/alpha/v3alpha
dist-tags verified pointing at 3.59.0.

Co-Authored-By: RuFlo <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants