LearnNewsExamplesServices
Frontmatter
id13565
titleHeartbeat wakes dropped for all interactive (osascript/tmux) agents
stateClosed
labels
bugairegressionarchitecturemodel-experience
assigneesneo-opus-ada
createdAtJun 19, 2026, 7:32 PM
updatedAtJun 20, 2026, 1:49 AM
githubUrlhttps://github.com/neomjs/neo/issues/13565
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 20, 2026, 1:49 AM

Heartbeat wakes dropped for all interactive (osascript/tmux) agents

Closed v13.1.0/archive-v13-1-0-chunk-4 bugairegressionarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on Jun 19, 2026, 7:32 PM

Context

Operator: "what is the progress on heartbeat wake-ups? we need them back." No heartbeat wake has reached any local interactive agent for ~17h. A2A (message) wakes still work. Diagnosed during the who_is_online work this session.

Release classification: post-release (agent-coordination regression; not a v13 gate). Boardless.

The Problem

PR #13457 (fix(ai): suppress benched heartbeat wake submits, merged 2026-06-18 ~21:53) added to the wake daemon's evaluateSubscription:

if (result.type === 'heartbeat_pulse' && isPromptSubmittingSubscription(sub)) return null;

isPromptSubmittingSubscription is true for the osascript/tmux adapters — and osascript is the darwin default when adapter is unset. So this drops heartbeat-pulse delivery for every interactive harness: all local Claude instances (Ada/Grace/Vega) and the Antigravity/tmux routes. The only adapter that still receives heartbeats is codex-app-server (@neo-gpt) — which is why the regression was invisible to its author.

V-B-A (this session)

  • Emitter alive: GraphLog heartbeat_pulse rows land per-cycle for the active swarm (SwarmHeartbeatService.pulse Step 6).
  • WAKE_SUBSCRIPTION adapter audit: every osascript/null sub → dropped; only codex-app-server → delivered.
  • A2A (SENT_TO_ME) is not heartbeat_pulse, so it bypasses the line → message-wakes still work (matches the symptom).

The Architectural Reality

  • ai/daemons/wake/daemon.mjs:431 — the suppression. Helpers getSubscriptionAdapter (:471) + isPromptSubmittingSubscription (:481) are used only by this line.
  • ai/daemons/wake/daemon.mjs:422isWakeTargetEligible (benched filter) — correct, keep.
  • ai/daemons/orchestrator/services/WakeDecisionService.mjsdecideWake = "Wake = active AND idle AND ready"; idle = no A2A activity for 15m. So a heartbeat_pulse is emitted only for an idle agent. The delivery-side suppression is therefore redundant: the "don't submit into a live composer" concern (#13456) is already handled at emit — a mid-turn (non-idle) agent never gets a pulse.

The Fix

  • Remove daemon.mjs:431 + the two now-dead helpers (getSubscriptionAdapter, isPromptSubmittingSubscription). Keep isWakeTargetEligible.
  • Flip the two #13456 daemon spec tests (daemon.spec.mjs:1873 tmux, :2192 Codex UI) from "does not queue pure-heartbeat interactive submit" to "delivers pure-heartbeat interactive submit" — the daemon trusts the emit-side idle-gate.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
wake-daemon heartbeat_pulse delivery WakeDecisionService.decideWake emit-side idle-gate heartbeat_pulse delivers through ALL adapters (osascript/tmux/codex), gated upstream to idle agents only prior osascript/tmux blanket drop (the regression) daemon JSDoc flipped daemon spec tests (delivery asserted)
benched-agent heartbeat filter isWakeTargetEligible / identity roots unchanged — non-active identities still filtered before delivery n/a daemon JSDoc existing benched test stays green

Decision Record impact

none (no ADR). Challenges #13456 AC3 ("pure-heartbeat does not submit through osascript/tmux") — reconciled: AC3's intent (don't interrupt a live composer) is satisfied by the emit-side idle-gate, so the delivery-side blanket suppression is removed. @neo-gpt (#13456/#13457 author) reviews the reversal cross-family.

Acceptance Criteria

  • daemon.mjs no longer suppresses heartbeat_pulse for osascript/tmux; benched filter retained.
  • Dead helpers removed (no lint/unused-symbol residue).
  • The two #13456 daemon spec tests assert delivery of a pure-heartbeat to interactive adapters.
  • Existing benched-suppression + non-heartbeat tests stay green.
  • Post-merge (L4): after wake/orchestrator daemon restart, local Claude instances receive heartbeat wakes again.

Out of Scope

  • Re-enabling the wake safety gate (disabled 2026-06-05) that separately skips idle-out-nudge / fresh-session-spawn / all-agent-idle dispatch — the Step-6 heartbeat-pulse emit is NOT gate-blocked, so this fix alone restores periodic heartbeats.
  • Sharpening decideWake's idle signal for the rare ">15m A2A-silent long turn looks idle" edge — better addressed at emit than by re-suppressing delivery.

Avoided Traps

  • Re-adding a delivery-layer presence/pid check to decide "is it safe to submit" — that's the harness-presence coupling the operator rejected ("local neo activity" is not mergeable); idle-gating belongs at emit (decideWake).
  • Reverting #13457 wholesale — its benched-filter (isWakeTargetEligible / isHeartbeatTargetEligible) is correct and stays; only the prompt-submitting suppression is removed.

Related

Origin Session ID: abe80be3-6235-4a9e-99bc-b14659ba806a

Handoff Retrieval Hints: query_raw_memories("heartbeat wake osascript tmux suppression isPromptSubmittingSubscription decideWake idle gate"); commit anchor d4330c412 (#13457).