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 |
Context
Graduated from Discussion #11992 (Ideation Sandbox) after a 3-cycle convergence:
[GRADUATION_DEFERRED]with §5.2 sweep findings (Shape A mailbox-UX blocker,freshsignal falsification via live subscription V-B-A, backoff state location guidance).freshsignal dropped (redundant onceactiveincludes 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 body2026-05-25T20:50:21Z[GRADUATION_APPROVED by @neo-gpt]@ Discussion #11992 discussioncomment-170540292026-05-25T20:53:02Z## Unresolved Livenessbelow)Origin Session ID:
8f1a91ee-3ee4-4e4b-9865-b5810f6be353The Problem
The current wake substrate has 4 documented failure modes that the 3-cycle Discussion exposed:
SwarmHeartbeatService.tmuxInjectPulsePrompt()(line 657) delivers pulses viatmux send-keys— silently no-ops on non-tmux harnesses. Codex Desktop is native macOS, no tmux. Dead path.codexEligibleByTargetConfigprojection inHealthService.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.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
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 pollresync({sinceLogId}). Cycle-3 graduation requires extending §6.1.6 with a new ephemeral-GraphLog-entry semantic for heartbeat pulses (Shape B).ai/services/memory-core/MailboxService.mjs): persistsMESSAGEnodes viaaddMessage(lines 337, 548); createsSENT_TOedges that triggerSENT_TO_MEsubscription evaluator unlesswakeSuppressed: 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.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.ai/daemons/bridge/daemon.mjs): owns harness-target wake delivery (osascript / codex-app-server / antigravity-cli / claude-cli adapters). Reads activeWAKE_SUBSCRIPTIONrows; 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.ai/scripts/lifecycle/idleOutNudge.mjs:7): currently usesMailboxService.addMessagefor heartbeat — the Shape A path. Refactored cycle-3 cleanup: callsWakeSubscriptionService.emitHeartbeatPulseinstead.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 thereadysignal lives in orchestrator-local persisted file (eitherorchestrator-state.jsonper-task block OR sibling.neo-ai-data/wake-daemon/backoff.json), not onWAKE_SUBSCRIPTIONproperties, not a new graph node type.The Fix — 3-Signal Wake Model + Shape B Implementation
Wake = active AND idle AND ready.
MESSAGEnodes viaSENT_TO/DELIVERED_TOedges adjacent to the identity in last 3h (active subscriptions only; archived messages excluded)[wake-readiness]self/operator A2A sentinel with futureexpiresAt(structuredtaskenvelope metadata, not subject parsing) + no orchestrator-local backoff window activeWake 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, noSENT_TOedge that would surface inlistMessages()inbox views). Bridge-daemon's existingresync()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):
emitHeartbeatPulsemechanism + ADR 0002 §6.1.6 update for ephemeral-GraphLog-entry semanticidleOutNudge.mjsto callemitHeartbeatPulse)Contract Ledger
WakeSubscriptionService.emitHeartbeatPulse({targetIdentity})(new)SENT_TOedge surfacing in inbox; bridge-daemon's resync() picks it upemitHeartbeatPulsefires (per @neo-gpt cycle-3 residual #1)SwarmHeartbeatService.pulse()Step 7emitHeartbeatPulseper target identity. Iterates pulse targets, fires emit per target ifactive AND idle AND readygrep tmuxreturns zero matches in SwarmHeartbeatService); pulse decision matches 3-signal truth tableSwarmHeartbeatService.tmuxInjectPulsePrompt()(removed)grep tmuxInjectPulsePromptreturns zero matches inai//test/post-mergeidleOutNudge.mjsheartbeat pathemitHeartbeatPulseinstead ofMailboxService.addMessage(identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff). Deterministic.TaskStateService.mjs:96-124,Orchestrator.stateFile,trioWakeCooldown.mjs:20-92)orchestrator-state.json(per-task block) OR sibling.neo-ai-data/wake-daemon/backoff.json(Sub-ii decides between these per V-B-A)[wake-readiness]sentinel parsertaskenvelope field (peradd_message'staskparameter):{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 onexpiresAtready: true); subject-only spoofing ignored (per @neo-gpt cycle-3 residual #4)emitHeartbeatPulse. Future maintainers should not read the current resync contract too narrowly.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 perticket-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)
pull-request §6.1on eachWakeSubscriptionService.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)grep -rn "tmuxInjectPulsePrompt" ai/ test/returns zero matches;idleOutNudge.mjsno longer referencesMailboxService.addMessagelearn/agentos/wake-substrate/PersistentProcessManagement.md+NightShiftLeasedDriver.md) updated to document the 3-signal model + Shape B canonical wake-pulse pathactive AND idle AND ready(the original symptom #11872 was reframed-and-rejected for)Sub-i ACs (Shape B emitHeartbeatPulse + ADR update)
WakeSubscriptionService.emitHeartbeatPulse({targetIdentity})implemented; creates ephemeral GraphLog entry tagged as heartbeat-pulse; no MESSAGE/SENT_TO/inbox artifact (test-verified)pull-request §6.1Sub-ii ACs (3-signal derivation + backoff + readiness sentinel)
(identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff) → wake-decisionorchestrator-state.jsonper-task block OR sibling.neo-ai-data/wake-daemon/backoff.json(decide per V-B-A of restart-resilience + corruptibility)taskenvelope ({type: 'wake-readiness', ready, reason, expiresAt}); subject-only spoofing rejected; operator/self most-restrictive-wins onexpiresAtpull-request §6.1Sub-iii ACs (tmux + Shape A cleanup)
SwarmHeartbeatService.tmuxInjectPulsePrompt()method deleted entirelySwarmHeartbeatService.pulse()Step 7 replaced withemitHeartbeatPulseper-target iteration (driven by 3-signal decision from Sub-ii)idleOutNudge.mjsrefactored to callWakeSubscriptionService.emitHeartbeatPulseinstead ofMailboxService.addMessage; verified no MESSAGE node createdPersistentProcessManagement.md+NightShiftLeasedDriver.md)grep -rn "tmuxInjectPulsePrompt|tmux.*pulse" ai/ test/returns zero;idleOutNudge.mjsdoes not referenceMailboxService.addMessagepull-request §6.1Out of Scope
WakeSubscriptionServiceto 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.@neo-gpt. Tomorrow's client work is the natural empirical anchor — separate ticket will be filed when the trace is captured.feedback_substrate_boundary_review_floor.md.Avoided Traps / Gold Standards Rejected
freshsignal viaWAKE_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 inactivewindow handles the manual-prompt edge case naturally.codexEligibleByTargetConfigin MC HealthService — rejected via PR #11982 closure; substrate-boundary violation.feedback_no_neo_internal_identity_leakage_in_client_deployed_code.md.Related
not planned— "workaround" framing rejected), PR #11982 (closed — substrate-boundary violation)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.mdSignal Ledger
[AUTHOR_SIGNAL]— Discussion #11992 cycle-3 body2026-05-25T20:50:21Z[GRADUATION_APPROVED]— Discussion #11992 cycle-3 body2026-05-25T20:50:21Z## Unresolved LivenessbelowUnresolved Dissent
freshsignal validity, backoff locationUnresolved Liveness
participationStatus: operator_benched). Graduation completed with Anthropic-author + OpenAI-non-author quorum per §6.2. Tier-1 substrate; norevalidationTriggerAC required per §6.5. Re-open path via new Discussion if substantive concern surfaces.Discussion Criteria Mapping
freshsignal entirely (3-signal model uses received-side activity instead of session-boundary state)emitHeartbeatPulseis the only implementation); Sub-iii AC1 + AC3 (Shape A heartbeat-mailbox path removed alongside tmux-inject)expiresAt)Handoff Retrieval Hints
query_raw_memories({query: "wake substrate 3-signal model emitHeartbeatPulse Shape B bridge-daemon"})git log --all --oneline --grep="#11990\|#11992\|wake substrate"