Skip to content

fix(cli): dms list must query the relay-emitted kind:39000, not kind:41001 - #6847

Open
lodar wants to merge 1 commit into
block:mainfrom
5dive-ai:upstream-pr/dms-list-kind-39000
Open

fix(cli): dms list must query the relay-emitted kind:39000, not kind:41001#6847
lodar wants to merge 1 commit into
block:mainfrom
5dive-ai:upstream-pr/dms-list-kind-39000

Conversation

@lodar

@lodar lodar commented Aug 26, 2026

Copy link
Copy Markdown

Summary

dms list queried {kinds:[41001], "#p":[me]}. KIND_DM_CREATED (41001) is declared in buzz-core, but no code path in this repo ever emits it — so the verb returns [] for every account on every relay, while channels list, channels members and messages get all see the same conversation. Returning an empty list rather than an error is why this reads as "no DMs exist" instead of "my predicate matched nothing".

The relay-emitted truth is kind:39000 NIP-29 group metadata: emit_group_discovery_events tags it with t=<channel_type> plus one p tag per participant for DMs. kind:39000 is absent from required_scope_for_kind's allowlist, so client ingest rejects it as "restricted: unknown event kind" — only the relay can author one. t=dm is therefore a relay-attested channel type rather than a client-supplied name or membership heuristic, which matters because consumers skip the @-mention test on the DM path.

Key decisions:

  • dm_list_filter() is extracted, so a regression back to a kind nothing emits is a red test rather than a silent empty list.
  • parse_query_events() treats a relay error object as an error. The old unwrap_or_default() made a dead relay indistinguishable from an empty inbox.
  • dms_from_group_metadata() keeps only t=dm events with a non-empty d tag that lists us; a valueless ["t"] tag no longer masks a later ["t","dm"]; the newest revision per channel wins; results sort newest first.
  • KIND_DM_CREATED is annotated as reserved-and-unemitted rather than deleted, so nothing downstream breaks on the constant disappearing.

Related issue

No issue opened for this. Closest existing reports: #5137, #5424 and #2897 — all "dms list returns empty for an active conversation".

Two open PRs already attack the same defect inside dms.rs: #5725 and #5139. This one differs in resolving against the relay-attested kind:39000 discovery event, in distinguishing a relay error from an empty inbox, and in pinning the wire kind with a regression arm. If a maintainer prefers either of those directions, say so and I will close this in their favour.

Testing

  • 9 new unit tests in crates/buzz-cli/src/commands/dms.rs covering the filter shape, non-DM channel types, missing/valueless t tags, non-participation, empty channel uuid, newest-revision-wins ordering, malformed tags, and the relay-error-is-not-an-empty-list case.
  • One end-to-end arm added to crates/buzz-test-client/tests/e2e_nostr_interop.rs that pins the wire kind so the relay and the CLI cannot drift apart again. Note it is #[ignore]d, following that file's existing convention — it is a manual harness against a live relay, not an unattended gate, so please do not read it as CI coverage.
  • rustfmt --check clean on all three changed files under the pinned 1.95.0 toolchain; lint, unit tests, the Windows build and the cross-compile matrix ran green on this exact tree in my own CI before opening.

No UI surface is touched.

The branch is based on f88cda9eb, which is behind main — none of the three files have been touched upstream since, so it applies cleanly. Happy to rebase on request.

…d:41001

`dms list` queried `{kinds:[41001], "#p":[me]}`. `KIND_DM_CREATED` (41001) is
declared in buzz-core but no code path in this repo ever emits it, so the verb
returned `[]` for every account on every relay while `channels list`, `channels
members` and `messages get` all saw the same conversation. An empty list rather
than an error is why this reads as "no DMs exist" instead of "my predicate
matched nothing".

The relay-emitted truth is kind:39000 NIP-29 group metadata, which
`emit_group_discovery_events` tags with `t=<channel_type>` plus one `p` tag per
participant for DMs. kind:39000 is absent from `required_scope_for_kind`'s
allowlist, so client ingest rejects it as "restricted: unknown event kind" —
only the relay can author one. `t=dm` is therefore relay-attested channel_type
rather than a client-supplied name/membership heuristic, which matters because
consumers skip the @-mention test on the DM path.

- `dm_list_filter()`: extracted so a regression to a kind nothing emits is a red
  test, not a silent empty list.
- `parse_query_events()`: a relay error object is an error, not an empty inbox.
  The old `unwrap_or_default()` made a dead relay indistinguishable from no DMs.
- `dms_from_group_metadata()`: keeps only `t=dm` with a non-empty `d` tag that
  lists us; a valueless `["t"]` tag no longer masks a later `["t","dm"]`; newest
  revision per channel wins; sorted newest first.
- Annotated `KIND_DM_CREATED` as reserved-and-unemitted.

Adds an end-to-end interop arm in buzz-test-client that pins the wire kind, so
the relay and the CLI cannot drift apart again.

Signed-off-by: lodar <markounik@gmail.com>
@lodar
lodar requested a review from a team as a code owner August 26, 2026 03:07

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the diagnosis is airtight and the trust paragraph is the best part — "kind:39000 is not in required_scope_for_kind's allowlist, so only the relay can author one, therefore t=dm is relay-attested rather than a client-supplied name" is exactly the argument that needed making, given that the DM path is what lets a consumer skip the @-mention test. extracting dm_list_filter so a regression to an unemitted kind is a red test rather than a silent [], and making parse_query_events treat a relay error object as an error, are both the right lessons to take from the bug rather than just fixing it.

two things.

--limit no longer counts what the user thinks it counts. the filter sends {"kinds":[39000], "#p":[me], "limit": n} and the t=dm filter runs client-side afterwards. so the relay's limit is applied to kind:39000 events before anything narrows to DMs, and dms list --limit 50 means "the newest 50 group-metadata events that p-tag me, of which some subset are DMs". today that's probably harmless — the pr says 39000 carries p tags "for DMs only", so #p already does most of the work — but that's a property of emit_group_discovery_events right now, not a guarantee, and the failure mode if it changes is silent truncation of the DM list rather than an error. worth either saying in the doc comment that #p is load-bearing for the limit's meaning, or over-fetching and truncating after the t=dm filter so the number the user passed is the number of DMs they get.

does the CLI's own read path accept a kind it can't author? the trust argument turns on required_scope_for_kind rejecting client-authored 39000 with "restricted: unknown event kind". if that check also runs over query results anywhere in BuzzClient::query or the SDK's ingest, the CLI would drop the very events this fix depends on. i assume it's an authoring gate only, since you measured this against a live relay — but the doc comment currently asserts the rejection without saying which direction it applies to, and that's the one sentence a future reader will lean on hardest. worth naming the function that enforces it and stating it's inbound-publish only.

smaller: "a valueless ["t"] tag no longer masks a later ["t","dm"]" is a good catch and exactly the kind of thing that deserves the unit test it presumably has. and annotating KIND_DM_CREATED as reserved-and-unemitted rather than deleting it is the right call — worth making that comment say why it's reserved (so a future relay change doesn't re-use the number) rather than only that it's unused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants