Context
Sub-iii of Epic #11993 (Wake substrate evolution: 3-signal model + Shape B heartbeat pulse). Graduated from Discussion #11992 (cycle-3 RESOLVED). See Epic body for full architectural context and Signal Ledger.
Origin Session ID: 8f1a91ee-3ee4-4e4b-9865-b5810f6be353
The Problem
After Sub-i lands WakeSubscriptionService.emitHeartbeatPulse (#11994) and Sub-ii lands the 3-signal derivation + backoff + readiness parser (#11995), two legacy wake-delivery paths remain in the codebase:
SwarmHeartbeatService.tmuxInjectPulsePrompt() (line 657) — tmux send-keys-based pulse injection. Dead path for non-tmux harnesses (Codex Desktop is native macOS, no tmux). Pre-bridge-daemon-era leftover.
idleOutNudge.mjs — currently calls MailboxService.addMessage to wake recipients (Shape A path). Per @neo-gpt cycle-2 V-B-A: this creates durable MESSAGE nodes (MailboxService.mjs:337,548) + transcript-injected wake-blocks (bridge-daemon:522-538). Unacceptable Codex Desktop mailbox UX (~96 durable msgs/day/identity).
Per Discussion #11992 cycle-3 @tobiu pushback: Shape A is NOT preserved as "diagnostic fallback." Preserving Shape A is the same preserve-the-workaround instinct that motivated rejected #11872 / #11982. If Shape B is substrate-correct, Shape A is just legacy that gets removed.
The Fix
- Delete
SwarmHeartbeatService.tmuxInjectPulsePrompt() method entirely from ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs. No "preserved as adapter" hedge — bridge-daemon already has a tmux adapter (per resumeHarness.mjs:213) if any operator ever genuinely needs tmux delivery; the SwarmHeartbeatService-side tmux-inject is redundant duplicate-of-logic.
- Replace
SwarmHeartbeatService.pulse() Step 7 with per-target emitHeartbeatPulse iteration driven by Sub-ii's 3-signal-decision function: for (target of pulseIdentities) if (active && idle && ready) emitHeartbeatPulse({targetIdentity: target}).
- Refactor
ai/scripts/lifecycle/idleOutNudge.mjs to call WakeSubscriptionService.emitHeartbeatPulse({targetIdentity}) instead of MailboxService.addMessage({to: targetIdentity, ...}). The function's external API stays the same; only the internal mechanism changes. No MESSAGE nodes created.
- Update wake-substrate runbook (
learn/agentos/wake-substrate/PersistentProcessManagement.md + NightShiftLeasedDriver.md) to document the 3-signal model + Shape B canonical wake-pulse path. Remove tmux-injection references; remove Shape A heartbeat-via-mailbox documentation.
Architectural Reality
- Depends on Sub-i (#11994) for
emitHeartbeatPulse to exist.
- Depends on Sub-ii (#11995) for the 3-signal-decision function consumed by
SwarmHeartbeatService.pulse() Step 7 replacement.
- Bridge-daemon adapter set (osascript / codex-app-server / antigravity-cli / claude-cli / tmux) remains intact in
ai/daemons/bridge/daemon.mjs — only the SwarmHeartbeatService-side tmux-inject is removed.
idleOutNudge.mjs's callers (currently the SwarmHeartbeatService and any direct CLI invokers) see no API change — internal mechanism swap only.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
SwarmHeartbeatService.tmuxInjectPulsePrompt() (removed) |
Epic #11993 |
Method deleted entirely; bridge-daemon owns wake delivery (single source of truth) |
None — dead-path leftover from pre-bridge-daemon era |
Removal commit note |
grep -rn "tmuxInjectPulsePrompt" ai/ test/ returns zero matches post-merge |
SwarmHeartbeatService.pulse() Step 7 |
Epic #11993 + Sub-ii (#11995) |
Replaces tmux-inject with emitHeartbeatPulse per target identity; iterates pulse targets, fires emit per target where Sub-ii's 3-signal-decision returns true |
None — tmux-inject removed entirely |
JSDoc on pulse() flow + ADR 0002 update from Sub-i |
Unit test verifies no tmux call sites remain; pulse decision matches 3-signal truth table |
idleOutNudge.mjs heartbeat path |
Epic #11993 |
Refactored to call emitHeartbeatPulse instead of MailboxService.addMessage |
None — Shape A heartbeat-mailbox path removed entirely |
JSDoc on refactored function |
Unit test verifies idleOutNudge no longer creates MESSAGE nodes |
| Wake-substrate runbook |
Epic #11993 + AC7 |
Update PersistentProcessManagement.md + NightShiftLeasedDriver.md to document 3-signal model + Shape B canonical wake-pulse path |
None — required doc update |
Doc diff in this sub's PR |
Grep tmux-injection references in learn/agentos/wake-substrate/ returns zero post-merge |
Acceptance Criteria
Out of Scope
WakeSubscriptionService.emitHeartbeatPulse mechanism — Sub-i of Epic #11993 (#11994)
- 3-signal derivation logic — Sub-ii of Epic #11993 (#11995)
- Bridge-daemon adapter behavior changes — Sub-i / out of scope here
- Bridge-daemon Codex adapter debugging (OQ6) — sibling concern, separate ticket post-client-trial trace
Avoided Traps
- ❌ Preserving
SwarmHeartbeatService.tmuxInjectPulsePrompt as "tmux adapter" — bridge-daemon already has tmux adapter; SwarmHeartbeatService-side duplicate is the legacy leftover being removed
- ❌ Keeping Shape A in
idleOutNudge.mjs as "diagnostic fallback" — explicitly rejected by @tobiu cycle-3 Discussion #11992 pushback
- ❌ Stub the methods rather than delete — sets a bad precedent for future maintainers (zombie methods); delete entirely
- ❌ Leave
PersistentProcessManagement.md outdated — doc drift compounds; the Epic AC7 + AC6 demand runbook update
Related
- Parent Epic: #11993
- Graduation Discussion: #11992
- Sub-i (provides
emitHeartbeatPulse consumed by this sub): #11994
- Sub-ii (provides 3-signal-decision function consumed by this sub): #11995
- Files touched:
ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs, ai/scripts/lifecycle/idleOutNudge.mjs, learn/agentos/wake-substrate/PersistentProcessManagement.md, learn/agentos/wake-substrate/NightShiftLeasedDriver.md
Handoff Retrieval Hints
- Semantic:
query_raw_memories({query: "tmux-inject Shape A cleanup SwarmHeartbeatService idleOutNudge Sub-iii"})
- Empirical:
git log --all --oneline -- ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs ai/scripts/lifecycle/idleOutNudge.mjs learn/agentos/wake-substrate/
Context
Sub-iii of Epic #11993 (Wake substrate evolution: 3-signal model + Shape B heartbeat pulse). Graduated from Discussion #11992 (cycle-3 RESOLVED). See Epic body for full architectural context and Signal Ledger.
Origin Session ID:
8f1a91ee-3ee4-4e4b-9865-b5810f6be353The Problem
After Sub-i lands
WakeSubscriptionService.emitHeartbeatPulse(#11994) and Sub-ii lands the 3-signal derivation + backoff + readiness parser (#11995), two legacy wake-delivery paths remain in the codebase:SwarmHeartbeatService.tmuxInjectPulsePrompt()(line 657) —tmux send-keys-based pulse injection. Dead path for non-tmux harnesses (Codex Desktop is native macOS, no tmux). Pre-bridge-daemon-era leftover.idleOutNudge.mjs— currently callsMailboxService.addMessageto wake recipients (Shape A path). Per @neo-gpt cycle-2 V-B-A: this creates durable MESSAGE nodes (MailboxService.mjs:337,548) + transcript-injected wake-blocks (bridge-daemon:522-538). Unacceptable Codex Desktop mailbox UX (~96 durable msgs/day/identity).Per Discussion #11992 cycle-3 @tobiu pushback: Shape A is NOT preserved as "diagnostic fallback." Preserving Shape A is the same preserve-the-workaround instinct that motivated rejected #11872 / #11982. If Shape B is substrate-correct, Shape A is just legacy that gets removed.
The Fix
SwarmHeartbeatService.tmuxInjectPulsePrompt()method entirely fromai/daemons/orchestrator/services/SwarmHeartbeatService.mjs. No "preserved as adapter" hedge — bridge-daemon already has a tmux adapter (perresumeHarness.mjs:213) if any operator ever genuinely needs tmux delivery; the SwarmHeartbeatService-side tmux-inject is redundant duplicate-of-logic.SwarmHeartbeatService.pulse()Step 7 with per-targetemitHeartbeatPulseiteration driven by Sub-ii's 3-signal-decision function:for (target of pulseIdentities) if (active && idle && ready) emitHeartbeatPulse({targetIdentity: target}).ai/scripts/lifecycle/idleOutNudge.mjsto callWakeSubscriptionService.emitHeartbeatPulse({targetIdentity})instead ofMailboxService.addMessage({to: targetIdentity, ...}). The function's external API stays the same; only the internal mechanism changes. No MESSAGE nodes created.learn/agentos/wake-substrate/PersistentProcessManagement.md+NightShiftLeasedDriver.md) to document the 3-signal model + Shape B canonical wake-pulse path. Remove tmux-injection references; remove Shape A heartbeat-via-mailbox documentation.Architectural Reality
emitHeartbeatPulseto exist.SwarmHeartbeatService.pulse()Step 7 replacement.ai/daemons/bridge/daemon.mjs— only the SwarmHeartbeatService-side tmux-inject is removed.idleOutNudge.mjs's callers (currently the SwarmHeartbeatService and any direct CLI invokers) see no API change — internal mechanism swap only.Contract Ledger
SwarmHeartbeatService.tmuxInjectPulsePrompt()(removed)grep -rn "tmuxInjectPulsePrompt" ai/ test/returns zero matches post-mergeSwarmHeartbeatService.pulse()Step 7emitHeartbeatPulseper target identity; iterates pulse targets, fires emit per target where Sub-ii's 3-signal-decision returns trueidleOutNudge.mjsheartbeat pathemitHeartbeatPulseinstead ofMailboxService.addMessageidleOutNudgeno longer creates MESSAGE nodesPersistentProcessManagement.md+NightShiftLeasedDriver.mdto document 3-signal model + Shape B canonical wake-pulse pathlearn/agentos/wake-substrate/returns zero post-mergeAcceptance Criteria
SwarmHeartbeatService.tmuxInjectPulsePrompt()method deleted entirely fromai/daemons/orchestrator/services/SwarmHeartbeatService.mjsSwarmHeartbeatService.pulse()Step 7 replaced withemitHeartbeatPulseper-target iteration; gated by Sub-ii's 3-signal-decision functionidleOutNudge.mjsrefactored to callWakeSubscriptionService.emitHeartbeatPulseinstead ofMailboxService.addMessageidleOutNudgeno longer creates MESSAGE nodes (assert post-call mailbox count unchanged)learn/agentos/wake-substrate/PersistentProcessManagement.md+NightShiftLeasedDriver.mdgrep -rn "tmuxInjectPulsePrompt|tmux.*pulse" ai/ test/returns zero;idleOutNudge.mjsdoes not referenceMailboxService.addMessagepull-request §6.1active AND idle AND readyfor@neo-gpt(the original symptom #11872 was reframed-and-rejected for)Out of Scope
WakeSubscriptionService.emitHeartbeatPulsemechanism — Sub-i of Epic #11993 (#11994)Avoided Traps
SwarmHeartbeatService.tmuxInjectPulsePromptas "tmux adapter" — bridge-daemon already has tmux adapter; SwarmHeartbeatService-side duplicate is the legacy leftover being removedidleOutNudge.mjsas "diagnostic fallback" — explicitly rejected by @tobiu cycle-3 Discussion #11992 pushbackPersistentProcessManagement.mdoutdated — doc drift compounds; the Epic AC7 + AC6 demand runbook updateRelated
emitHeartbeatPulseconsumed by this sub): #11994ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs,ai/scripts/lifecycle/idleOutNudge.mjs,learn/agentos/wake-substrate/PersistentProcessManagement.md,learn/agentos/wake-substrate/NightShiftLeasedDriver.mdHandoff Retrieval Hints
query_raw_memories({query: "tmux-inject Shape A cleanup SwarmHeartbeatService idleOutNudge Sub-iii"})git log --all --oneline -- ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs ai/scripts/lifecycle/idleOutNudge.mjs learn/agentos/wake-substrate/