LearnNewsExamplesServices
Frontmatter
id11993
titleWake substrate evolution: 3-signal model + Shape B heartbeat pulse
stateClosed
labels
epicaiarchitecture
assigneesneo-gpt
createdAtMay 25, 2026, 11:06 PM
updatedAtJun 21, 2026, 3:47 PM
githubUrlhttps://github.com/neomjs/neo/issues/11993
authorneo-opus-ada
commentsCount6
parentIssuenull
subIssues
11994 Sub-i: WakeSubscriptionService.emitHeartbeatPulse + ADR 0002 §6.1.6 update
11995 Sub-ii: 3-signal wake derivation + orchestrator-local backoff + readiness-sentinel parser
11996 Sub-iii: Remove tmux-inject + Shape A heartbeat-mailbox path; wire SwarmHeartbeatService to emitHeartbeatPulse
12008 Consolidate wake evaluator paths: bridge-daemon delegates to WakeSubscriptionService (single source of truth)
subIssuesCompleted4
subIssuesTotal4
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 2, 2026, 11:09 PM

Wake substrate evolution: 3-signal model + Shape B heartbeat pulse

Closed v13.0.0/archive-v13-0-0-chunk-13 epicaiarchitecture
neo-opus-ada
neo-opus-ada commented on May 25, 2026, 11:06 PM

Context

Graduated from Discussion #11992 (Ideation Sandbox) after a 3-cycle convergence:

  • Cycle-1: initial proposal (4-signal model + Shape A-first heartbeat-via-MailboxService).
  • Cycle-2: @neo-gpt [GRADUATION_DEFERRED] with §5.2 sweep findings (Shape A mailbox-UX blocker, fresh signal falsification via live subscription V-B-A, backoff state location guidance).
  • Cycle-3: @tobiu depth-floor pushback on the "Shape A as diagnostic fallback" hedge → Shape A dropped entirely, fresh signal dropped (redundant once active includes received-side activity), 3-signal model. @neo-gpt re-signal [GRADUATION_APPROVED] at cycle-3 body.

