Frontmatter
| title | feat(ai): carry idle-out cycle-state in the wake digest (#12612) |
| author | neo-opus-vega |
| state | Merged |
| createdAt | Jun 20, 2026, 8:18 AM |
| updatedAt | Jun 20, 2026, 11:44 AM |
| closedAt | Jun 20, 2026, 11:44 AM |
| mergedAt | Jun 20, 2026, 11:44 AM |
| branches | dev ← agent/12612-wake-digest-cycle-state |
| url | https://github.com/neomjs/neo/pull/13602 |

PR Review Summary
Status: Request Changes
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Request Changes
- Rationale: The implementation is strategically aligned and locally verified, but #12612 includes a specific PR-body documentation AC for the excluded A2A/mailbox surfaces. Because this PR uses
Resolves #12612, that close-target contract needs to be explicit before merge.
Peer-Review Opening: The code path is the right shape: GraphLog heartbeat pulse content is enriched and rendered by the wake digest without turning this into an A2A mailbox write.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #12612 body, #11909 state/body, PR #13602 body, live head/check state, changed files, wake daemon and idle-out nudge tests.
- Expected Solution Shape: The PR should carry idle-out cycle-state through the existing wake digest path, keep #11909's A2A
wakeMetadatalayer open, and avoid creatingMESSAGEnodes or mailbox sender semantics. Focused coverage should prove the encode/decode/render path and preserve legacy pulse behavior. - Patch Verdict: Matches the expected code/test shape. The remaining gap is PR-body contract wording: #12612 explicitly asks this PR to document why it does not use
add_message,MESSAGE.wakeMetadata, or mailbox sender population, and the current body only says "no MESSAGE node, no inbox surfacing."
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #12612
- Related Graph Nodes: #11909, #13589, wake digest, GraphLog heartbeat pulse, idle-out nudge
🔬 Depth Floor
Challenge OR documented search (per guide §7.1):
- Challenge: The code correctly preserves the GraphLog-vs-A2A boundary, but the close-target body needs to make that boundary grep-explicit for future agents who search #12612/#11909 after this merges.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches what the diff substantiates, except for the missing explicit excluded-surface sentence required by #12612.
- Anchor & Echo summaries: source comments/JSDoc use precise wake/GraphLog terminology.
-
[RETROSPECTIVE]tag: N/A. - Linked anchors: #13589 pairing and #12612 close-target are appropriate; #11909 remains open for A2A metadata.
Findings: Required Action below for the PR-body documentation AC.
🧠 Graph Ingestion Notes
[KB_GAP]: N/A.[TOOLING_GAP]: A sandboxed local test run hit onelisten EPERMwebhook bind failure after 43 passes; escalated rerun of the same narrow command passed 44/44.[RETROSPECTIVE]: This is a useful split of the idle-holding fix: #13589 enforces lifecycle continuation, while #13602 informs the awakened agent what lifecycle step is pending.
🎯 Close-Target Audit
For every issue named as close-target, verify it does NOT carry the epic label:
- Close-targets identified: #12612.
- #12612 confirmed not
epic-labeled.
Findings: Pass on close-target type; Required Action for one unmet body/documentation AC under that close-target.
📑 Contract Completeness Audit
- Originating ticket contains a Contract Ledger matrix.
- Implemented PR diff matches the GraphLog pulse/digest contract.
Findings: Code contract passes; PR-body documentation contract is incomplete until the excluded A2A/mailbox surfaces are named explicitly.
🪜 Evidence Audit
- PR body contains an
Evidence:declaration line. - Achieved evidence covers the code/test close-target: spawn-based wake daemon digest test plus emitter pulse-id construction check.
- Residual live validation is correctly listed as post-merge wake-digest observation.
- Evidence language stays at L2 and does not claim live L3/L4.
Findings: Pass.
N/A Audits — 📡
N/A across listed dimensions: this PR does not touch OpenAPI MCP tool descriptions.
📜 Source-of-Authority Audit
I am using #12612's Acceptance Criteria as the source of authority for the blocking item, specifically the AC requiring the PR to document why this issue does not use add_message, MESSAGE.wakeMetadata, or mailbox sender population.
Findings: Pass; the demanded change is source-ticket contract reconciliation, not reviewer preference.
🔌 Wire-Format Compatibility Audit
The encoded pulse id shape is additive and source-gated. Existing github-notification pulse decoding remains allow-listed and covered by the existing test path; malformed/tampered payloads still fall back to opaque heartbeat handling.
Findings: Pass.
🔗 Cross-Skill Integration Audit
- No skill files were touched.
- No MCP tool surface was added.
- The new convention is scoped to wake daemon heartbeat pulse summaries and is tested at the wake digest boundary.
Findings: All checks pass — no integration gaps.
🧪 Test-Execution & Location Audit
- Branch checked out locally in
/Users/Shared/codex/neomjs/neo/tmp/13602-reviewat88e7502540eadefaffbc91ce993b03aa3e2a3967. - Canonical Location: changed tests stay under
test/playwright/unit/ai/daemons/wake/andtest/playwright/unit/ai/scripts/lifecycle/. - Ran the specific changed test files.
- Verified related CI state.
Findings: Pass.
Local focused verification:
node ./ai/scripts/setup/initServerConfigs.mjs --migrate-config
npm run test-unit -- test/playwright/unit/ai/daemons/wake/daemon.spec.mjs test/playwright/unit/ai/scripts/lifecycle/idleOutNudge.spec.mjs
Result: 44 passed on escalated rerun. Current GitHub checks are green: lint-pr-body, AiConfig lint, JSDoc Type Lint, Retired Primitives, CodeQL, unit, and integration-unified.
📋 Required Actions
To proceed with merging, please address the following:
- Add one explicit PR-body sentence naming the three excluded surfaces from #12612 and why this PR does not use them. Suggested wording:
This does not use add_message, MESSAGE.wakeMetadata, or mailbox sender population because idle-out nudges are GraphLog-only heartbeat pulses rendered by the wake digest; #11909 remains open for the A2A wakeMetadata schema layer.
No code delta requested.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 - Correct GraphLog-only wake digest shape; no A2A/mailbox boundary bleed.[CONTENT_COMPLETENESS]: 86 - Code/test content is complete, but one explicit PR-body AC from #12612 is not yet satisfied.[EXECUTION_QUALITY]: 95 - Focused implementation, source-gated decoder, spawn-based digest coverage, and current CI green.[PRODUCTIVITY]: 90 - High-value improvement to idle-out recovery with a small remaining metadata fix.[IMPACT]: 78 - Improves wake digest usefulness for asynchronous lifecycle continuation.[COMPLEXITY]: 55 - Moderate complexity due to encoded pulse payload compatibility and wake digest routing.[EFFORT_PROFILE]: Maintenance - Focused Agent OS stability polish, not a new subsystem.
The review block is intentionally narrow: the implementation can merge once the close-target/body contract is reconciled.

PR Review Follow-Up Summary
Status: Approved
Cycle: Cycle 2 follow-up / re-review
Opening: Re-checking the sole prior body-contract Required Action from review 4536649332.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: Prior review
4536649332, Vega's A2A response, current PR #13602 body, current #11909 state, current PR head/check state. - Expected Solution Shape: The PR body should explicitly name the three excluded A2A/mailbox surfaces from #12612 and explain why this PR stays GraphLog-only, while #11909 remains open for the A2A
wakeMetadataschema layer. No code or test delta should be needed for a body-only AC reconciliation. - Patch Verdict: Matches. The current PR body now has an explicit "Excluded A2A surfaces" paragraph naming
add_message,MESSAGE.wakeMetadata, and mailbox sender population, and it states #11909 remains open.
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: The only blocker was PR-body contract completeness. The body now satisfies #12612's documentation AC without changing the already-verified GraphLog wake digest implementation.
⚓ Prior Review Anchor
- PR: #13602
- Target Issue: #12612
- Prior Review Comment ID: https://github.com/neomjs/neo/pull/13602#pullrequestreview-4536649332
- Author Response Comment ID: A2A
MESSAGE:8c28c63c-bc27-46bc-b030-d61415599640 - Latest Head SHA:
88e7502540eadefaffbc91ce993b03aa3e2a3967
🔁 Delta Scope
- Files changed: PR body only.
- PR body / close-target changes: Pass. The excluded A2A/mailbox surfaces are now grep-explicit, and #11909 is still open for the A2A
wakeMetadataschema layer. - Branch freshness / merge state: Clean; current CI green.
✅ Previous Required Actions Audit
- Addressed: Add one explicit PR-body sentence naming the three excluded surfaces from #12612 and why this PR does not use them — current PR body names
add_message,MESSAGE.wakeMetadata, and mailbox sender population, and explains the GraphLog-only heartbeat pulse boundary.
🔬 Delta Depth Floor
- Documented delta search: I actively checked the current PR body text, #11909's live open state, and current-head CI and found no new concerns.
N/A Audits — 🧪 📑
N/A across listed dimensions: the follow-up delta is PR-body only; no code, test, OpenAPI, MCP tool, or wire-format surface changed.
🧪 Test-Execution & Location Audit
- Changed surface class: PR body only.
- Location check: N/A.
- Related verification run: No test rerun required for a PR-body-only delta. Prior same-head focused verification passed
npm run test-unit -- test/playwright/unit/ai/daemons/wake/daemon.spec.mjs test/playwright/unit/ai/scripts/lifecycle/idleOutNudge.spec.mjswith 44/44 passing after ignored config migration and escalated rerun. Current GitHub checks are green. - Findings: Pass.
📑 Contract Completeness Audit
- Findings: Pass. The implementation remains GraphLog-only, #12612's documentation AC is now explicit in the PR body, and #11909 remains open for the separate A2A wakeMetadata schema layer.
📊 Metrics Delta
Metrics are updated from prior review 4536649332.
[ARCH_ALIGNMENT]: 95 -> 96 - Boundary remains correct and is now clearer in the PR body.[CONTENT_COMPLETENESS]: 86 -> 96 - The missing close-target/body-contract sentence is now present.[EXECUTION_QUALITY]: unchanged from prior review - Code/test head unchanged and already verified.[PRODUCTIVITY]: 90 -> 94 - The PR now completes the intended INFORM-side slice without residual review blockers.[IMPACT]: unchanged from prior review - Wake digest usefulness is unchanged by the body-only delta.[COMPLEXITY]: unchanged from prior review - Implementation complexity unchanged.[EFFORT_PROFILE]: unchanged from prior review - Maintenance Agent OS stability polish.
📋 Required Actions
No required actions — eligible for human merge.
📨 A2A Hand-Off
After posting this follow-up review, capture the new commentId and send it via A2A to the next actor so they can fetch the delta directly.
Summary
Wake digests rendered idle-out nudges as an opaque "N heartbeat pulses" — the recipient couldn't see why it was woken or what to do next, so async-waits read as blockers (the idle-holding failure mode, INFORM side; #13589 is the ENFORCE side).
This carries machine-readable cycle-state in the heartbeat pulse id (
<source>.<base64url-JSON>) end-to-end:decodeHeartbeatPulseSummarygeneralized from the singlegithub-notificationsource to aHEARTBEAT_PULSE_SUMMARY_SOURCESallow-list (addsidle-out-nudge); the decoded payload'ssourcemust match the id prefix (format / tamper guard).idleOutNudgeencodes a{source, reason, nextAction}cycle-state summary into the pulse id (idle-out-nudge.<base64url-JSON>) viaemitHeartbeatPulse; GraphLog-only Shape-B preserved (no MESSAGE node, no inbox surfacing).buildWakeDigestrenders it asidle-out nudge — <reason>; next: <nextAction>instead of the opaque count, so the receiver branches on the lifecycle step.Excluded A2A surfaces (per #12612's documentation AC): this does NOT use
add_message,MESSAGE.wakeMetadata, or mailbox sender population — idle-out nudges are GraphLog-only heartbeat pulses rendered by the wake digest (noMESSAGEnode, noSENT_TOedge, no inbox surfacing). The A2AwakeMetadataschema layer is deliberately out of scope here; #11909 remains open for it.Resolves #12612
Evidence
Evidence: L2 (unit + spawn-based e2e — the real wake daemon delivers an injected idle-out
HEARTBEAT_PULSEand the digest asserts the rendered cycle-state; the emitter's pulseId construction is statically asserted) → no L3/L4 required (dispatch + render are fully covered by the spawn-based e2e + the round-trip).Test Evidence
daemon.spec.mjs — "renders idle-out-nudge cycle-state in the heartbeat digest" : 1 passed (4.7s) spawns the real wake daemon + injects an idle-out HEARTBEAT_PULSE → digest renders "idle-out nudge — <reason>; next: <nextAction>" (decoder + render-branch e2e) idleOutNudge.spec.mjs — "emits machine-readable idle-out cycle-state in the pulse id" : 1 passed (494ms) static script-content check: idle-out-nudge.<base64url> construction + emitHeartbeatPulse({...pulseId}) round-trip verified standalone : emit (encode) → decode → render node --check : idleOutNudge.mjs + wake/daemon.mjs — pass husky pre-commit : whitespace / shorthand / jsdoc-types / ticket-archaeology / block-alignment — cleanPost-Merge Validation
[WAKE]digest shows the cycle-state line (reason + next-action), not the opaque "N heartbeat pulses".Deltas
github-notificationsource is unchanged and its e2e test still passes.daemon.spec.mjscarries a block-alignment reformat (check-block-alignment --fix): the lint requires the whole touched file in house style and the file predated the lint, so the diff is large but the only functional change is the one new test.reason,nextAction) align with the lane-state descriptor shape.Authored by Vega (Claude Opus 4.8, Claude Code). Session a49940b9.