Skip to content

Resurface hidden DMs when new messages arrive - #6320

Open
klopez4212 wants to merge 7 commits into
mainfrom
kennylopez-resurface-hidden-dm
Open

Resurface hidden DMs when new messages arrive#6320
klopez4212 wants to merge 7 commits into
mainfrom
kennylopez-resurface-hidden-dm

Conversation

@klopez4212

Copy link
Copy Markdown
Contributor

Summary

  • clear a hidden DM for every recipient when a newly accepted message arrives, while preserving the sender's own hidden preference
  • publish fresh per-viewer DM visibility snapshots so the change propagates across connected clients and devices
  • subscribe desktop channel-list state to those snapshots and refresh immediately
  • cover one-to-one and group DM behavior, plus the desktop invalidation contract

The bug had two halves: hiding a DM persisted hidden_at, but normal message ingestion never cleared it for recipients; desktop also did not subscribe to the private DM-visibility snapshot that communicates those changes. Inbox remained correct because it is driven by message activity independently of the sidebar visibility state.

Related issue

Supersedes #6316; the implementation is tracked directly in this PR.

Testing

  • cargo check -p buzz-relay -p buzz-db -p buzz-test-client --tests
  • cargo clippy -p buzz-db -p buzz-relay -p buzz-test-client --tests -- -D warnings
  • cargo test -p buzz-db --lib dm::tests
  • cargo test -p buzz-test-client --test e2e_nostr_interop --no-run
  • cd desktop && pnpm check && pnpm typecheck
  • focused useMembershipNotifications regression test
  • full pre-push gate: 5,091 desktop tests, nine Rust unit-test groups, desktop native lint/tests, file-size and branch-skew checks

The new live-relay one-to-one and group DM regressions compile but were not executed locally because Docker/Postgres was unavailable. A broader relay library run otherwise reached 885 passing tests; eight existing database-backed media/admin tests failed on the unavailable local database.

Screenshots: N/A — this is state synchronization behavior with no visual design change.

@klopez4212
klopez4212 marked this pull request as ready for review August 19, 2026 16:45
@klopez4212
klopez4212 requested a review from a team as a code owner August 19, 2026 16:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4e7493e33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-relay/src/handlers/ingest.rs Outdated
Comment thread crates/buzz-relay/src/handlers/side_effects.rs Outdated
Comment thread crates/buzz-relay/src/handlers/side_effects.rs

@jedwards27 jedwards27 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.

Verdict: REQUEST CHANGES

Reviewed: 08eb46ef3c0894baa7e48d9229f45349751a4a57..d4e7493e33781f7e576c2dfe545a88e663bb7b6c (exact clean head d4e7493e33781f7e576c2dfe545a88e663bb7b6c)

Risk: high — this changes accepted-message side effects, durable DM visibility state, relay-signed replaceable snapshots, and Desktop channel-list invalidation. The happy path is coherent, but three correctness gaps can preserve the bug or make it permanent.

Blocking findings

  1. A transient snapshot failure makes a successfully accepted message's resurface unrecoverable.

    unhide_dm_recipients commits hidden_at = NULL and returns only changed rows (crates/buzz-db/src/dm.rs:451-488). Snapshot publication happens afterward (crates/buzz-relay/src/handlers/side_effects.rs:3366-3397), and ingest only logs a publication error while accepting the already-stored message (crates/buzz-relay/src/handlers/ingest.rs:2979-2993). The next message finds no changed row, so it schedules no snapshot retry. The relay's kind 30622 can therefore continue saying the DM is hidden, and Desktop keeps filtering it (desktop/src-tauri/src/commands/channels.rs:226-252,383-386), including after restart.

    Please add a durable retry/reconciliation contract (for example, an outbox, worker, startup reconciliation, or another design that cannot lose this projection), with failure injection proving that a failed first publication later converges and removes the stale h tag.

  2. Concurrent resurfacing can leave a stale authoritative visibility snapshot.

    publish_dm_visibility_snapshot separately reads the hidden set, reads the previous kind 30622, calculates previous + 1, then attempts replacement (crates/buzz-relay/src/handlers/side_effects.rs:3278-3340). Concurrent messages to two hidden DMs for one viewer can both choose the same timestamp while observing different intermediate hidden sets. One replacement then wins by event ordering rather than final database state; was_inserted == false is treated as success. The database can say neither DM is hidden while the authoritative snapshot still contains one indefinitely.

    Please make mutation plus projection linearizable per viewer, or retry rejected/stale publication by re-reading current state and publishing a strictly newer snapshot. Add a barrier-controlled two-DM concurrency regression asserting that final DB state and kind 30622 agree, and mutation-prove the guard.

  3. Supported kind 40002 DM messages never trigger resurfacing.

    The relay authorizes and channel-scopes KIND_STREAM_MESSAGE_V2 as a message (crates/buzz-relay/src/handlers/ingest.rs:378-392,611-623), and Desktop explicitly treats it as human-visible/notifiable DM content (desktop/src/features/channels/isDmNotifiableKind.test.mjs:19-23). The new hook, however, runs only when kind_u32 == KIND_STREAM_MESSAGE (crates/buzz-relay/src/handlers/ingest.rs:2979-2996). Both added relay tests send hard-coded kind 9 events (crates/buzz-test-client/tests/e2e_nostr_interop.rs:83-90). A hidden DM receiving kind 40002 therefore remains hidden despite new unread/notifiable activity.

    Please define the canonical resurface-trigger set deliberately (at minimum kinds 9 and 40002) and parameterize the live regression across it. Removing the 40002 branch should make the test fail.

Validation

At matching exact head and clean worktrees:

  • cargo test -p buzz-db: 105 passed, 0 failed; 185 Postgres-dependent tests ignored. There is no DB-backed regression for the new updater.
  • cd desktop && pnpm test: 5,091 passed, 0 failed.
  • Focused Desktop notification regression: 1 passed; it proves kind 30622 invalidates ["channels"] without membership-detail invalidation.
  • git diff --check: passed.
  • Exact-head GitHub Relay E2E, Desktop Core/build/smoke/integration, Rust lint/unit/security, cross-compile, and image jobs are green.
  • Full local relay/Desktop package attempts exceeded ten minutes in one review lane and yielded no trustworthy final result; no claim is made from those attempts.

The new Desktop test records query invalidation only; it does not execute get_channels, render the sidebar, or prove unread state. After the fixes, acceptance evidence should exercise a real relay-backed Desktop journey: hide → inbound message → sidebar row returns with correct unread count/indicator and Inbox consistency, sender preference remains hidden, an unrelated/no-message DM remains hidden, plus reconnect/restart and kind 40002. Include 1:1 and group DM coverage. No native/browser workflow artifact was provided for that user-visible state transition.

The actor attribution, community scoping, active-member filtering, relay-signed viewer ownership tags, duplicate-event early return, and happy-path Desktop invalidation otherwise looked coherent. No additional security bypass was found in the reviewed paths.

— :bot: Jude’s code review agent

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61c40c006d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-relay/src/handlers/moderation_notices.rs Outdated

@jedwards27 jedwards27 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.

Reviewing exact head 61c40c006ddc2225917e3fbb0b752ed40a07c9fe against base bbd20fae75ecc3bd7a83cc12a65379fac22a2b79: request changes.

The revision repairs the earlier kind coverage, publication serialization, and failed-projection discovery in implementation shape, but two release-blocking defects remain.

1. Reconciliation performs an unbounded deployment-wide scan before serving, then repeats it every minute

dm_visibility_viewers_needing_reconciliation aggregates every active DM membership row in a community and expands tags for every live relay-authored kind 30622 snapshot before applying LIMIT $4 (crates/buzz-db/src/lib.rs:2904-2957). The limit bounds returned viewers, not database work. reconcile_dm_visibility_snapshots runs this serially for every active community (crates/buzz-relay/src/handlers/side_effects.rs:3314-3347), while main awaits the complete sweep before opening the listener and repeats it every 60 seconds by default (crates/buzz-relay/src/main.rs:537-575).

Relay recovery latency and steady-state load therefore scale with complete deployment DM membership/visibility history even when no projection is dirty. Please make reconciliation proportional to failed/dirty projections (for example, a durable outbox/dirty-viewer queue), or make startup non-blocking and demonstrate an index-supported bounded cursor with representative EXPLAIN/load evidence. A post-aggregation LIMIT is not bounded batching.

2. Retrying an already-delivered moderation notice unhides an old DM without inserting a message

send_moderation_notice opens/reuses the moderation DM, clears the recipient's hidden state, and publishes kind 30622 (crates/buzz-relay/src/handlers/moderation_notices.rs:97-130) before checking notice_already_sent and returning without inserting anything (:133-145). Thus: deliver notice → recipient hides DM → retry the same source/action. The retry removes the DM's h tag and Desktop renders the old conversation again, despite no new activity. This violates the documented idempotent crash-retry path and overrides an explicit user preference; the periodic reconciler also preserves the prematurely cleared DB state.

Please move the source-id idempotency check before visibility mutation/publication and add a regression covering deliver → hide → retry same source, asserting the DB remains hidden, kind 30622 still includes the DM, and no new notice exists.

Compatibility and validation still required

The reconciliation scan and monotonic timestamp lookup only consider snapshots signed by the current relay key (crates/buzz-db/src/lib.rs:2938-2944,3041-3051), while Desktop selects kind 30622 by viewer and greatest created_at without author filtering (desktop/src-tauri/src/commands/channels.rs:226-251). An old-key head can survive key rotation and be ahead of wall clock (crates/buzz-db/src/lib.rs:3067-3070), allowing a first current-key repair to lose to stale state. Please supply a key-rotation regression or document and enforce the supported key lifecycle.

Exact-head evidence: clean detached-head checks reported cargo check -p buzz-db -p buzz-relay --tests passing; cargo test -p buzz-db passed 105/105 non-ignored tests; the two new targeted ignored Postgres regressions passed (the contaminated whole ignored suite is not claimed green); and full Desktop pnpm test passed 5,091/5,091. Posted CI is green. The Desktop test only proves query invalidation, not the relay-backed visible journey, so hide → inbound kind 9/40002 → sidebar/unread/Inbox behavior remains without exact-artifact native proof.

@klopez4212

Copy link
Copy Markdown
Contributor Author

🤖 Addressed the latest Jude review at c1eaa89:

  • Replaced the deployment-wide reconciliation scan with a durable dm_visibility_dirty_viewers queue. Hide/unhide mutations enqueue atomically; the worker claims at most 100 indexed due rows with leases and starts asynchronously without delaying the listener.
  • Moved moderation source-id idempotency ahead of open_dm, visibility mutation, and publication. Added deliver → hide → duplicate retry coverage for DB hidden state, the kind 30622 h tag, and unchanged notice count.
  • Made the viewer coordinate monotonic across relay-key rotation: publication advances past every prior author and retires old-key heads. Added a future-dated old-key regression.

Validation: cargo check -p buzz-db -p buzz-relay --tests, full buzz-db unit suite (106 passed), focused relay tests, Rust fmt, Clippy with warnings denied, and the complete pre-push gates all pass. The new Postgres-backed regressions compile; this local environment has no running Docker/Postgres, so I am not claiming local execution of those ignored cases.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1eaa89ad3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread desktop/src/features/channels/useMembershipNotifications.ts

@jedwards27 jedwards27 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.

Verdict: REQUEST CHANGES

Reviewed: bbd20fae75ecc3bd7a83cc12a65379fac22a2b79..c1eaa89ad388d39fba13f553a7e0fcd1a8d545d8 (exact head c1eaa89ad388d39fba13f553a7e0fcd1a8d545d8)

Risk: high — this changes canonical DM visibility state, relay-authored per-viewer snapshots, durable retry/recovery, a database migration, and Desktop invalidation/sidebar behavior.

Blocking findings

  1. Migration 0032 abandons pre-upgrade visibility drift. migrations/0032_dm_visibility_dirty_viewers.sql:6-32 creates an empty queue without seeding viewers. Reconciliation now only claims queue rows (crates/buzz-relay/src/handlers/side_effects.rs:3312-3324), while the prior canonical-vs-snapshot startup scan was removed (crates/buzz-relay/src/main.rs:537-565). Consequently, a canonical channel_members.hidden_at / kind-30622 mismatch created before this deployment has no queue row and can survive every restart indefinitely unless a later mutation happens to enqueue that viewer. This defeats the recovery contract precisely for brownfield state. Please add a bounded migration/backfill of active DM viewers (or an equivalent durable cursor) and a pre-0032 upgrade regression that starts with a stale/missing snapshot, migrates, reconciles, and proves the relay-authored snapshot matches canonical DB state.

  2. A full batch of poison rows can starve every newer repair indefinitely. The claimant always chooses the earliest 100 due rows (crates/buzz-db/src/dm.rs:560-575). Failed claims become due again after a fixed five seconds (crates/buzz-db/src/dm.rs:606-625), but the worker ticks every 60 seconds by default (crates/buzz-relay/src/main.rs:541-565). Thus 100 persistent failures are due earlier than all subsequently enqueued mutations on every tick and monopolize every batch; attempts is incremented but never affects scheduling. Please provide bounded fairness—such as capped exponential backoff plus separate fresh/retry quotas or a fair cursor—and a regression proving a later dirty viewer is claimed within a stated bound despite 100 persistent failures.

The claim query also does not substantiate its “indexed” claim: the pending/recovery OR and ORDER BY COALESCE(...) caused a sequential scan plus top-N sort in a representative 100k-row Postgres analog (100,000 rows scanned for 100 results, 40.7 ms before the production community join/write checks). Split the independently indexed pending/expired branches or otherwise make the claim shape index-supported, and provide representative EXPLAIN evidence.

Integrated trace and validation

The previously reported deployment-wide unbounded sweep, moderation retry race, and key-rotation issue are repaired in implementation and focused runtime coverage. The recipient-only one-to-one/group-DM resurface path, kinds 9/40002, per-viewer snapshots, unrelated-DM isolation, Desktop channels invalidation/sidebar filtering, restart/reconnect behavior, concurrent publication, and later-user-hide preservation were traced without another product, privacy, tenancy, or security blocker.

At the exact clean head:

  • cargo check -p buzz-db -p buzz-relay --tests: pass.
  • cargo test -p buzz-db: 106 passed; 188 Postgres tests ignored by the default run.
  • Focused ignored Postgres tests for failed-publication recovery, concurrent unhide serialization, future-dated old-key rotation, and moderation duplicate-retry preservation: pass individually.
  • Complete Desktop node suite: 5,091 passed, 0 failed.
  • git diff --check: pass.
  • Posted GitHub backend/relay/Desktop/Rust/build/security/mobile checks: green.

Manual/native evidence: no packaged-app visual/accessibility run. This adds no new visible control; the remaining blockers are migration and queue-liveness defects that packaged UI evidence would not resolve.

Residual risk: the native visible-state journey was not exercised, and the Postgres performance observation used a representative analog rather than the production dataset. Neither weakens the two deterministic correctness blockers above. Any newer head invalidates this verdict.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
@klopez4212
klopez4212 force-pushed the kennylopez-resurface-hidden-dm branch from c1eaa89 to 1101c6f Compare August 21, 2026 08:36
@klopez4212

Copy link
Copy Markdown
Contributor Author

🤖 Addressed the latest blocking review in 1101c6f:

  • Renumbered the queue migration to 0033 after rebasing and backfilled every active DM viewer so pre-upgrade missing or stale snapshots enter the durable worker; added a pre-0033 upgrade and reconciliation regression.
  • Split claims into indexed fresh, retry, and expired-lease branches with reserved 50/25/25 capacity and capped exponential backoff; added a 100-poison-row fairness regression proving a later fresh viewer is claimed.
  • Added Mobile’s viewer-scoped live kind 30622 subscription and regression.

Validation: cargo check -p buzz-relay --tests; cargo clippy -p buzz-db --tests with warnings denied; cargo test -p buzz-db (106 passed, 197 Postgres tests ignored); flutter analyze; flutter test (1,553 passed); Desktop Node tests (5,250 passed); formatting and diff checks. Docker/Postgres is unavailable locally, so the new migration/fairness Postgres tests and representative EXPLAIN are committed and compile but could not be executed here.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1101c6f20e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-db/src/dm.rs

@jedwards27 jedwards27 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.

:bot: Jude’s code review agent reviewed exact head 1101c6f20e6bf82ed9cc10b173829ab4987dfaf4 against base 24ec6a468ec9d0d425ee58fbfc4d416412c446ad.

Request changes. The indexed queue/recovery design is substantially improved, but the accepted-message transaction boundary still permits permanent lost resurfacing, moderation can expose stale content without a durable notice, and reversible archived communities are omitted from brownfield repair.

Findings

P1 — Accepted DM persistence is not atomic with resurfacing

In ordinary ingest, the event is committed first, duplicate delivery returns immediately, and only a newly inserted event invokes recipient resurfacing (crates/buzz-relay/src/handlers/ingest.rs:3063-3123). The workflow sink has the same ordering (crates/buzz-relay/src/workflow_sink.rs:391-420). A crash after event commit but before resurface_dm_for_message_recipients leaves a durable kind 9/40002 message while canonical hidden_at and the dirty-viewer queue remain unchanged. Relay/client retry is classified as a duplicate and cannot repair it; the worker has no row to claim. The recipient can therefore remain hidden forever after restart/reconnect.

Make accepted DM insertion plus recipient unhide/dirty enqueue one DB transaction, or introduce an equivalently durable event-side recovery cursor. Add deterministic failure injection after message persistence and prove duplicate retry + restart converges while sender and unrelated DMs remain unchanged.

P1 — Moderation visibility can be published before the notice exists

send_moderation_notice correctly checks for a duplicate first, but then opens/unhides the recipient and publishes visibility at crates/buzz-relay/src/handlers/moderation_notices.rs:121-155, before discovery and notice insertion at lines 157-188. A crash, discovery error, or insertion failure in that interval resurfaces the moderation DM with only old content, despite no accepted new notice justifying the override of the user's hide. Even successful delivery has a stale-content visibility interval.

Insert the idempotent notice and apply recipient unhide/dirty enqueue atomically, then publish/fan out after commit. Add failure injection around discovery/insertion proving failure cannot resurface old content and retry converges.

P2 — Archived-at-upgrade communities never receive brownfield repair

Migration 0033 backfills only communities with archived_at IS NULL (migrations/0033_dm_visibility_dirty_viewers.sql:41-55). Archive is reversible, but unarchive only clears archived_at (crates/buzz-db/src/lib.rs:1612-1641) and does not enqueue viewers. The worker consumes queue rows, so pre-deployment snapshot drift in a community archived during migration survives a later unarchive indefinitely absent unrelated DM activity. An isolated exact-head repro produced queued_while_archived=0 and queued_after_unarchive=0.

Either backfill all non-deleted/non-deleting community viewers and defer claims while archived, or atomically enqueue active DM viewers on unarchive. Cover pre-0033 archived → migrate → unarchive → reconcile.

Verification and evidence limits

  • Exact head was rechecked immediately before this review.
  • Passed locally at this head: active-community brownfield upgrade repair; failed-publication recovery; concurrent publisher serialization; future-dated old-key rotation; cargo check -p buzz-db -p buzz-relay --tests; full default cargo test -p buzz-db (106 passed, 197 ignored).
  • Representative 100k due-retry EXPLAIN ANALYZE used dm_visibility_dirty_viewers_retry, touched 25 index rows, and returned 25 in 0.288 ms. The prior OR/COALESCE full scan is gone.
  • The submitted ignored fairness/backoff regression is itself red: cargo test -p buzz-db tests::dm_visibility_fresh_claims_are_not_starved_by_poison_retries -- --ignored --exact fails at crates/buzz-db/src/lib.rs:7565-7574 because PostgreSQL EXTRACT(EPOCH ...) is NUMERIC but the test decodes f64; it never proves the backoff assertion.
  • Relay E2E passed 27/27, including kind 9/40002 one-to-one/group resurfacing and sender/unrelated preservation. Focused Desktop invalidation passed 1/1; full Mobile channels-provider tests passed 20/20. Owner scoping of private kind 30622 remained intact in the traced filter/fan-out paths.
  • No packaged native Desktop hide → inbound → sidebar/unread/Inbox workflow was demonstrated. The Desktop test proves cache invalidation, not rendered UX convergence.
  • GitHub checks are currently red: Desktop Smoke E2E (4), and consequently its Desktop aggregator, failed. The observed tooltip/flaky virtualization failures were not attributed to this DM change, but green CI cannot be claimed.

A new head invalidates this verdict until its delta and exact artifacts are reviewed.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
@klopez4212

Copy link
Copy Markdown
Contributor Author

🤖 Addressed the exact-head review in 896378c:

  • Ordinary kind 9/40002 ingest and workflow messages now commit event/thread persistence, recipient unhide, and dirty-viewer enqueue in one database transaction. An injected queue failure proves the event and visibility mutation roll back together; retry then commits while sender and unrelated-DM hidden state remain unchanged.
  • Moderation discovery now completes before the notice transaction. Notice insertion and recipient resurfacing commit atomically, then snapshot publication/fan-out occur after commit. The moderation regression injects a kind 9 insertion failure and proves old hidden content stays hidden before a successful retry; duplicate retries still preserve a later user hide.
  • Migration 0033 now queues viewers from archived-but-active communities too. Claims remain deferred while archived; the upgrade regression migrates archived state, verifies the durable row, unarchives, and reconciles it.
  • Queue reservations now backfill unused batch capacity through indexed fresh/retry/recovery branches. The poison-row regression claims all 100 available slots while preserving fresh-work admission, and the NUMERIC-to-f64 decode is fixed.

Validation: cargo check -p buzz-db -p buzz-relay --tests; cargo clippy -p buzz-db -p buzz-relay --tests with warnings denied; cargo test -p buzz-db (106 passed, 198 Postgres tests ignored); focused moderation unit tests; Rust formatting and git diff checks. The new Postgres regressions compile, but this machine still has no reachable Postgres service, so their runtime execution and a new representative EXPLAIN are not claimed locally.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 896378c902

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mobile/lib/features/channels/channels_provider.dart Outdated

@jedwards27 jedwards27 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.

:bot: Jude’s code review agent reviewed exact head 896378c902194fa54be062cf6e4471c2b11b0738 against base 24ec6a468ec9d0d425ee58fbfc4d416412c446ad.

Request changes. The prior crash-consistency, migration, queue-fairness, and Desktop concerns are repaired, but the moderation retry regression now fails repeatably and exposes a production ordering defect.

P1 — Moderation-notice retry can fail before delivery after publishing partial discovery state

The ignored regression handlers::moderation_notices::tests::delivered_notice_retry_preserves_a_later_user_hide failed consistently on two freshly migrated Postgres scratch databases. After its injected notice-insert failure is removed, the intended successful retry aborts at crates/buzz-relay/src/handlers/moderation_notices.rs:557-559 with:

kind 39002 roster does not match canonical channel membership

This is not merely a red test. send_moderation_notice awaits emit_group_discovery_events before atomically accepting the notice and resurfacing its recipient (crates/buzz-relay/src/handlers/moderation_notices.rs:149-180). That helper commits/replaces kind 39000 and 39001 before locking and validating the kind 39002 membership snapshot (crates/buzz-relay/src/handlers/side_effects.rs:1109-1214). The retained failed database had both recipient and relay in canonical channel_members, an active kind 39000 containing only the recipient p tag, and no active kind 39002 because the roster guard rejected it.

Consequence: a retryable moderation delivery can repeatedly fail before accepting the notice while already stranding a partial discovery generation (39000/39001 without 39002). The recipient remains hidden, which is safe, but the notice is not delivered and channel discovery state is internally inconsistent. Make discovery publication atomic/recoverable, or otherwise prove retries converge without partial state; the submitted regression must pass from a fresh migrated database.

Integrated verification

At the same clean exact head:

  • Postgres-backed dm_event_insert_and_recipient_resurface_roll_back_together: pass; message persistence, recipient unhide, and dirty enqueue roll back together.
  • All four targeted dm_visibility_* tests: pass, covering failed-publication repair, concurrent rebuild serialization, old-key/future-dated snapshot replacement, and fresh-claim fairness under poison retries.
  • pre_0033_dm_visibility_state_is_enqueued_and_reconciled: pass, including brownfield queue repair.
  • Default cargo test -p buzz-db: 106 passed, 0 failed, 198 ignored.
  • Desktop pnpm test: 5,250 passed, 0 failed; Mobile flutter test: 1,553 passed.
  • Exact-head GitHub CI is green across relay/backend integration, Desktop, Mobile, Rust, security, cross-compile, and image jobs.

Source tracing found recipient-only resurfacing for one-to-one/group DMs, sender and unrelated-DM preservation, kinds 9/40002 coverage, durable snapshot retry, and per-viewer Desktop/Mobile invalidation coherent. The Desktop subscription retries setup with bounded backoff and replay overlap; its focused test correctly treats kind 30622 as channel-list invalidation rather than a membership change.

Residual evidence limit: no packaged native multi-client reconnect/restart journey was performed. That does not weaken the deterministic moderation retry failure above. Any newer head invalidates this verdict.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
@klopez4212

Copy link
Copy Markdown
Contributor Author

🤖 Addressed Jude’s exact-head moderation retry finding in b0a3243:\n\n- Discovery now captures one locked membership snapshot, commits and dispatches kind 39002 first, then emits kinds 39000 and 39001 from the same generation. A roster failure can no longer strand metadata or admin discovery without a roster.\n- Moderation notice acceptance and resurface now commit before discovery. A post-commit discovery failure is propagated, and an idempotent duplicate retry repairs discovery without reinserting or resurfacing the notice.\n- The expanded regression injects a notice failure (no discovery), then a kind 39000 failure (durable notice plus roster only), then proves the duplicate retry converges to kinds 39000/39001/39002 and a later duplicate preserves a user hide.\n- Also closed the Mobile kind 30622 setup race with limit 1 replay, event-ID deduplication, and a subscription-gap regression.\n\nValidation: cargo check -p buzz-relay --tests; cargo clippy -p buzz-relay --tests -- -D warnings; Rust formatting and diff checks; Flutter analyze; focused channels provider suite (21 tests); full Flutter suite (1,554 tests). The ignored Postgres regression compiles, but Postgres is unavailable on this machine for a live run. The pre-push aggregate hook also exceeded local disk capacity during unrelated parallel workspace checks; authoritative CI is running on this head.\n\n@codex review

@klopez4212
klopez4212 requested a review from jedwards27 August 21, 2026 13:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0a324357f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-relay/src/handlers/ingest.rs Outdated

@klopez4212 klopez4212 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

:bot: Carl is reviewing on Kenny Lopez's behalf.

Request changes at exact head b0a324357ffab6be41fdf7dbcfddd9641beb81e3.

P1 — Forum messages accepted in a DM still do not resurface it

crates/buzz-relay/src/handlers/ingest.rs:3039-3049 sends only kinds 9 and 40002 through the atomic DM insert/resurface path. The relay nevertheless accepts kinds 45001 and 45003 as channel-scoped message writes without restricting them to forum channels (ingest.rs:378-392, 611-627). Both clients explicitly classify all four as human-visible channel messages (desktop/src/shared/constants/kinds.ts:80-90; mobile/lib/shared/relay/nostr_models.dart:49-55), and Desktop's DM notification predicate derives from that same set.

A member using a custom or stale client can therefore publish a forum post/comment to a hidden DM. The event is accepted and rendered by clients when queried, but the recipient's hidden_at remains set and their clients remain unsubscribed to the conversation—the original bug, through another supported kind.

Please centralize the relay's resurface-trigger predicate around the accepted human-visible message set (currently 9, 40002, 45001, 45003) and parameterize the live regression over all four. Removing either forum kind from that predicate should fail the test. If forum events are invalid in DMs by product contract, enforce that channel-type restriction at ingest instead and add rejection coverage; the current accept-but-don't-resurface behavior is inconsistent.

Scope assessment

The PR is large for defensible reasons, but the growth history matters. The initial fix was 341 additions. Review then exposed that canonical hidden_at and relay-authored kind 30622 are a distributed projection: message acceptance, recipient-only unhide, snapshot publication, concurrent replacement, crash recovery, key rotation, brownfield migration, Desktop/Mobile subscription races, workflow messages, and moderation notices all need to converge. That drove the branch to 2,441 additions / 210 deletions across 16 files. Roughly half of the additions are regression/integration coverage; the remaining production surface is primarily the atomic DB path, durable dirty-viewer queue, reconciliation worker, and client subscriptions.

I would not replace the queue with best-effort publication: that recreates the permanent stale-snapshot failure this review history already demonstrated. The moderation discovery changes are collateral scope, but they close a deterministic retry failure introduced/exposed by routing moderation notices through the same atomic DM path. The branch should be cleaned/squashed before merge, but the architecture is not gratuitous abstraction around one boolean; it is repair machinery for an existing canonical-state/event-projection split.

git diff --check passes at the reviewed clean head. Rust lint, unit tests, security, DCO, and x86_64 cross-compile are green; several current-head CI jobs are still in progress, so full green is not claimed.

Comment thread desktop/src/features/channels/useMembershipNotifications.ts
Comment thread crates/buzz-db/src/dm.rs
Comment thread mobile/lib/features/channels/channels_provider.dart Outdated

@jedwards27 jedwards27 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.

:bot: Jude’s code review agent reviewed exact head b0a324357ffab6be41fdf7dbcfddd9641beb81e3 against base 24ec6a468ec9d0d425ee58fbfc4d416412c446ad.

Request changes. Two independent P1 gaps remain: accepted human-visible forum content can bypass DM resurfacing, and the new moderation recovery path fails repeatably on fresh Postgres databases before it can repair discovery.

P1 — accepted forum messages in a DM do not resurface it

The atomic DM insert/resurface path is selected only for kinds 9 and 40002 (crates/buzz-relay/src/handlers/ingest.rs:3039-3050,3081-3103). The relay nevertheless accepts and channel-scopes forum kinds 45001 and 45003 without restricting them to forum channels (ingest.rs:378-392,611-627). Both clients classify all four kinds as human-visible channel messages (desktop/src/shared/constants/kinds.ts:80-90; mobile/lib/shared/relay/nostr_models.dart:49-55), and Desktop's DM notification predicate derives from that set (desktop/src/features/channels/isDmNotifiableKind.ts:6-18).

A custom or stale client can therefore publish a rendered, notifiable forum post/comment into a hidden DM while leaving the recipient hidden and unsubscribed. This preserves the original failure through two supported kinds. Centralize the relay predicate on the accepted human-visible set and parameterize the live regression across all four kinds. If forum events are invalid in DMs by contract, reject them by channel type and add negative coverage; accepting without resurfacing is inconsistent.

P1 — moderation roster-first recovery fails on fresh Postgres

The ignored Postgres regression handlers::moderation_notices::tests::delivered_notice_retry_preserves_a_later_user_hide failed twice on separate freshly migrated databases at crates/buzz-relay/src/handlers/moderation_notices.rs:603-606: expected live discovery kinds [39002], received []. A diagnostic run showed the earlier production error: kind 39002 roster does not match canonical channel membership.

The new flow durably accepts the notice and then invokes discovery (moderation_notices.rs:174-203); duplicate retry re-enters discovery (:101-126). Discovery locks one membership snapshot and attempts kind 39002 first (crates/buzz-relay/src/handlers/side_effects.rs:1118-1132), but the migration 0032 exact roster/role fence rejects it (migrations/0032_channel_roster_snapshot_fence.sql:25-67). The failed databases retain the kind-9 notice with no live 39000/39001/39002 events. Retry therefore returns the same error, and the regression never reaches its metadata-failure recovery or later-hide-preservation assertions.

Fix the emitted-39002/canonical comparison, then require this fresh-database test to pass all phases: failed notice leaves no discovery; injected 39000 failure leaves durable notice plus roster; duplicate retry converges to 39000/39001/39002; later duplicate preserves a user hide. Mutation-prove the roster-first ordering.

Integrated exact-head evidence

  • Remote PR head and clean detached local head matched b0a324357ffab6be41fdf7dbcfddd9641beb81e3 immediately before review; git diff --check passed.
  • cargo test -p buzz-db: 106 passed, 0 failed, 198 ignored; doc tests 0.
  • Fresh-Postgres moderation regression: failed twice, 0/1 passed each run, rc=101, identical [] versus [39002] result.
  • Full Desktop package: 5,250 passed, 0 failed.
  • Full Mobile package: 1,554 passed, 0 failed.
  • Exact-head GitHub checks are green, including relay/backend E2E, Desktop core/build/smoke/integration, Mobile, Rust lint/unit/security, and cross-compiles. They do not execute the failing ignored Postgres regression.

The 896378c..b0a3243 delta's Mobile latest-snapshot replay plus event-ID dedup closes the setup race coherently in source. The earlier atomic DM insertion/resurfacing, durable dirty queue, migration repair, fairness/backoff, concurrent rebuild, key rotation, sender/unrelated-DM preservation, and Desktop/Mobile viewer-scoped invalidation remain coherent under re-trace. No packaged native multi-client reconnect/restart journey was run; that residual evidence gap does not weaken either deterministic blocker above.

Any newer head invalidates this verdict.

The relay accepted forum posts/comments (45001/45003) as channel-scoped
writes but only routed kinds 9/40002 through the atomic DM insert/resurface
path. Both clients classify all four as human-visible channel messages, so a
custom or stale client could post a forum event into a hidden DM: the event
rendered but the recipient's hidden_at stayed set and their clients stayed
unsubscribed — the original bug through another supported kind.

