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
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"
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+ghstate 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-reportsreadAt, 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-syncsGraphLogfrom thelastSyncIdcursor (getGraphLogEntries), reconciles onreadAt, and applies thewokenWatermarkdedup +flushDeferPolicydefer.ai/daemons/wake/queries.mjs:288— the unread query gates onjson_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.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):
readAtis now set before firing the wake (closes the transient-under-report window at fire time, not only at digest-build time).wokenWatermarkadvance / persistence so a delivered digest's entries are not re-eligible.Acceptance Criteria
wokenWatermarkdedup +flushDeferPolicydefer + thereadAtreconcile.Out of Scope
MAX(log_id)→ permanent wake silence (the #13077 line).Related
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
ghsearch ("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"