LearnNewsExamplesServices
Frontmatter
id11073
titleExtract HeartbeatCoordinatorService as M4 per-task coordinator (fold SwarmHeartbeatService into orchestrator)
stateClosed
labels
enhancementaiarchitecturemodel-experience
assignees[]
createdAtMay 10, 2026, 12:54 AM
updatedAtMay 12, 2026, 4:08 AM
githubUrlhttps://github.com/neomjs/neo/issues/11073
authorneo-opus-4-7
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 10, 2026, 5:45 PM

Extract HeartbeatCoordinatorService as M4 per-task coordinator (fold SwarmHeartbeatService into orchestrator)

Closedenhancementaiarchitecturemodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 10, 2026, 12:54 AM

Context

M4 per-task coordinator landscape per learn/agentos/v13-path.md:193:

"Same shape for SwarmHeartbeatService"

Operator confirmed 2026-05-10: "of course we want heartbeat and bridge daemon inside our new orchestrator!" Fold direction is in-scope, not future.

Currently SwarmHeartbeatService runs as a separate Neo-singleton-as-self-invoke-entry-point hybrid (post-#11058 split: class-only file + ai/scripts/swarm-heartbeat-daemon.mjs wrapper). Operator goal: ONE daemon process under orchestrator, not N per-daemon launchd plists. The M4 fold eliminates the wrapper + plist anti-pattern.

The Problem

SwarmHeartbeatService's current architecture as a standalone daemon adds operator burden:

  • Operator runs node ai/scripts/swarm-heartbeat-daemon.mjs in a separate terminal/launchd
  • Cross-platform packaging anti-pattern (the plist work was closed-not-planned per #11066/#11067 retraction)
  • Substrate state (heartbeat-concurrency.lock, etc.) duplicates orchestrator's PID-file substrate

Folding into orchestrator unifies persistent-process management.

Architectural Reality

Pulse() logic currently in SwarmHeartbeatService.pulse():

  1. Concurrency-lock check (skip vs clear)
  2. TTL sweep via MailboxService.sweepExpiredTasks()
  3. Sunset detection via checkSunsetted.mjs subprocess
  4. All-agent-idle detection via checkAllAgentIdle.mjs subprocess
  5. Heartbeat-bypass check (push-capable identities)
  6. Token-economy gate (mailbox unread + open issues)
  7. Tmux-inject pulse prompt

Folding shape: HeartbeatCoordinatorService.getDueTask() returns trigger envelope; orchestrator's cadenceEngine.runIfDue('heartbeat', ...) invokes pulse work IN-PROCESS (not subprocess — heartbeat is light); per-pulse subprocess invocations (#10795 dual-mode-CLI tickets) become direct module imports where feasible.

Open question for pickup: does heartbeat pulse fold to runIfDue callback (cheap, in-process) OR via processSupervisorService spawn (matches current sub-daemon shape, but unnecessary heavy)? /ticket-intake validates against post-#11062/#11065 wire-in patterns.

Acceptance Criteria

  • /ticket-intake validates premise + open question (in-process vs spawn)
  • ai/daemons/services/HeartbeatCoordinatorService.mjs per #11062 sibling shape
  • SwarmHeartbeatService.mjs class file becomes a CONSUMED service (no self-invoke; no separate entry-point script)
  • ai/scripts/swarm-heartbeat-daemon.mjs deleted (entry-point wrapper obsoleted by fold)
  • learn/agentos/wake-substrate/com.neomjs.swarm-heartbeat.plist.template deleted (anti-pattern per operator #11066/#11067 retraction)
  • learn/agentos/wake-substrate/PersistentProcessManagement.md scope-trimmed or retired (no longer documents SwarmHeartbeatService as standalone daemon)
  • Wire-in via cadenceEngine.runIfDue
  • Cross-family review per pull-request §6.1

Dependencies

  • Hard: Blocked by #11062 + #11065 merge for wire-in pattern
  • Coordination: plist + docs cleanup ACs ride along with this PR (avoid orphan cleanup PR)

Out of Scope

  • Detailed prescription pre-staged here
  • Subprocess-to-direct-import migration of checkSunsetted.mjs etc. — covered by #10795 (separate sibling work)
  • Operator's launchd installations (operator-territory; they bootout swarm-heartbeat plist, restart orchestrator)

Related

  • Parent landscape: #11022 → M4
  • Siblings: #11062, #11065, +3 more in batch (this is one of them)
  • Bridge fold sibling: WakeSubstrateCoordinatorService (filed in same batch)
  • Source: ai/daemons/SwarmHeartbeatService.mjs, ai/scripts/swarm-heartbeat-daemon.mjs
  • Closed-as-anti-pattern: PR #11067 (per-daemon plist packaging); operator correction 2026-05-09
  • Subprocess-elimination companion: #10795
  • Architectural anchor: learn/agentos/v13-path.md:193

Self-Identification: @neo-opus-4-7 (Claude Opus 4.7, Claude Code) — landscape-visibility filing 2026-05-10 per operator directive.

Origin Session ID: c2912891-b459-4a03-b2af-154d5e264df1