Sub-ii 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.
The current wake substrate has no model of harness readiness — it uses partial signals (subscription presence, push-capability, tmux session existence) instead of activity-derived state. Three failure modes follow:
Wakes wake long-departed harnesses (no active signal) — wastes bridge-daemon dispatch budget
Wakes interrupt focused work (no idle signal) — operator's "stop bothering this agent" intervention has no mechanism
Wakes restart-loop on structurally-blocked harnesses (no ready signal) — rate-limited (Anthropic weekly limit at 5d), operator-benched (vendor-side ecosystem issues), or crash-loop-broken harnesses get banged on every 15m, producing rejection responses or zombie processes
Per Discussion #11992 cycle-3 architectural conclusion: the wake-decision must be a pure function of (identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff) — Wake = active AND idle AND ready.
The Fix
Implement the 3-signal wake-decision substrate:
active signal — query A2A graph for any MESSAGE node via SENT_TO / DELIVERED_TO edges adjacent to the identity in last 3h. Active subscriptions only; archived messages excluded.
idle signal — same query, narrower 15m window. Received activity counts (operator's prompt at t=0 protects the harness from wakes until t=15m+).
ready signal — composition of two sub-signals:
(a) No [wake-readiness] self/operator A2A sentinel with future expiresAt (structured task envelope metadata, not subject parsing)
(b) No orchestrator-local backoff window active for this identity
Orchestrator-local backoff state — persisted in orchestrator-state.json (per-task block) OR sibling .neo-ai-data/wake-daemon/backoff.json (decide between these per V-B-A of restart-resilience + corruptibility profile, citing TaskStateService.mjs:96-124, Orchestrator.stateFile at Orchestrator.mjs:439-453, and trioWakeCooldown.mjs:20-92 as precedent).
[wake-readiness] sentinel parser — reads structured A2A task envelope field: {type: 'wake-readiness', ready: bool, reason: string, expiresAt: ISO timestamp}. Subject + taggedConcepts are human-visible only (not parser authority). Operator/self sentinels compose most-restrictive-wins on expiresAt. Subject-only spoofing rejected.
The 3-signal-decision function is consumed by Sub-iii (SwarmHeartbeatService.pulse() Step 7 replacement), so Sub-ii ships the pure logic + state primitives.
AC1 — 3-signal derivation pure function implemented; signature: (identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff) → wake-decision
AC2 — Orchestrator-local backoff state location chosen between orchestrator-state.json per-task block vs .neo-ai-data/wake-daemon/backoff.json per V-B-A of restart-resilience + corruptibility; decision documented in PR body
AC3 — Backoff state tests: restart-persistence (state survives orchestrator restart), corrupt-state graceful fallback (malformed file ignored, not crash-loop), TTL expiry (expired backoff windows auto-cleared on read), per-identity isolation (backoff for one identity does not affect others)
❌ Implementing the active/idle signal as "sent within window" only (excluding received side) — falsifies the operator's manual-prompt edge case (received-activity at t=0 is the grace anchor)
❌ Boot-marker primitive for fresh signal — explicitly rejected cycle-3 by collapsing to 3-signal model
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
Context
Sub-ii 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
The current wake substrate has no model of harness readiness — it uses partial signals (subscription presence, push-capability, tmux session existence) instead of activity-derived state. Three failure modes follow:
activesignal) — wastes bridge-daemon dispatch budgetidlesignal) — operator's "stop bothering this agent" intervention has no mechanismreadysignal) — rate-limited (Anthropic weekly limit at 5d), operator-benched (vendor-side ecosystem issues), or crash-loop-broken harnesses get banged on every 15m, producing rejection responses or zombie processesPer Discussion #11992 cycle-3 architectural conclusion: the wake-decision must be a pure function of
(identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff)— Wake = active AND idle AND ready.The Fix
Implement the 3-signal wake-decision substrate:
activesignal — query A2A graph for anyMESSAGEnode viaSENT_TO/DELIVERED_TOedges adjacent to the identity in last 3h. Active subscriptions only; archived messages excluded.idlesignal — same query, narrower 15m window. Received activity counts (operator's prompt at t=0 protects the harness from wakes until t=15m+).readysignal — composition of two sub-signals:[wake-readiness]self/operator A2A sentinel with futureexpiresAt(structuredtaskenvelope metadata, not subject parsing)orchestrator-state.json(per-task block) OR sibling.neo-ai-data/wake-daemon/backoff.json(decide between these per V-B-A of restart-resilience + corruptibility profile, citingTaskStateService.mjs:96-124,Orchestrator.stateFileatOrchestrator.mjs:439-453, andtrioWakeCooldown.mjs:20-92as precedent).[wake-readiness]sentinel parser — reads structured A2Ataskenvelope field:{type: 'wake-readiness', ready: bool, reason: string, expiresAt: ISO timestamp}. Subject + taggedConcepts are human-visible only (not parser authority). Operator/self sentinels compose most-restrictive-wins onexpiresAt. Subject-only spoofing rejected.The 3-signal-decision function is consumed by Sub-iii (
SwarmHeartbeatService.pulse()Step 7 replacement), so Sub-ii ships the pure logic + state primitives.Architectural Reality
MailboxService.listMessages, GraphService node/edge access)TaskStateService(ai/daemons/orchestrator/services/TaskStateService.mjs:96-124) provides the persisted-state read/write patternOrchestrator.stateFile(ai/daemons/orchestrator/Orchestrator.mjs:439-453) is the canonical orchestrator-local state filetrioWakeCooldown.mjs(ai/scripts/lifecycle/trioWakeCooldown.mjs:20-92) demonstrates the wake-cooldown-in-.neo-ai-data/wake-daemon/precedentadd_message'staskparameter already exists per A2A Task Schema; structured envelope field is the parser surfaceContract Ledger
(identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff) → wake-decisionorchestrator-state.jsonper-task block OR.neo-ai-data/wake-daemon/backoff.json(decide per V-B-A)[wake-readiness]sentinel parsertaskenvelope ({type: 'wake-readiness', ready, reason, expiresAt}); subject-only spoofing rejected; operator/self most-restrictive-wins onexpiresAtready: true)Acceptance Criteria
(identity, current-time, A2A-graph-state, readiness-sentinel-state, orchestrator-state-backoff) → wake-decisionorchestrator-state.jsonper-task block vs.neo-ai-data/wake-daemon/backoff.jsonper V-B-A of restart-resilience + corruptibility; decision documented in PR body[wake-readiness]sentinel parser reads structuredtaskenvelope ({type: 'wake-readiness', ready, reason, expiresAt})taskenvelope → sentinel NOT applied)expiresAt; multi-sentinel merge testedpull-request §6.1Out of Scope
emitHeartbeatPulsemechanism — Sub-i of Epic #11993SwarmHeartbeatService.pulse()Step 7 replacement — Sub-iii of Epic #11993 (consumes this sub's 3-signal-decision function)Avoided Traps
WAKE_SUBSCRIPTIONproperties — pollutes client-deployment graph substrate with operator-local harness healthsubject.startsWith('[wake-readiness]')) — fragile, spoofable by accidental subject patternsactive/idlesignal as "sent within window" only (excluding received side) — falsifies the operator's manual-prompt edge case (received-activity at t=0 is the grace anchor)freshsignal — explicitly rejected cycle-3 by collapsing to 3-signal modelRelated
TaskStateService.mjs:96-124,Orchestrator.stateFileatOrchestrator.mjs:439-453,trioWakeCooldown.mjs:20-92emitHeartbeatPulsemechanismHandoff Retrieval Hints
query_raw_memories({query: "3-signal wake derivation backoff readiness sentinel Sub-ii"})git log --all --oneline -- ai/daemons/orchestrator/Orchestrator.mjs ai/daemons/orchestrator/services/TaskStateService.mjs ai/scripts/lifecycle/trioWakeCooldown.mjs