Context
D#12630 graduated the idle-holding fix under Epic #11829. This issue is Sub B of that graduation.
The previous body described A2A add_message / MESSAGE.wakeMetadata consumer work. That shape is stale. The corrected source of authority is daemon GraphLog-pulse content: idleOutNudge.mjs -> WakeSubscriptionService.emitHeartbeatPulse() -> the bridge-daemon wake digest. Daemon pulses are not MESSAGE nodes, so they cannot use the A2A wakeMetadata schema.
Verified Source Anchors
- D#12630 final
[GRADUATED_TO_TICKET] maps Sub B to #12612 and names the corrected daemon-vs-A2A split.
ai/scripts/lifecycle/idleOutNudge.mjs emits idle-out heartbeat pulses through WakeSubscriptionService.emitHeartbeatPulse().
WakeSubscriptionService.emitHeartbeatPulse() records GraphLog pulse events rather than mailbox messages.
ai/daemons/bridge/daemon.mjs renders delivered wake digests; current heartbeat lines are opaque count summaries such as N heartbeat pulses plus the latest GraphLog id.
- PR #12608 A2A
wakeMetadata was held/closed as superseded for this lane; #11909 remains open for the daemon wake-content layer.
The Problem
The daemon wake digest can tell a receiver that heartbeat or idle-out pulses happened, but it does not expose enough lifecycle state for the agent to choose the next cycle step without prose inference.
That opacity is part of the idle-holding failure mode: an agent sees a wake, treats async wait states as synchronous blockers, and signs off instead of advancing the lifecycle queue or claiming a non-colliding lane.
The Fix
Enrich the daemon GraphLog pulse path and bridge-daemon digest with compact cycle content:
- Carry machine-readable cycle-state / lane-ownership / next-action fields for idle-out and heartbeat pulse wakes at the GraphLog payload boundary, bridge summary boundary, or a clearly documented equivalent daemon boundary.
- Render those fields in the bridge-daemon wake digest so the receiving harness can branch on the cycle step without regex-parsing subject/body prose.
- Preserve replay and delivery semantics for GraphLog-only pulses. Do not convert daemon pulses into
MESSAGE nodes unless a separate contract explicitly requires it.
- Add focused tests for the daemon pulse payload/digest path and legacy heartbeat fallback.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Evidence |
idleOutNudge.mjs -> WakeSubscriptionService.emitHeartbeatPulse() |
D#12630 Sub B + #11909 daemon scope |
Idle-out pulses carry enough structured cycle state for the receiver to select the next step |
If a pulse has no cycle context, legacy GraphLog-only delivery still works |
Focused wake-subscription / lifecycle tests |
ai/daemons/bridge/daemon.mjs wake digest |
D#12630 Sub B + #11829 wake-driver substrate |
Heartbeat / idle-out digest lines include cycle-state, lane ownership, and next-action content when present |
Existing opaque count digest remains valid for legacy pulses |
Bridge-daemon digest tests |
A2A add_message.wakeMetadata |
Corrected daemon-vs-A2A split in D#12630 |
Out of scope for this issue; it is a different mechanism |
A2A awareness/noise control is handled separately through the existing wakeSuppressed path |
No A2A schema assertions in this PR |
Acceptance Criteria
Out of Scope
- A2A
MESSAGE.wakeMetadata schema/storage/read surfaces.
- Relaxing
wakeSuppressed for FYI peer-message noise control.
- Reworking the whole wake-subscription trigger system.
- Replacing bridge-daemon delivery adapters.
Related
Parent epic: #11829
Graduation source: D#12630
Daemon backing ticket: #11909
Superseded A2A path: PR #12608
Retrieval Hint: daemon GraphLog heartbeat pulse bridge digest idle-out cycle-state next-action
Context
D#12630 graduated the idle-holding fix under Epic #11829. This issue is Sub B of that graduation.
The previous body described A2A
add_message/MESSAGE.wakeMetadataconsumer work. That shape is stale. The corrected source of authority is daemon GraphLog-pulse content:idleOutNudge.mjs->WakeSubscriptionService.emitHeartbeatPulse()-> the bridge-daemon wake digest. Daemon pulses are notMESSAGEnodes, so they cannot use the A2AwakeMetadataschema.Verified Source Anchors
[GRADUATED_TO_TICKET]maps Sub B to #12612 and names the corrected daemon-vs-A2A split.ai/scripts/lifecycle/idleOutNudge.mjsemits idle-out heartbeat pulses throughWakeSubscriptionService.emitHeartbeatPulse().WakeSubscriptionService.emitHeartbeatPulse()records GraphLog pulse events rather than mailbox messages.ai/daemons/bridge/daemon.mjsrenders delivered wake digests; current heartbeat lines are opaque count summaries such asN heartbeat pulsesplus the latest GraphLog id.wakeMetadatawas held/closed as superseded for this lane; #11909 remains open for the daemon wake-content layer.The Problem
The daemon wake digest can tell a receiver that heartbeat or idle-out pulses happened, but it does not expose enough lifecycle state for the agent to choose the next cycle step without prose inference.
That opacity is part of the idle-holding failure mode: an agent sees a wake, treats async wait states as synchronous blockers, and signs off instead of advancing the lifecycle queue or claiming a non-colliding lane.
The Fix
Enrich the daemon GraphLog pulse path and bridge-daemon digest with compact cycle content:
MESSAGEnodes unless a separate contract explicitly requires it.Contract Ledger Matrix
idleOutNudge.mjs->WakeSubscriptionService.emitHeartbeatPulse()ai/daemons/bridge/daemon.mjswake digestadd_message.wakeMetadatawakeSuppressedpathAcceptance Criteria
add_message,MESSAGE.wakeMetadata, or mailbox sender population.Out of Scope
MESSAGE.wakeMetadataschema/storage/read surfaces.wakeSuppressedfor FYI peer-message noise control.Related
Parent epic: #11829
Graduation source: D#12630
Daemon backing ticket: #11909
Superseded A2A path: PR #12608
Retrieval Hint:
daemon GraphLog heartbeat pulse bridge digest idle-out cycle-state next-action