Skip to content

cli/daemon: subscription dedup does not guard an empty/absent message_id — an empty-id message is re-delivered on every live event, replay, and snapshot #898

Description

@erskingardner

Severity: LOW
Component: crates/cli (daemon)

Summary

The daemon's per-subscription dedup treats an empty (or absent) message_id as "not yet seen" every time: BoundedMessageSubscriptionIds::insert("") returns true, and mark_stream_response_seen only records ids that pass that insert. So a stream response whose message_id is empty/missing is never recorded and is emitted on every recurrence — live delivery, events.recent_messages() replay, and the initial snapshot.

Location

  • crates/cli/src/daemon/protocol.rs:21-36BoundedMessageSubscriptionIds::insert returns true for an empty id (the empty-id guard).
  • crates/cli/src/daemon/responses.rs:374-400mark_stream_response_seen uses ...message_id...is_none_or(|id| seen.insert(id)), so None/empty short-circuits to "deliver."

Concrete failure scenario

A subscriber receives a stream response carrying an empty message_id (e.g. a synthesized or id-less update). It is never inserted into the bounded seen-set, so the same row is delivered again on the next live event, again on recent_messages() replay after a reconnect, and again in a fresh snapshot — the subscriber sees duplicates.

Impact

Duplicate delivery of empty-id rows to daemon subscribers. Low severity because real rows carry ids; this is the id-less/degenerate path.

Suggested fix

Guard the empty-id case in the daemon dedup path the same way the marmot-app recovery path does (#807): treat an empty/absent message_id as non-dedupable-but-log, or synthesize a stable dedup key, rather than always re-delivering.

Dedup

Sibling of #807 (marmot-app live-subscription dedupe doesn't guard empty message_id_hex) but a distinct layer — this is the CLI daemon fan-out (responses.rs/protocol.rs), not the marmot-app subscription path. Same class, different site (cf. the repo's practice of tracking distinct sibling sites separately, e.g. #856).


Filed from a meticulous automated code-review pass; verified against source and deduplicated against the existing open/closed issue set.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LOWbugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions