LearnNewsExamplesServices
Frontmatter
id16284
titleWake daemon digest names latest by queue position, not event time
stateClosed
labels
bugai
assigneesneo-kimi-iris
createdAtAug 1, 2026, 7:37 PM
updatedAtAug 1, 2026, 8:44 PM
githubUrlhttps://github.com/neomjs/neo/issues/16284
authorneo-kimi-iris
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 1, 2026, 8:44 PM

Wake daemon digest names latest by queue position, not event time

neo-kimi-iris
neo-kimi-iris commented on Aug 1, 2026, 7:37 PM

Context

Found by @neo-opus-ada during her APPROVED review of PR #16277 (2026-08-01T17:03:32Z): "a second implementation of the same digest in ai/daemons/wake/daemon.mjs that still carries the defect your own #16263 fixed — pre-existing, outside this diff… a live divergence that deserves its own falsifier." Verified at source before filing (V-B-A). This is the standalone wake daemon (Shape C), sibling to the in-process CoalescingEngineService digest that #16263 / #16275 repaired.

The Problem

buildWakeDigest() (ai/daemons/wake/daemon.mjs:2640-2688) names each bucket's "latest" by array positionmessages[messages.length - 1] (:2654), same shape for tasks (:2660), permissions (:2664), heartbeats (:2668). The queue is arrival-ordered (coalesceState[subId].queue.push(eventPayload), :814), so any out-of-order arrival — projection replay, retry-union rebuilds (:2693-2698 explicitly rebuild one digest over the UNION of events across failures), resync re-walks — can name a stale event "latest" while a newer one sits earlier in the array. The digest's "latest" pointer is what the woken agent reads first to decide whether the wake is worth acting on.

The sharpest evidence is internal: the same file's partitionMessageWakesByFreshness() (:917-944) already refuses position — "GraphLog position is intentionally not consulted: projection replay can append a new position for an old message" (:918-919). The daemon's freshness partitioner knows position lies; its digest builder trusts it.

Per-bucket clock inventory, verified against the daemon's own event mapper (:655-686):

Bucket Event shape (daemon mapper) True clock present?
message {type, messageId, from, subject, priority, sentAt, logId} (:658-666) sentAt
task {type, sourceEventId, taskId, …, lastModifiedAt, logId} (:667-679) lastModifiedAt
permission {type, scope, grantedBy, logId} (:681) ❌ none carried
heartbeat {type, targetIdentity, pulseId, summary, logId} (:683) ❌ none carried (logId is a cursor, not a clock)

The Architectural Reality

  • The fix lives in buildWakeDigest() only: pick "latest" per bucket by max resolvable event time where the event shape carries one (sentAt for messages, lastModifiedAt for tasks), keeping last-position behavior only for clock-less buckets (permission, heartbeat) — the same per-bucket truth discipline #16275 landed for the in-process engine.
  • partitionMessageWakesByFreshness (:927) already parses sentAt per message; reuse the same parse-and-guard idiom (Date.parse + Number.isFinite), no new dependency.
  • The retry-union path (:2787) rebuilds the digest via the same builder, so one fix covers both the live-flush and retry-union digests.
  • Existing daemon specs live at test/playwright/unit/ai/daemons/wake/ (sibling pattern from Ada's recent localWakeAdapters.spec.mjs / receiver.spec.mjs work); the new spec follows the same placement.
  • Decision Record impact: aligned-with ADR 0002 (wake substrate standards alignment — the same authority the engine-side fix cites).

The Fix

  1. In buildWakeDigest(): for the message bucket, pick latest by max sentAt (parse-guarded; unparseable/missing → fall back to position for that candidate). For the task bucket, pick latest by max lastModifiedAt. Permission and heartbeat buckets stay position-based — no clock exists on their daemon event shape — and the builder's JSDoc says exactly that, per bucket (no uniform-recency overclaim).
  2. No event-shape changes: the clocks already ride the mapper output; permission/heartbeat stay as-is (minting clocks for them is a wire-contract decision, Out of Scope).
  3. Spec (test/playwright/unit/ai/daemons/wake/, sibling harness): out-of-order message pair → freshest sentAt named latest; out-of-order task pair → freshest lastModifiedAt named latest; clock-less buckets keep last-position; the retry-union rebuild path gets the same recency (union arrays are not arrival-sorted by construction).

Acceptance Criteria

  • An out-of-order message queue names the max-sentAt message as latest in the digest string; falsifying spec RED before, GREEN after.
  • An out-of-order task queue names the max-lastModifiedAt transition as latest; RED → GREEN.
  • Permission and heartbeat buckets remain position-based, pinned by spec, and the builder JSDoc states the per-bucket truth.
  • The retry-union rebuild (:2787) resolves latest by the same recency (covered by one of the specs above driving the union path or by a direct builder-level union-array case).
  • Existing daemon specs pass unmodified.

Out of Scope

  • Minting a delivery-time clock for the daemon's permission / heartbeat event shapes (wire-contract decision; the in-process engine side already made its own choice at WakeSubscriptionService.mjs:1431-1433).
  • CoalescingEngineService (in-process Shape A/B digest) — completed by #16263 / #16275, different file.
  • partitionMessageWakesByFreshness semantics — already time-aware; untouched.

Avoided Traps / Gold Standards Rejected

  • Sorting the whole queue by clock before building. The queue's arrival semantics feed other logic (freshness partitioning, readAt reconcile, dedup); reordering it for one consumer risks side effects. The builder selects, it does not sort.
  • Claiming uniform recency again. The #16263 JSDoc overclaim is what #16275 just repaired on the engine side; this ticket ships the per-bucket truthful wording from the start.

Related

  • #16263 (engine-side position→recency fix, merged via PR #16265)
  • #16275 (engine-side completion: grantedAt / lastModifiedAt + JSDoc truth, PR #16277 — the review that surfaced this ticket)
  • ADR 0002 §6 (wake substrate standards alignment)

Live latest-open sweep: checked latest 20 open issues (created-desc) at 2026-08-01T17:45Z; no equivalent found (closest is my own #16275, engine-side by design). A2A in-flight sweep (last ~60 min, all read-states): claims on #16283 (Emmy, orchestrator healthcheck), #16280 (Ada, co-author trailers), #16278 (Vega, N=1 tenant) — no overlap.

Origin Session ID: session_fdc69689-d147-442f-8e12-1a2bc72ae4ee

Retrieval Hint: query_raw_memories("daemon buildWakeDigest latest position sentAt lastModifiedAt retry union")

Authored by Iris (@neo-kimi-iris, Kimi K3, Kimi Code CLI) 🌈

tobiu referenced in commit 198bced - "fix(ai): daemon digest names latest by per-bucket event time (#16284) (#16288)" on Aug 1, 2026, 8:44 PM
tobiu closed this issue on Aug 1, 2026, 8:44 PM