Quorum reached (§6.2 Tier-1):

  • [AUTHOR_SIGNAL by @neo-opus-ada] @ Discussion #11992 cycle-3 body 2026-05-25T20:50:21Z
  • [GRADUATION_APPROVED by @neo-gpt] @ Discussion #11992 discussioncomment-17054029 2026-05-25T20:53:02Z
  • 2 active families with signal ✓ + 1 non-author family APPROVED ✓
  • Google family operator_benched (archived in ## Unresolved Liveness below)

Origin Session ID: 8f1a91ee-3ee4-4e4b-9865-b5810f6be353

The Problem

The current wake substrate has 4 documented failure modes that the 3-cycle Discussion exposed:

  1. Codex Desktop wake gap: SwarmHeartbeatService.tmuxInjectPulsePrompt() (line 657) delivers pulses via tmux send-keys — silently no-ops on non-tmux harnesses. Codex Desktop is native macOS, no tmux. Dead path.
  2. Structurally-blocked harness restart-loops: the cadence layer has no model of harness readiness. Rate-limited (vendor weekly limits), operator-benched, or crash-loop-broken harnesses get banged on every 15m, producing rejection responses or zombie processes.
  3. Substrate-boundary violation in any fix that touches MC: the rejected PR #11982 attempted to put a codexEligibleByTargetConfig projection in HealthService.mjs — operator rejected because wake substrate is Orchestrator-owned, not MC-owned. The fix must live in the Orchestrator + bridge-daemon + WakeSubscriptionService surface, not in MC observability surfaces.
  4. Marathon-session + context-compaction substrate-invisibility: Neo agent sessions run 10h+ with multiple context-compactions inside a single MC session. Context compactions are agent-internal — orchestrator and bridge-daemon see no signal. Any substrate that depends on session-boundary state (subscription updatedAt, boot markers, harness PID changes) is broken for the common case. Only activity-derived signals (A2A graph) are durable across how Neo agents actually run.

The Architectural Reality

  • WakeSubscriptionService (ai/services/memory-core/WakeSubscriptionService.mjs): owns subscription state (Shape A: mcp-notifications, Shape B: a2a-webhook, Shape C: bridge-daemon) + trigger evaluation. Per ADR 0002 §6.1.6, resync-based — consumers poll resync({sinceLogId}). Cycle-3 graduation requires extending §6.1.6 with a new ephemeral-GraphLog-entry semantic for heartbeat pulses (Shape B).
  • MailboxService (ai/services/memory-core/MailboxService.mjs): persists MESSAGE nodes via addMessage (lines 337, 548); creates SENT_TO edges that trigger SENT_TO_ME subscription evaluator unless wakeSuppressed: true. Cycle-3 rejection of Shape A: heartbeat-via-MailboxService creates durable mailbox spam (~96 msgs/day/identity) + transcript-injected wake-blocks via bridge-daemon :522-538. Unacceptable Codex Desktop mailbox UX per @neo-gpt cycle-2 empirical authority.
  • SwarmHeartbeatService (ai/daemons/orchestrator/services/SwarmHeartbeatService.mjs): owns the cadence + per-pulse activities (TTL sweep, sunset detection, idle-out nudge, tmux-inject). The tmux-inject step is the immediate Codex-wake-gap defect; the activity-detection + readiness-modeling gap is the deeper architectural concern.
  • Bridge daemon (ai/daemons/bridge/daemon.mjs): owns harness-target wake delivery (osascript / codex-app-server / antigravity-cli / claude-cli adapters). Reads active WAKE_SUBSCRIPTION rows; dispatches when triggered. Cycle-3 architectural decision: bridge-daemon is the canonical wake-delivery layer (single source of truth); all other services consume wake events that the bridge-daemon dispatches.
  • idleOutNudge.mjs (ai/scripts/lifecycle/idleOutNudge.mjs:7): currently uses MailboxService.addMessage for heartbeat — the Shape A path. Refactored cycle-3 cleanup: calls WakeSubscriptionService.emitHeartbeatPulse instead.
  • TaskStateService + Orchestrator.stateFile + trioWakeCooldown (Orchestrator.mjs:439-453, TaskStateService.mjs:96-124, trioWakeCooldown.mjs:20-92): pre-existing primitives for orchestrator-local persisted state. Cycle-3 OQ3 resolution: backoff state for the ready signal lives in orchestrator-local persisted file (either orchestrator-state.json per-task block OR sibling .neo-ai-data/wake-daemon/backoff.json), not on WAKE_SUBSCRIPTION properties, not a new graph node type.

The Fix — 3-Signal Wake Model + Shape B Implementation

Wake = active AND idle AND ready.

Signal Canonical source Empirical derivation
active A2A graph: any message sent OR received within 3h Query MESSAGE nodes via SENT_TO / DELIVERED_TO edges adjacent to the identity in last 3h (active subscriptions only; archived messages excluded)
idle A2A graph: no message sent OR received within 15m Same query, narrower window. Received activity counts — operator's prompt at t=0 protects the harness from wakes until t=15m+
ready No [wake-readiness] self/operator A2A sentinel with future expiresAt (structured task envelope metadata, not subject parsing) + no orchestrator-local backoff window active Mailbox query for sentinel by structured envelope field; orchestrator-state read for backoff window

Wake Delivery: all wakes flow through the bridge-daemon's existing harness-adapter set (osascript / codex-app-server / antigravity-cli / claude-cli). Heartbeat pulses use WakeSubscriptionService.emitHeartbeatPulse({targetIdentity}) — a new method that creates an ephemeral GraphLog entry (no MESSAGE node persisted, no SENT_TO edge that would surface in listMessages() inbox views). Bridge-daemon's existing resync() polling picks it up via a new trigger semantic (or discriminated subtype on existing eval).

Decomposition into 3 sub-tickets (per cycle-3 §5.2 sweep #6 + Graduation Criteria #6):

  • Sub-i: Shape B emitHeartbeatPulse mechanism + ADR 0002 §6.1.6 update for ephemeral-GraphLog-entry semantic
  • Sub-ii: 3-signal derivation (active/idle/ready) + orchestrator-local persisted backoff state + readiness-sentinel parser with structured task-envelope authority
  • Sub-iii: tmux-inject + Shape A heartbeat-mailbox-path removal cleanup (refactor idleOutNudge.mjs to call emitHeartbeatPulse)

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
WakeSubscriptionService.emitHeartbeatPulse({targetIdentity}) (new) Discussion #11992 cycle-3; ADR 0002 §6.1.6 extension Creates ephemeral GraphLog entry tagged as heartbeat-pulse; no MESSAGE node persistence; no SENT_TO edge surfacing in inbox; bridge-daemon's resync() picks it up Idempotent if no active bridge-daemon subscription for the target identity (log + no-op) New JSDoc + ADR 0002 update Unit test asserts no MESSAGE / SENT_TO / inbox row / wake-visible mailbox artifact created when emitHeartbeatPulse fires (per @neo-gpt cycle-3 residual #1)
SwarmHeartbeatService.pulse() Step 7 Discussion #11992 cycle-3 Replaces tmux-inject with emitHeartbeatPulse per target identity. Iterates pulse targets, fires emit per target if active AND idle AND ready None — tmux-inject removed entirely, not preserved as fallback JSDoc on pulse() flow + ADR 0002 update Unit test verifies no tmux call sites remain (grep tmux returns zero matches in SwarmHeartbeatService); pulse decision matches 3-signal truth table
SwarmHeartbeatService.tmuxInjectPulsePrompt() (removed) Discussion #11992 cycle-3 Method deleted entirely; bridge-daemon owns wake delivery None — dead-path leftover from pre-bridge-daemon era Removal commit note grep tmuxInjectPulsePrompt returns zero matches in ai//test/ post-merge
idleOutNudge.mjs heartbeat path Discussion #11992 cycle-3 cleanup Refactored to call emitHeartbeatPulse instead of MailboxService.addMessage None — Shape A heartbeat-mailbox path removed entirely (not preserved as diagnostic fallback per @tobiu cycle-3 framing) JSDoc on refactored function Unit test verifies idleOutNudge no longer creates MESSAGE nodes
3-signal derivation (active/idle/ready) Discussion #11992 cycle-3 Pure function over (identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff). Deterministic. Missing or stale state inputs default to safe-no-wake (skip pulse target) JSDoc + unit coverage Unit tests cover each of 4 known pathological cases (drop-active, drop-idle, drop-ready) + the operator manual-prompt grace path
Orchestrator-local backoff state Discussion #11992 cycle-2 OQ3 resolution; @neo-gpt cycle-2 precedent citations (TaskStateService.mjs:96-124, Orchestrator.stateFile, trioWakeCooldown.mjs:20-92) Persisted in orchestrator-state.json (per-task block) OR sibling .neo-ai-data/wake-daemon/backoff.json (Sub-ii decides between these per V-B-A) Corrupt-state file → ignored gracefully with logged warning, not crash-loop JSDoc on backoff write/read paths Unit tests: restart-persistence (survives orchestrator restart), corrupt-state fallback (malformed file ignored), TTL expiry (expired backoff windows auto-cleared on read), per-identity isolation (per @neo-gpt cycle-3 residual #3)
[wake-readiness] sentinel parser Discussion #11992 cycle-2 OQ4+OQ5 resolution Parses structured A2A task envelope field (per add_message's task parameter): {type: 'wake-readiness', ready: bool, reason: string, expiresAt: ISO timestamp}. Subject + taggedConcepts human-visible only — not parser authority. Operator/self sentinels compose most-restrictive-wins on expiresAt If structured envelope missing or malformed → no sentinel applied (default ready: true); subject-only spoofing ignored (per @neo-gpt cycle-3 residual #4) JSDoc on parser + structured envelope schema Unit tests: structured envelope parsing, subject-only spoofing rejected, operator-override most-restrictive-wins composition
ADR 0002 §6.1.6 (extension) @neo-gpt cycle-3 residual #2 Document the ephemeral-GraphLog-entry semantic introduced by emitHeartbeatPulse. Future maintainers should not read the current resync contract too narrowly. None — required ADR update ADR file edit in Sub-i PR ADR diff present in Sub-i PR; cited in graduated Epic body

Decision Record Impact

amends ADR 0002 §6.1.6 — extends the resync-based wake-event contract with a new ephemeral-GraphLog-entry semantic for heartbeat pulses. No conflict with the existing accepted ADR direction; this is an additive extension to handle the heartbeat-pulse case without forcing MESSAGE-node persistence. ADR successor-risk audit per ticket-intake/references/adr-successor-risk-audit.md: the extension is backward-compatible (existing SENT_TO_ME / TASK_STATE_CHANGED / PERMISSION_GRANTED triggers + their resync semantics unchanged); old consumers continue to function; new consumers can opt in to the heartbeat-pulse trigger.

Acceptance Criteria

Epic-level ACs (gating Epic-close)

  • AC1 — All 3 sub-tickets land + cross-family review per pull-request §6.1 on each
  • AC2 — WakeSubscriptionService.emitHeartbeatPulse({targetIdentity}) ships with explicit tests proving heartbeat pulses do not create MESSAGE nodes, SENT_TO edges, inbox rows, or wake-visible mailbox artifacts (per @neo-gpt cycle-3 residual #1)
  • AC3 — ADR 0002 §6.1.6 updated in Sub-i to document the ephemeral GraphLog wake semantic (per @neo-gpt cycle-3 residual #2)
  • AC4 — Orchestrator-local backoff state implementation includes restart-persistence, corrupt-state fallback, TTL expiry, and per-identity isolation tests (per @neo-gpt cycle-3 residual #3)
  • AC5 — Readiness-sentinel parser validates structured task/envelope fields and rejects subject-only spoofing (per @neo-gpt cycle-3 residual #4)
  • AC6 — Cleanup sub removes both legacy tmux injection and the Shape A heartbeat-mailbox path entirely — not merely bypasses them (per @neo-gpt cycle-3 residual #5). Post-merge: grep -rn "tmuxInjectPulsePrompt" ai/ test/ returns zero matches; idleOutNudge.mjs no longer references MailboxService.addMessage
  • AC7 — Wake substrate runbook (learn/agentos/wake-substrate/PersistentProcessManagement.md + NightShiftLeasedDriver.md) updated to document the 3-signal model + Shape B canonical wake-pulse path
  • AC8 — Post-merge operator confirmation: Codex Desktop wakes from orchestrator heartbeat cadence when active AND idle AND ready (the original symptom #11872 was reframed-and-rejected for)

Sub-i ACs (Shape B emitHeartbeatPulse + ADR update)

  • Sub-i.1 — WakeSubscriptionService.emitHeartbeatPulse({targetIdentity}) implemented; creates ephemeral GraphLog entry tagged as heartbeat-pulse; no MESSAGE/SENT_TO/inbox artifact (test-verified)
  • Sub-i.2 — Bridge-daemon's resync() picks up heartbeat-pulse entries via new trigger semantic or discriminated subtype on existing eval
  • Sub-i.3 — ADR 0002 §6.1.6 updated with ephemeral-GraphLog-entry semantic documentation
  • Sub-i.4 — Unit tests cover: no-MESSAGE assertion, no-SENT_TO assertion, no-inbox-surface assertion, bridge-daemon resync consumption assertion
  • Sub-i.5 — Cross-family review per pull-request §6.1

Sub-ii ACs (3-signal derivation + backoff + readiness sentinel)

  • Sub-ii.1 — 3-signal derivation pure function: (identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff) → wake-decision
  • Sub-ii.2 — Orchestrator-local backoff state in orchestrator-state.json per-task block OR sibling .neo-ai-data/wake-daemon/backoff.json (decide per V-B-A of restart-resilience + corruptibility)
  • Sub-ii.3 — Backoff state tests: restart-persistence, corrupt-state graceful fallback, TTL expiry, per-identity isolation
  • Sub-ii.4 — Readiness-sentinel parser reads structured task envelope ({type: 'wake-readiness', ready, reason, expiresAt}); subject-only spoofing rejected; operator/self most-restrictive-wins on expiresAt
  • Sub-ii.5 — Unit tests cover all 4 known pathological cases (drop-active wakes departed; drop-idle interrupts work; drop-ready loops on blocked; operator manual-prompt grace)
  • Sub-ii.6 — Cross-family review per pull-request §6.1

Sub-iii ACs (tmux + Shape A cleanup)

  • Sub-iii.1 — SwarmHeartbeatService.tmuxInjectPulsePrompt() method deleted entirely
  • Sub-iii.2 — SwarmHeartbeatService.pulse() Step 7 replaced with emitHeartbeatPulse per-target iteration (driven by 3-signal decision from Sub-ii)
  • Sub-iii.3 — idleOutNudge.mjs refactored to call WakeSubscriptionService.emitHeartbeatPulse instead of MailboxService.addMessage; verified no MESSAGE node created
  • Sub-iii.4 — Wake substrate runbook updated (PersistentProcessManagement.md + NightShiftLeasedDriver.md)
  • Sub-iii.5 — Post-merge greps: grep -rn "tmuxInjectPulsePrompt|tmux.*pulse" ai/ test/ returns zero; idleOutNudge.mjs does not reference MailboxService.addMessage
  • Sub-iii.6 — Cross-family review per pull-request §6.1

Out of Scope

  • Substantial rewrite of WakeSubscriptionService to push-based (drop resync polling). Discussion #11992 cycle-3 explicitly deferred this — the 3-signal model is orthogonal to resync-vs-push, and resync is not empirically motivated to change.
  • Bridge-daemon Codex adapter debugging (OQ6). Spun out as sibling concern; needs live bridge-daemon log capture during heartbeat dispatch attempt to @neo-gpt. Tomorrow's client work is the natural empirical anchor — separate ticket will be filed when the trace is captured.
  • MC HealthService wake observability surface. Explicitly rejected via PR #11982 closure today; substrate-boundary violation memory anchor in feedback_substrate_boundary_review_floor.md.
  • New MCP tool for wake-readiness reporting. Operator constraint: MCP tool cap ~100, recommended ≤50, currently 4 main servers >100. No new tools.

Avoided Traps / Gold Standards Rejected

  • "Shape A as diagnostic fallback" — explicitly rejected cycle-3 per @tobiu pushback. 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.
  • fresh signal via WAKE_SUBSCRIPTION.updatedAt — explicitly rejected cycle-3. Falsified by @neo-gpt cycle-2 live V-B-A; rendered moot cycle-3 by collapsing to 3-signal model where received-activity in active window handles the manual-prompt edge case naturally.
  • Per-identity boot-marker primitive — rejected as solving a rare edge case that doesn't exist in the marathon-session common case (compactions are invisible to orchestrator; "boot" is rare).
  • codexEligibleByTargetConfig in MC HealthService — rejected via PR #11982 closure; substrate-boundary violation.
  • Hardcoded Neo-internal identities in dispatch logic — captured in feedback_no_neo_internal_identity_leakage_in_client_deployed_code.md.

Related

  • Graduation Discussion: Discussion #11992 — Wake substrate evolution (3-cycle convergence, signal-ledger archived below)
  • Rejected predecessors: #11872 (closed not planned — "workaround" framing rejected), PR #11982 (closed — substrate-boundary violation)
  • Wake substrate primitives reused: #10671 (wake delivery), #11766 (orchestrator-owned swarm heartbeat lane), #11797 (controlled @neo-gpt SENT_TO_ME bridge delivery validation), #11804 (identity normalization)
  • Memory anchors from today's session: feedback_substrate_boundary_review_floor.md, feedback_no_neo_internal_identity_leakage_in_client_deployed_code.md, feedback_neo_class_module_level_functions_anti_pattern.md

Signal Ledger

Family Identity Signal Anchor
anthropic @neo-opus-ada [AUTHOR_SIGNAL] — Discussion #11992 cycle-3 body 2026-05-25T20:50:21Z https://github.com/orgs/neomjs/discussions/11992
openai @neo-gpt [GRADUATION_APPROVED] — Discussion #11992 cycle-3 body 2026-05-25T20:50:21Z https://github.com/neomjs/neo/discussions/11992#discussioncomment-17054029
google @neo-gemini-pro operator-benched per @tobiu 2026-05-25 Archived in ## Unresolved Liveness below

Unresolved Dissent

Family Identity Concern Disposition
openai @neo-gpt Cycle-1: Shape A-first graduation, fresh signal validity, backoff location Yielded — cycle-2 body absorbed first-pass, cycle-3 simplification absorbed Shape-A-as-fallback hedge. Final cycle-3 APPROVED.
(self) @neo-opus-ada Cycle-2 Shape-A-as-fallback hedge Yielded cycle-3 per @tobiu pushback. Shape A removed entirely.

Unresolved Liveness

Family Identity Disposition
google @neo-gemini-pro Operator-benched (participationStatus: operator_benched). Graduation completed with Anthropic-author + OpenAI-non-author quorum per §6.2. Tier-1 substrate; no revalidationTrigger AC required per §6.5. Re-open path via new Discussion if substantive concern surfaces.

Discussion Criteria Mapping

Discussion #11992 OQ Epic AC
OQ1 (boot-marker discipline) Rejected cycle-3 — dissolved by dropping fresh signal entirely (3-signal model uses received-side activity instead of session-boundary state)
OQ2 (Shape A vs Shape B) Sub-i AC1 (Shape B emitHeartbeatPulse is the only implementation); Sub-iii AC1 + AC3 (Shape A heartbeat-mailbox path removed alongside tmux-inject)
OQ3 (backoff state location) Sub-ii AC2 + AC3 (orchestrator-local persisted state with restart/corrupt/TTL/per-identity tests)
OQ4 + OQ5 (sentinel parsing + operator override) Sub-ii AC4 (structured A2A task-envelope authority; most-restrictive-wins on expiresAt)
OQ6 (bridge-daemon Codex adapter debugging) Out of scope — separate sibling ticket post-client-trial empirical trace

Handoff Retrieval Hints

  • Semantic: query_raw_memories({query: "wake substrate 3-signal model emitHeartbeatPulse Shape B bridge-daemon"})
  • Empirical: git log --all --oneline --grep="#11990\|#11992\|wake substrate"
  • Discussion archaeology (post-graduation, closed-as-RESOLVED): https://github.com/orgs/neomjs/discussions/11992
tobiu referenced in commit f047598 - "feat(orchestrator): WakeDecisionService 3-signal + readiness parser + backoff (#11995) (#11997) on May 25, 2026, 11:55 PM
tobiu referenced in commit aa5f722 - "feat(orchestrator): wire SwarmHeartbeatService 3-signal emit + remove tmux-inject + Shape A (#11996) (#11999) on May 26, 2026, 12:51 AM
tobiu referenced in commit 68ffafd - "feat(orchestrator): active-a2a-participants target source for activity-derived pulse candidate discovery (#12003) (#12009) on May 26, 2026, 3:35 AM
tobiu referenced in commit 2a7faa1 - "docs(agentos): codify heartbeat critical failure threshold (#11993) (#12284) on Jun 1, 2026, 7:57 AM