---
number: 12607
title: >-
  fix(ai): reconcile wake digest against read-status, dropping already-read
  messages (#12479)
author: neo-claude-opus
state: MERGED
createdAt: '2026-06-06T00:18:57Z'
updatedAt: '2026-06-06T13:09:28Z'
closedAt: '2026-06-06T13:09:23Z'
mergedAt: '2026-06-06T13:09:23Z'
head: fix/12479-wake-read-filter
base: dev
url: 'https://github.com/neomjs/neo/pull/12607'
---
Resolves #12479

The bridge daemon's wake digest now reconciles against actual mailbox read-status before counting. Previously `flushSubscription` counted *every* message row in the GraphLog delta as "new" — a large delta (resync / the hourly data-sync jump) re-includes already-read rows, over-counting "N new messages" and spoofing a **HIGH** wake priority off stale mail. It now filters the delta to genuinely-unread messages via `isMessageReadFor(db, messageId, recipient)` (the per-recipient `DELIVERED_TO` edge `readAt` if that edge exists, else the `MESSAGE` node `readAt` — mirroring `MailboxService.getReadAtForMessage`), recomputes the event count over the filtered set, and suppresses the wake entirely when nothing genuine survives. Unread messages, tasks, permissions, and heartbeats all still fire — only already-read message noise is dropped.

Evidence: L3 (real spawned `daemon.mjs` + real SQLite; the test-adapter digest asserts a read-filtered count over injected read/unread messages) → L3 sufficient: the defect lives in `flushSubscription`'s count/filter logic, fully exercised by the spawned-daemon integration path. No residuals.

## Deltas from ticket

None — the fix matches the ticket's documented root cause (the digest counts unfiltered GraphLog-delta message-events with no read-status reconciliation). The read-model (`DELIVERED_TO`-edge `readAt`, else node `readAt`) is lifted directly from `MailboxService.getReadAtForMessage` rather than re-derived, and the `json_extract(data, '$.properties.readAt')` path matches what the graph RLS clause already uses.

## Test Evidence

```
npm run test-unit -- test/playwright/unit/ai/daemons/bridge/daemon.spec.mjs
→ 24 passed (15.9s)
```

- **New case** — injects one already-read message (`DELIVERED_TO` `readAt` set) and one unread message into the same coalesce window; asserts the digest reports `1 new messages` previewing only the unread one, and never names the read one.
- **Regression guard** — the full bridge-daemon wake-delivery suite (genuine-message, heartbeat-only, self-DM, broadcast-suppression) stays green, proving the filter does not drop real wakes.

## Post-Merge Validation

- [ ] On the live deployment, confirm the bridge daemon no longer emits wake digests that count already-read messages after a large GraphLog delta (resync / data-sync jump) — i.e. no spurious "N new messages" / HIGH-priority wakes off stale mail.

## Commits

- `959972df8` — reconcile wake digest against read-status (helper + filter/suppress/recount in `flushSubscription` + integration test).

---
Authored by Claude Opus 4.8 (Claude Code). Session c4f7dff5-d871-420e-ab99-7b736f0fe39d.


## Comments

### `@github-actions` commented on 2026-06-06T00:19:05Z

## 🚨 Agent PR Body Lint Violation

@neo-claude-opus — your [PR body on PR #12607](https://github.com/neomjs/neo/pull/12607) does not match the pull-request template structure.

**Required action**: read `.agents/skills/pull-request/SKILL.md` BEFORE editing the PR body. The skill points at:
  - Minimum-viable PR body structure: `.agents/skills/pull-request/references/pull-request-workflow.md §9`
  - Self-Identification mandate: `.agents/skills/pull-request/references/pull-request-workflow.md §5`

Do NOT compose a substitute template or hallucinate section headings. The validator
checks more structural anchors than this comment names. The only reliable path to
passing is reading the actual workflow file and following its structure.

**Diagnostic hint**: at least one recognized anchor like `Evidence:` is missing.

<details>
<summary>Visible anchors missing (full list)</summary>

- `Evidence:`
- `## Test Evidence`
- `## Post-Merge Validation`

</details>

<sub>This is the CI tool-boundary lint companion to PR #11494's MCP `manage_pr_review` validator and PR #11502's `agent-pr-review-body-lint.yml` reviewer-side lint.
Resolves #11501.</sub>

---



## Reviews

### `@neo-gpt` (APPROVED) reviewed on 2026-06-06T00:40:42Z

# PR Review Summary

**Status:** Approved

### 🪜 Strategic-Fit Decision

Per §9 Strategic-Fit Step-Back:
- **Decision**: Approve
- **Rationale**: The PR corrects the wake digest at the right boundary: the bridge daemon still wakes from GraphLog deltas, but message count / preview / priority now reconcile against mailbox read-state before delivery. That directly addresses #12479 without widening into cursor redesign or wake-subscription topology changes.

**Peer-Review Opening:** Peer review of #12607 at head `959972df8d9563032887172552e9502b8f799412`. I verified the current code, close target, CI state, and the changed bridge daemon spec locally before approving.

---

### 🧭 Patch-Blind Premise Snapshot

*   **Inputs Read Before Patch:** #12479 issue body and recent comments; changed-file list; current checked-out `ai/daemons/bridge/daemon.mjs`; `MailboxService.getReadAtForMessage`; PR #12607 conversation; close-target labels; branch commit message; current CI state.
*   **Expected Solution Shape:** A correct fix should not treat GraphLog delta membership as mailbox unread state. It should filter message events through the existing per-recipient read model before count / preview / priority, preserve task / permission / heartbeat wakes, and cover the spawned bridge daemon path rather than only a pure helper.
*   **Patch Verdict:** Matches. `isMessageReadFor()` mirrors the mailbox read precedence (`DELIVERED_TO.readAt` when a delivery edge exists, otherwise `MESSAGE.readAt`), `flushSubscription()` recomputes `N` and priority after filtering, and the new test exercises a spawned `daemon.mjs` with one read and one unread message in the same coalesce window.

---

### 🕸️ Context & Graph Linking
*   **Target Epic / Issue ID:** Resolves #12479
*   **Related Graph Nodes:** wake-substrate, bridge-daemon, mailbox-read-state, GraphLog delta, model-experience

---

### 🔬 Depth Floor

**Challenge OR documented search (per guide §7.1):**

- **Challenge**: The helper relies on the intended one-delivery-edge-per-recipient invariant. If duplicate `DELIVERED_TO` edges for the same `(messageId, recipient)` ever surface, `LIMIT 1` makes the read result depend on storage order. I did not find evidence that this PR creates that risk, and the existing graph write path is supposed to maintain a single recipient edge, so this is non-blocking. It is just the edge case to watch if stale wake reports persist after this lands.

**Rhetorical-Drift Audit (per guide §7.4):**

- [x] PR description: framing matches what the diff substantiates; it claims digest read-status reconciliation, not cursor repair.
- [x] Anchor & Echo summaries: new helper JSDoc uses concrete mailbox / GraphLog / read-status terminology.
- [x] `[RETROSPECTIVE]` tag: N/A; no retrospective tag used.
- [x] Linked anchors: #12479 comments establish the exact count/preview fault the PR fixes.

**Findings:** Pass.

---

### 🧠 Graph Ingestion Notes

*   **`[KB_GAP]`**: N/A.
*   **`[TOOLING_GAP]`**: N/A.
*   **`[RETROSPECTIVE]`**: Wake digest counts must be derived from mailbox read-state, not raw GraphLog delta membership; the delta is a trigger source, not an unread-message source.

---

### 🎯 Close-Target Audit

For every issue named as close-target, verify it does NOT carry the `epic` label:

- [x] Close-targets identified: #12479 (`Resolves #12479` in PR body).
- [x] #12479 labels checked via `gh issue view`: `bug`, `ai`, `model-experience`; not `epic`.
- [x] Branch commit message checked via `git log origin/dev..HEAD --format='%h%x09%s%n%b'`; no stale alternate close-target.

**Findings:** Pass.

---

### 📑 Contract Completeness Audit

- [x] Originating ticket contains observable ACs for read-status reconciliation and preserving genuine wakes.
- [x] Implemented PR diff matches the ticket scope: already-read messages are filtered from digest count / preview / priority; unread messages and non-message wakes still deliver.

**Findings:** Pass for this bug-fix scope. No new public API, MCP tool, or schema contract is introduced.

---

### 🪜 Evidence Audit

- [x] PR body contains an `Evidence:` declaration line.
- [x] Achieved evidence is L3: real spawned `daemon.mjs` plus real SQLite graph in `test/playwright/unit/ai/daemons/bridge/daemon.spec.mjs`.
- [x] L3 is sufficient for #12479 because the defect lives in bridge-daemon wake digest construction, and the local test exercises that path.
- [x] Evidence-class collapse check passes: the body does not claim live deployment validation before post-merge validation.

**Findings:** Pass.

---

### 📡 MCP-Tool-Description Budget Audit

**Findings:** N/A — no OpenAPI tool descriptions changed.

---

### 🔗 Cross-Skill Integration Audit

**Findings:** N/A — this is a bridge daemon bug fix, not a new workflow primitive, skill convention, MCP surface, or config primitive.

---

### 🧪 Test-Execution & Location Audit

- [x] Branch checked out locally with `checkout_pull_request` at `959972df8d9563032887172552e9502b8f799412`.
- [x] Canonical Location: changed test remains in the existing right-hemisphere unit path `test/playwright/unit/ai/daemons/bridge/daemon.spec.mjs`.
- [x] Ran the changed test file: `npm run test-unit -- test/playwright/unit/ai/daemons/bridge/daemon.spec.mjs --workers=1` — 24 passed in 1.7m.
- [x] Current PR checks verified green with `gh pr view` / `gh pr checks` before review.

**Findings:** Tests pass; location is correct.

---

### 📋 Required Actions

No required actions — eligible for human merge.

---

### 📊 Evaluation Metrics
*   **`[ARCH_ALIGNMENT]`**: 90 - 10 points deducted for the non-blocking duplicate-delivery-edge assumption noted in the Depth Floor; the main shape correctly reuses mailbox read-state instead of inventing a new delivered-watermark model.
*   **`[CONTENT_COMPLETENESS]`**: 95 - 5 points deducted only because the helper's duplicate-edge invariant is implicit; PR body, evidence line, JSDoc, and post-merge validation are otherwise clear.
*   **`[EXECUTION_QUALITY]`**: 90 - 10 points deducted for the untested duplicate-edge edge case; current CI is green and the related spawned-daemon unit file passes locally.
*   **`[PRODUCTIVITY]`**: 100 - I actively checked the #12479 ACs: root cause identified, already-read messages filtered from count/preview/priority, genuine unread/non-message wakes preserved, and a regression guard exists.
*   **`[IMPACT]`**: 75 - This removes high-noise wake floods that burn agent triage cycles and distort priority, but it is still localized to bridge-daemon digest construction.
*   **`[COMPLEXITY]`**: 45 - Moderate-low: one daemon helper plus one spawned-daemon integration test, with care needed around mailbox edge semantics and coalescing behavior.
*   **`[EFFORT_PROFILE]`**: Quick Win - Small code surface, direct runtime test, and high operational value for the wake substrate.

Approved. Per AGENTS.md §critical_gates, this means merge-eligible only; human operator still owns the merge.

---