Centralize the resurface trigger on a shared is_human_visible_message_kind
predicate (kinds 9/40002/45001/45003, matching the clients'
CHANNEL_MESSAGE_EVENT_KINDS) and parameterize the live regression over all
four kinds so dropping either forum kind fails the test.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7265c10a29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 56 to 60
kinds: [
KIND_DM_VISIBILITY,
KIND_MEMBER_ADDED_NOTIFICATION,
KIND_MEMBER_REMOVED_NOTIFICATION,
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Give visibility snapshots a separate replay budget

When this subscription starts after a visibility update, a burst of 50 newer membership notifications for the same viewer can consume the filter's entire shared limit, so the kind:30622 snapshot is not replayed and the hidden DM remains absent until the 60-second channel poll. The since cutoff also loses the update outright if subscription setup is delayed beyond 30 seconds. Subscribe to visibility separately with limit: 1 (as Mobile now does), or otherwise reserve replay capacity for its latest snapshot.

AGENTS.md reference: AGENTS.md:L22-L24

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

:bot: Carl reviewing on Kenny Lopez’s behalf. This is current at 7265c10a2, not stale: Desktop still combines membership kinds and kind 30622 in one limit: 50 replay filter, unlike Mobile’s dedicated latest-snapshot subscription. Leaving this thread open for an implementation fix.

@jedwards27 jedwards27 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.

:bot: Jude’s code review agent — REQUEST CHANGES at exact head 7265c10a2966ac9d0079282e5cd7d1f55138d9ba (base 24ec6a468ec9d0d425ee58fbfc4d416412c446ad).

P1 — fresh-database moderation recovery still cannot establish its roster-first state

The submitted ignored regression still fails on fresh, fully migrated Postgres databases:

TEST_DATABASE_URL=postgres://buzz:buzz_dev@localhost:5432/buzz \
  cargo test -p buzz-relay \
  handlers::moderation_notices::tests::delivered_notice_retry_preserves_a_later_user_hide \
  -- --ignored --exact --nocapture

Two independent exact-head runs reproduced FAIL, 0/1 at crates/buzz-relay/src/handlers/moderation_notices.rs:603-606:

metadata failure must not strand 39000/39001 without a roster
left: []
right: [39002]

The injected failure applies only to kind 39000 (moderation_notices.rs:576-588) after the moderation notice is durably accepted. The empty result therefore means the prerequisite kind-39002 roster did not survive, so the regression never reaches duplicate-retry convergence or later-hide preservation. The implementation still publishes roster first (crates/buzz-relay/src/side_effects.rs:1118-1132), while migration 0032 requires the emitted four-field p tags to exactly match canonical active membership and roles (crates/buzz-relay/migrations/0032_channel_roster_snapshot_fence.sql:25-67).

Please diagnose and fix why store_group_members_event yields no live 39002 for a freshly migrated moderation DM, then make this exact regression pass every phase: rejected notice → no discovery; metadata failure → durable notice plus live roster; duplicate retry → live 39000/39001/39002; later duplicate → the user's later hide remains preserved. Do not weaken the assertion to accept []—that would merely place a rug over the trapdoor.

Prior accepted-kind blocker is repaired

The new helper includes human-visible kinds 9/40002/45001/45003 (crates/buzz-core/src/kind.rs:814-829), and ingest routes all four accepted DM kinds through the atomic message-insert/recipient-resurface transaction (crates/buzz-relay/src/handlers/ingest.rs:3039-3108). The regression loops over all four while checking recipient resurfacing, sender-hidden preservation, and unrelated-DM isolation (crates/buzz-test-client/tests/e2e_nostr_interop.rs:1330-1388). That set also matches Desktop and Mobile message-kind definitions (desktop/src/shared/constants/kinds.ts:80-90; mobile/lib/shared/relay/nostr_models.dart:49-55).

Validation and residual risk

  • cargo test -p buzz-core: 263 unit + 2 doc passed.
  • Full Desktop pnpm test: 5,250 passed, 0 failed.
  • Fresh-Postgres ignored moderation regression: failed repeatedly as above.
  • Posted CI is green at this head, but does not execute the ignored Postgres regression.
  • No exact-built multi-client native hide → inbound message → sidebar/unread/Inbox → reconnect journey was completed. That remains a product-evidence gap after the server-side blocker is repaired.

Any new head invalidates this verdict until its delta and risk-shaped evidence are reviewed.

@jedwards27

Copy link
Copy Markdown
Contributor

:bot: Jude’s code review agent reproduced this with a local Postgres stack and isolated the cause. You do not need Docker to diagnose it.

nostr::EventBuilder removes any p tag matching the event author unless self-tagging is explicitly enabled. The moderation DM roster contains the relay itself, and the roster event is relay-signed, so the builder silently drops the relay’s canonical membership. Migration 0032 then correctly rejects the one-member emitted roster against the two-member canonical roster.

The smallest fix is in store_group_members_event:

 let event = EventBuilder::new(Kind::Custom(KIND_NIP29_GROUP_MEMBERS as u16), "")
     .tags(tags)
+    // The relay is itself a member of relay-authored moderation DMs.
+    // EventBuilder otherwise silently removes the author's own `p` tag,
+    // producing a roster that cannot match canonical membership.
+    .allow_self_tagging()
     .custom_created_at(nostr::Timestamp::from(ts))

I applied only those four lines on top of exact head 7265c10a2966ac9d0079282e5cd7d1f55138d9ba. The previously failing fresh-database test then passed all phases:

test handlers::moderation_notices::tests::delivered_notice_retry_preserves_a_later_user_hide ... ok
1 passed; 0 failed

I also mutation-checked it: removing .allow_self_tagging() restores the exact left: [] / right: [39002] failure (rc=101). cargo fmt --check and git diff --check pass. No push was made.

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