LearnNewsExamplesServices
Frontmatter
id10622
titleAuto-wake heartbeat skips every pulse: get_unread_count uses wrong JSON path ($.type vs $.label)
stateClosed
labels
bugairegression
assigneesneo-opus-4-7
createdAtMay 3, 2026, 10:44 AM
updatedAtMay 3, 2026, 11:17 AM
githubUrlhttps://github.com/neomjs/neo/issues/10622
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 3, 2026, 11:17 AM

Auto-wake heartbeat skips every pulse: get_unread_count uses wrong JSON path ($.type vs $.label)

Closedbugairegression
neo-opus-4-7
neo-opus-4-7 commented on May 3, 2026, 10:44 AM

Origin Session ID: b1839431-cba1-4b6d-913f-27b09e472e67

Context

Discovered while diagnosing an 8h43m idle gap across all 3 swarm agents (@neo-opus-4-7, @neo-gemini-3-1-pro, @neo-gpt) on 2026-05-02 → 2026-05-03 morning, despite running swarm-heartbeat.sh daemons launched per-identity.

Root cause (substrate-schema mismatch — same family as #10619 Cycle 1)

ai/scripts/swarm-heartbeat.sh:66 get_unread_count() queries:

WHERE json_extract(n.data, '$.type') = 'MESSAGE'

But MESSAGE nodes in the Memory Core graph use $.label = 'MESSAGE', not $.type. The query returns 0 unread regardless of mailbox state.

The token-economy gate immediately below (line 161) then says:

if [ "$unread" -eq 0 ] && [ "$issues" -eq 0 ]; then
    continue
fi

So every heartbeat pulse silently skips in the unread-only state. No pulse injection ever fires, even when the mailbox holds dozens of unread messages.

Empirical confirmation (cross-family, 2 agents)

Agent $.type query $.label query Mailbox preview unread
@neo-opus-4-7 0 48 48-49
@neo-gpt 0 20 (matches Codex inventory)

Same SQL with the corrected JSON path returns the expected counts on both substrates.

Fix shape

Single-line query correction:

-    local count=$(sqlite3 "$DB_PATH" "SELECT count(DISTINCT n.id) FROM Nodes n JOIN Edges e ON n.id = e.source AND e.type = 'SENT_TO' WHERE json_extract(n.data, '$.type') = 'MESSAGE' AND ...
+    local count=$(sqlite3 "$DB_PATH" "SELECT count(DISTINCT n.id) FROM Nodes n JOIN Edges e ON n.id = e.source AND e.type = 'SENT_TO' WHERE json_extract(n.data, '$.label') = 'MESSAGE' AND ...

Plus an inline comment referencing #10619 Cycle 1 (the AGENT_MEMORY hollow-success pattern that's structurally identical) so future RAs catch the category.

Avoided traps

  • Don't fold this into the active-idle non-tmux delivery design lane. That's a separate scoping concern (tmux send-keys only addresses tmux harnesses). This ticket is the substrate-truth fix that unblocks the existing path; the design lane is what makes the path useful for non-tmux harnesses.
  • Don't fold into identity-normalization hardening. Independent root cause; same structural family (substrate-schema drift) but different surface. Separate small PR.
  • Don't add a $.type fallback "for compatibility." No live MESSAGE rows use $.type. The pre-existing spec passed only because no test exercised this path with realistic data — same as the AGENT_MEMORY case in #10619.

Acceptance

  • swarm-heartbeat.sh:66 query uses $.label = 'MESSAGE'
  • Spec coverage: regression test that asserts get_unread_count returns nonzero when MESSAGE-labelled rows exist in the graph (mirrors the positive-extraction test pattern landed in #10619 Cycle 2)
  • PR body cites empirical query results from both my and @neo-gpt's substrates as ground-truth anchors

Cross-family review

@neo-gpt already proposed this work split in MESSAGE:3d80ed90... and confirmed the bug independently — primary reviewer once PR opens.

Provenance

  • Empirical anchor: 8h43m idle gap on 2026-05-02→05-03 with 3 swarm-heartbeat instances running, ZERO pulse output despite 104 expected cycles. Logs at .neo-ai-data/wake-daemon/heartbeat-*.log showed only "Starting Sleep-Cycle MVP Heartbeat Wrapper..." and "Booting Agent Session..." — no recovery messages, no pulse output, because every cycle silently skipped at the token-economy gate.
  • Pattern anchor: structurally identical to PR #10619 Cycle 1 finding (AGENT_MEMORY query targeted $.label='MEMORY' + $.properties.agent against substrate that uses 'AGENT_MEMORY' + userId). Both are "test passes but substrate path returns hollow data" — pre-existing spec exercised only the empty-DB fallback path, not the substrate query.
tobiu referenced in commit f0d237b - "fix(ai): align swarm-heartbeat unread query with MESSAGE schema (#10622) (#10623) on May 3, 2026, 11:17 AM
tobiu closed this issue on May 3, 2026, 11:17 AM