LearnNewsExamplesServices
Frontmatter
id12850
titleWAKE digest counts already-woken-for messages → spoofs HIGH priority (#12479 residual)
stateClosed
labels
bugaimodel-experience
assigneesneo-opus-ada
createdAtJun 10, 2026, 7:42 PM
updatedAtJun 10, 2026, 11:41 PM
githubUrlhttps://github.com/neomjs/neo/issues/12850
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 10, 2026, 11:41 PM

WAKE digest counts already-woken-for messages → spoofs HIGH priority (#12479 residual)

Closed v13.0.0/archive-v13-0-0-chunk-17 bugaimodel-experience
neo-opus-ada
neo-opus-ada commented on Jun 10, 2026, 7:42 PM

Context

This session a [WAKE][priority:high] 449 events … 449 new messages fired for @neo-opus-ada whose cited "latest" was a message from 11:42 — read and processed ~5h earlier at session start. The actual newest unread was a 16:02 message I'd already handled; the ~535-message unread backlog (FYI broadcasts + an old batch, none mark_read'd) had been re-surfaced into the coalesced queue and reported as 449 "new", at HIGH priority.

This is the residual of #12479 ("Night-shift WAKE events reference already-read messages", CLOSED via PR #12690). #12479's shipped fix took hypotheses 2/3 — defer the flush during heavy GraphLog deltas + reconcile against readAt — but not hypothesis 1 (dedup the candidate set against already-delivered/woken message ids). So #12479's own AC2 ("a WAKE is not emitted for — nor previews/counts — a message the recipient has already been woken for / read") is only partially met: the count/priority still over-includes genuinely-unread-but-already-woken-for backlog.

The Problem

The digest reconciles on the wrong axis. flushSubscription filters the coalesced messages to not-marked-read only:

  • A message I've already been woken for but never explicitly mark_read'd is still readAt = null → it survives the readAt filter and is counted as "new" indefinitely.
  • The unread backlog of such already-seen-but-unread broadcasts grows unbounded (FYI broadcasts are rarely marked-read; there is no bulk-mark-read) — so when a heavy-delta resync / cursor event re-queues it, the count inflates to ~the full backlog.
  • An old priority:high message in that backlog (e.g. a stale [lane-claim]) drives getHighestWakePriority(messages) → the whole digest is tagged HIGH.

Net effect (the #12479-stated impact, recurring): agents are trained to distrust HIGH wakes / burn a full triage turn V-B-A'ing "already addressed". The flushDeferPolicy guard only covers the heavy-delta-transient-readAt-leak facet (already-read rows transiently mis-read during a sync); it does not cover genuinely-unread-but-already-woken backlog, which is a distinct over-count source.

The Architectural Reality

  • ai/daemons/wake/daemon.mjsflushSubscription (~L525-589): builds the digest from the coalesced queue. L558 messages = messages.filter(ev => !isMessageReadFor(db, ev.messageId, identity)) is the only reconciliation — readAt-based. L565 N = messages.length + …; L568 digestPriority = getHighestWakePriority(messages); L589 the [WAKE][priority:${digestPriority}] ${N} events … header.
  • ai/daemons/wake/flushDeferPolicy.mjs: HEAVY_DELTA_THRESHOLD=500 / HEAVY_DELTA_SETTLE_MS=60000 — defers the flush while a heavy delta settles, so already-read rows don't leak past the read filter mid-sync. Targets the transient-readAt facet only.
  • ai/services/memory-core/WakeSubscriptionService.mjs: the emission + liveCursor (mirrored to wakeSubscriptionLiveCursorPath). #12479 hypotheses 1 (candidate set not deduped vs delivered ids) and 3 (cursor non-advance re-includes delivered rows) were the candidates not addressed by the defer+readAt fix.

The Fix

Track, per subscription, the events the recipient has already been delivered/woken-for (a durable delivered-id set or high-water-mark alongside liveCursor), and compute the digest count and priority over only the genuinely-new-since-last-wake events — not over "unread-ever". A re-queued already-woken backlog (heavy-delta re-include, cursor reset) must contribute zero to the count and must not raise digestPriority. Genuinely-new unread messages / tasks / permissions / heartbeats still fire as today.

This is the hypothesis-1 dedup #12479 deferred; it composes with (does not replace) the existing flushDeferPolicy defer + readAt reconcile.

Decision Record impact

none / aligned-with the wake substrate. Relates-to #12479 (residual of its AC2). Challenges no ADR.

Acceptance Criteria

  • The WAKE digest count and priority are computed only over events the recipient has not already been woken for (genuinely-new-since-last-wake), not merely readAt = null.
  • Repro guard: with a non-empty unread-but-already-woken backlog that includes an old priority:high message, trigger a re-queue (simulated heavy-delta re-include / cursor event) → the digest does not report the full backlog count and is not tagged HIGH from the stale message.
  • The already-woken set / high-water-mark is durable across the daemon poll/coalesce cycle (persisted like liveCursor), surviving restart.
  • Regression: genuinely-new unread messages, task transitions, permission grants, and heartbeat pulses still wake correctly — no dropped wakes (the #12479 / flushDeferPolicy "never withhold a genuine wake" invariant holds).

Out of Scope

  • The heavy-delta transient-readAt-leak facet — already fixed (#12479 / PR #12690 + flushDeferPolicy). This ticket is the complement (genuinely-unread-but-already-woken, not transiently-mis-read).
  • Mailbox backlog drain / a bulk-mark-read primitive (the contributing fuel) — separate concern; the digest must count correctly regardless of backlog size.
  • Wake-digest content enrichment (#12612).

Avoided Traps

  • "Reconcile against readAt harder"readAt is the wrong axis: a genuinely-unread message can still be already-woken-for (seen, not yet triaged/marked-read). The dedup must be against delivered/woken, not read.
  • Mark-read-on-wake-delivery — would corrupt the read/unread semantics agents depend on (a woken-but-untriaged message is not "read"). Use a separate delivered/woken high-water-mark; do not conflate with readAt.
  • Drop the defer/flushDeferPolicy guard — it correctly handles the transient-readAt facet; this fix composes with it.

Related

  • #12479 — CLOSED; this is the residual of its AC2 (its shipped fix used hypotheses 2/3 defer+readAt, not hypothesis 1 delivered-id dedup). PR #12690.
  • #12612 — adjacent (idle-out digest content enrichment), not the count-accuracy/priority bug.
  • #11829 / #10777 — wake-driver substrate + engagement discipline this protects (false-HIGH wakes erode the leased-driver heartbeat signal).
  • #12402 — sibling wake-delivery validation lane.

Origin Session ID: 57035a6b-7712-4e04-896b-0425241db6af

Handoff Retrieval Hints: query_raw_memories "wake digest over-count already-woken-for spoof HIGH priority flushSubscription readAt liveCursor". Code anchors: ai/daemons/wake/daemon.mjs flushSubscription (L558 readAt filter, L568 getHighestWakePriority, L589 digest header), ai/daemons/wake/flushDeferPolicy.mjs, ai/services/memory-core/WakeSubscriptionService.mjs (liveCursor / emission).

Authored by Neo Opus Ada (@neo-opus-ada).

tobiu referenced in commit 78f953c - "fix(wake-daemon): dedup already-woken events from the digest count + priority (#12850) (#12866) on Jun 10, 2026, 11:41 PM
tobiu closed this issue on Jun 10, 2026, 11:41 PM