LearnNewsExamplesServices
Frontmatter
id13281
titleWake digest re-fires already-read messages despite readAt mitigations
stateClosed
labels
bugaiarchitecturemodel-experience
assigneesneo-opus-ada
createdAtJun 15, 2026, 2:23 AM
updatedAtJun 15, 2026, 2:57 AM
githubUrlhttps://github.com/neomjs/neo/issues/13281
authorneo-opus-ada
commentsCount0
parentIssue13012
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 15, 2026, 2:57 AM

Wake digest re-fires already-read messages despite readAt mitigations

Closed v13.1.0/archive-v13-1-0-chunk-3 bugaiarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on Jun 15, 2026, 2:23 AM

Context

Empirical, observed by @neo-opus-ada across the 2026-06-14/15 nightshift: the daemon wake digest re-fired already-read, already-handled messages as "new" at least 4 times in one session. Two batches: (1) three wakes citing the #13271 review thread (REQUEST_CHANGES / review-comment / APPROVED) AFTER all three were read + handled + marked-read the prior turn; (2) a wake citing the #13277 REQUEST_CHANGES after it was addressed + marked-read. Each stale wake costs a full state re-verification cycle (list_messages + gh state checks) to confirm "nothing new" — multiplied across every agent and every nightshift, a real swarm-wide wake-efficiency tax.

The Problem

The wake daemon's per-message read-state lookup transiently under-reports readAt, so a message the recipient has already read leaks into the "N new messages" digest and fires a stale wake. This is documented in the daemon itself:

  • ai/daemons/wake/daemon.mjs:334 — "lookup transiently under-reports readAt, leaking already-read backlog into the digest."
  • ai/daemons/wake/flushDeferPolicy.mjs:7-9 — "the daemon's per-message read-state lookup (isMessageReadFor) transiently under-reports readAt, so [the digest reports] 'N new messages' and can spoof a HIGH priority from that inflated set even though nothing [new]."

Existing mitigations — wokenWatermark.mjs (per-subscription watermark dedup) + flushDeferPolicy.mjs (defer on transient under-report) — reduce but do not eliminate the re-fire: the 4+ empirical stale wakes occurred with these mitigations already in place. There is a residual gap.

The Architectural Reality

  • ai/daemons/wake/daemon.mjs — the digest loop tail-syncs GraphLog from the lastSyncId cursor (getGraphLogEntries), reconciles on readAt, and applies the wokenWatermark dedup + flushDeferPolicy defer.
  • ai/daemons/wake/queries.mjs:288 — the unread query gates on json_extract(data, '$.properties.readAt') IS NULL; the transient under-report is in the per-message read-state lookup feeding the digest, not this query.
  • ai/daemons/wake/wokenWatermark.mjs — per-subscription watermark, advanced after a digest is delivered, meant to suppress re-delivery.
  • Alternative candidate mechanism: the bounded retry store for failed wake deliveries (#13077) — if a delivery is marked failed when the recipient has in fact consumed the message, the retry would re-fire it. The implementer should V-B-A which mechanism (digest readAt-leak vs retry-store re-fire) produces the residual.

The Fix (investigation — exact mechanism to be V-B-A'd)

Tighten the residual so a message the recipient has marked read does not re-fire a wake. Candidate directions (implementer chooses after V-B-A):

  • A post-digest read-state re-check that drops entries whose readAt is now set before firing the wake (closes the transient-under-report window at fire time, not only at digest-build time).
  • More reliable wokenWatermark advance / persistence so a delivered digest's entries are not re-eligible.
  • If the retry store (#13077) is the driver: mark a delivery satisfied on recipient read, so a consumed message is not retried.

Acceptance Criteria

  • A message the recipient has marked read does not re-fire as a "new message" in a subsequent wake digest (the residual is closed), with a test exercising the read-then-next-cycle path.
  • The fix composes with (does not regress) the wokenWatermark dedup + flushDeferPolicy defer + the readAt reconcile.
  • Evidence: a daemon-level test that reads a message, advances the cycle, and asserts no stale re-fire.

Out of Scope

  • The opposite failure — cursor ahead of MAX(log_id) → permanent wake silence (the #13077 line).
  • Digest content enrichment (#12862).
  • The broader idle-out-prevention epic (#11829) — this is a narrow correctness residual, not the multi-strategy substrate.

Related

  • #13077 (the wokenWatermark / cursor mitigations — this is a residual in that line). #11829 (Multi-strategy wake-driver epic). #12862 (digest enrichment). Parent epic #13012 (Agent Harness).

Release classification: post-release (Agent-runtime wake-efficiency; not v13.x-blocking).

Live latest-open sweep: checked the latest 20 open issues + a targeted grep over synced issues + a live gh search ("wake digest readAt") immediately before filing; no equivalent residual ticket (adjacent: #13077 cursor/silence, #12862 enrichment, #11829 idle-out).

Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e

Retrieval Hint: "wake digest re-fires already-read messages transient readAt under-report wokenWatermark flushDeferPolicy stale wake residual"