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():
- Concurrency-lock check (skip vs clear)
- TTL sweep via
MailboxService.sweepExpiredTasks()
- Sunset detection via
checkSunsetted.mjs subprocess
- All-agent-idle detection via
checkAllAgentIdle.mjs subprocess
- Heartbeat-bypass check (push-capable identities)
- Token-economy gate (mailbox unread + open issues)
- 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
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
Context
M4 per-task coordinator landscape per
learn/agentos/v13-path.md:193: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.mjswrapper). 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:
node ai/scripts/swarm-heartbeat-daemon.mjsin a separate terminal/launchdFolding into orchestrator unifies persistent-process management.
Architectural Reality
Pulse() logic currently in
SwarmHeartbeatService.pulse():MailboxService.sweepExpiredTasks()checkSunsetted.mjssubprocesscheckAllAgentIdle.mjssubprocessFolding 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
runIfDuecallback (cheap, in-process) OR via processSupervisorService spawn (matches current sub-daemon shape, but unnecessary heavy)?/ticket-intakevalidates against post-#11062/#11065 wire-in patterns.Acceptance Criteria
/ticket-intakevalidates premise + open question (in-process vs spawn)ai/daemons/services/HeartbeatCoordinatorService.mjsper #11062 sibling shapeai/scripts/swarm-heartbeat-daemon.mjsdeleted (entry-point wrapper obsoleted by fold)learn/agentos/wake-substrate/com.neomjs.swarm-heartbeat.plist.templatedeleted (anti-pattern per operator #11066/#11067 retraction)learn/agentos/wake-substrate/PersistentProcessManagement.mdscope-trimmed or retired (no longer documents SwarmHeartbeatService as standalone daemon)cadenceEngine.runIfDuepull-request §6.1Dependencies
Out of Scope
checkSunsetted.mjsetc. — covered by #10795 (separate sibling work)Related
ai/daemons/SwarmHeartbeatService.mjs,ai/scripts/swarm-heartbeat-daemon.mjslearn/agentos/v13-path.md:193Self-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