Group DM messages can silently drop all recipients (zero p-tags)
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
A message sent in a 3+ member group DM went out with **zero** `p` tags — nobody in the DM was notified, including agents whose dispatch loop only wakes on an explicit mention. The message just sat unanswered for ~50 minutes until the sender separately pinged one recipient by name in an unrelated channel.
## Design intent (confirmed, not in dispute)
`author_allowed()` (`crates/buzz-acp/src/lib.rs:224`) documents the assumption plainly:
> Clients auto-p-tag every DM participant, so in a DM any participant's message looks like a mention.
The DM-hardening security model (owner/sibling-only inside DMs) is built on this always holding. The frontend has matching logic meant to guarantee it: `messageMentionPubkeys()` (`desktop/src/features/messages/lib/messageMentionPubkeys.ts`) unions explicit `@mentions` with `channel.memberPubkeys` + `channel.participantPubkeys` for any `channelType === "dm"` — group DMs included, there is no separate "group" type and member count doesn't gate the logic.
## Root cause — two independent failure sources, either can zero out recipients
1. **`member_pubkeys`** (`desktop/src-tauri/src/commands/channels.rs` ~line 325-337) comes from a separate, best-effort relay query (kind:39002) batched across all channels, run concurrently with the last-message query. The code's own comment: "Both tolerate failures — empty defaults leave counts at 0 ... rather than aborting." If that relay round-trip is slow, flaky, or fails, `member_pubkeys` silently stays empty for every channel in that fetch — no retry, no surfaced error, indistinguishable from "genuinely zero members."
2. **`participant_pubkeys`** (`desktop/src-tauri/src/nostr_convert.rs:130`) is parsed once, synchronously, from the channel's own metadata event `p` tags at fetch time — no network call, generally reliable. But it's static: if someone is added to the DM *after* that metadata event was created, this list has no mechanism to pick up the addition.
`messageMentionPubkeys()` unions both, and normally `participant_pubkeys` alone is enough to cover a flaky `member_pubkeys` query (see the new resilience test below). **The incident requires both to be empty/stale at the same moment** — a relay hiccup on the member-count query landing on a channel object whose participant list also never got refreshed (e.g. a member added post-creation).
## What's confirmed vs. open
- Confirmed: the union logic itself is correct and has a working fallback (`participantPubkeys` alone is sufficient when `memberPubkeys` is empty).
- Confirmed: the failure mode reproduces in isolation when both sources are empty (see test below) — this matches the raw event tags from the live incident (`h` and `e` tags only, no `p` tags).
- **Open:** which of the two paths actually failed in the live incident — a slow/failed `member_pubkeys` relay query, a `participant_pubkeys` staleness from a post-creation membership add, or both. Needs either an interactive repro with devtools open, or instrumentation at the `messageMentionPubkeys()` call site to catch it happening again.
## Test coverage (this PR)
Added to `desktop/src/features/messages/lib/messageMentionPubkeys.test.mjs`:
- Group DM (4 members), plain message — p-tags every other participant.
- Resilience case — `memberPubkeys` empty but `participantPubkeys` intact still notifies everyone (proves the union's fallback works as designed).
- **Known-gap case** — both sources empty → zero recipients, reproducing the actual incident. Intentionally asserts the *current broken* behavior; comment tells whoever lands the fix to flip the assertion once one exists.
## Suggested next step
Pick a fix once the open question above is resolved — likely one or both of:
- Retry / surface an error on the `member_pubkeys` relay query instead of silently defaulting to empty.
- Refresh `participant_pubkeys` on channel-membership-change events instead of only parsing it once at metadata-fetch time.
Not included in this PR — the two candidate fixes are different in shape and size, and picking the wrong one without a confirmed live repro risks fixing the wrong path.
Contributor guide
Assessment
This issue has not been assessed yet.