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:422 — isWakeTargetEligible (benched filter) — correct, keep.
ai/daemons/orchestrator/services/WakeDecisionService.mjs — decideWake = "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
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).
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'sevaluateSubscription:if (result.type === 'heartbeat_pulse' && isPromptSubmittingSubscription(sub)) return null;isPromptSubmittingSubscriptionis true for theosascript/tmuxadapters — andosascriptis the darwin default whenadapteris 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 iscodex-app-server(@neo-gpt) — which is why the regression was invisible to its author.V-B-A (this session)
GraphLogheartbeat_pulserows land per-cycle for the active swarm (SwarmHeartbeatService.pulseStep 6).WAKE_SUBSCRIPTIONadapter audit: everyosascript/null sub → dropped; onlycodex-app-server→ delivered.SENT_TO_ME) is notheartbeat_pulse, so it bypasses the line → message-wakes still work (matches the symptom).The Architectural Reality
ai/daemons/wake/daemon.mjs:431— the suppression. HelpersgetSubscriptionAdapter(:471) +isPromptSubmittingSubscription(:481) are used only by this line.ai/daemons/wake/daemon.mjs:422—isWakeTargetEligible(benched filter) — correct, keep.ai/daemons/orchestrator/services/WakeDecisionService.mjs—decideWake= "Wake = active AND idle AND ready";idle= no A2A activity for 15m. So aheartbeat_pulseis 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
daemon.mjs:431+ the two now-dead helpers (getSubscriptionAdapter,isPromptSubmittingSubscription). KeepisWakeTargetEligible.#13456daemon spec tests (daemon.spec.mjs:1873tmux,:2192Codex 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
heartbeat_pulsedeliveryWakeDecisionService.decideWakeemit-side idle-gateheartbeat_pulsedelivers through ALL adapters (osascript/tmux/codex), gated upstream to idle agents onlyisWakeTargetEligible/ identity rootsDecision 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.mjsno longer suppressesheartbeat_pulseforosascript/tmux; benched filter retained.#13456daemon spec tests assert delivery of a pure-heartbeat to interactive adapters.Out of Scope
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
decideWake).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 anchord4330c412(#13457).