You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A large cluster of issues are all symptoms of one missing contract: the inbound message / convergence pipeline is not designed to do incremental, bounded, non-blocking work. Instead, per-event and per-pass code recomputes derived state from scratch, runs unbounded queries, blocks the critical path on long I/O, and — worse — computes the same logical quantity (ordering keys, derived counts, parsed routes) inconsistently in more than one place, which turns a performance smell into data loss.
This issue tracks one pipeline contract: define what state the receive/convergence path maintains incrementally, what work may run on the per-event critical section, and which derived quantities have a single canonical source.
marmot-protocol/darkmatter#692 — re-recording an already-stored CHAT forces a full per-group timeline rebuild.
marmot-protocol/darkmatter#704 — unread_summary_tx runs a redundant COUNT(*) scan in addition to the full per-row mention scan over the identical window.
The architecture docs describe the inbound/convergence pipeline as incremental + bounded + non-blocking, and name the canonical derived-state owners.
Lag recovery replays a bounded, watermark-keyed window; no limit: None history query remains on the hot path.
Convergence replay does not re-replay shared path prefixes or re-serialize the full seen set per pass (benchmarks or complexity tests demonstrate sub-quadratic behavior).
Relay-URL parsing, unread/mention counts, and per-message directory/group/profile lookups are computed once per window, not per endpoint/row/event.
Relay I/O, full resync, and idempotency fsync no longer execute on the per-event/per-command critical section; a test or trace shows independent events are not head-of-line blocked.
Fixed individually these read as a dozen unrelated perf nits — but #439 and #435 prove the same root cause also drops messages and strands convergence work. The unifying fix is not "optimize each query"; it is to give the receive/convergence path one contract (incremental, bounded, off-critical-path, single-source-of-truth for shared quantities) so new pipeline code inherits the property instead of reintroducing the next variant.
Summary
A large cluster of issues are all symptoms of one missing contract: the inbound message / convergence pipeline is not designed to do incremental, bounded, non-blocking work. Instead, per-event and per-pass code recomputes derived state from scratch, runs unbounded queries, blocks the critical path on long I/O, and — worse — computes the same logical quantity (ordering keys, derived counts, parsed routes) inconsistently in more than one place, which turns a performance smell into data loss.
This issue tracks one pipeline contract: define what state the receive/convergence path maintains incrementally, what work may run on the per-event critical section, and which derived quantities have a single canonical source.
Child issues / concrete symptoms
Redundant recomputation (cost):
limit: None) on every lag event #444 — broadcast-lag inbound replay re-queries the entire per-account/group history (limit: None) on every lag event.seen_message_idsset into a freshBTreeSet<String>on every pass #436 — convergence re-hex-serializes the entireseen_message_idsset into a freshBTreeSet<String>on every pass.routes_for/endpoints_matchre-parses both relay URLs (doubleRelayUrl::parse) per stored endpoint, per inbound event, on the slow path #414 —routes_for/endpoints_matchre-parses both relay URLs per stored endpoint, per inbound event.unread_summary_txruns a redundantCOUNT(*)scan in addition to the full per-row mention scan over the identical window.Head-of-line blocking (critical-path I/O):
send_finalidempotency persistence does blockingstd::fs+ doublefsyncon the async send hot path #417 —send_finalidempotency persistence does blockingstd::fs+ doublefsyncon the async send hot path.Consistency failures (same quantity computed two ways → data loss / liveness gaps):
Proposed unifying solution
Define one inbound/convergence processing contract with three guarantees:
seen_message_ids, parsed route tables) is maintained incrementally and cached, not rebuilt per event or per pass. Convergence replay memoizes path prefixes rather than re-replaying them (Encrypted-media upload: fail over across the group's Blossom endpoints #635), reuses a stable encodedseenset (CLI: expose dm groups avatar-url set/show for the group avatar-url component #636), parses relay URLs once into a cached structure (Create local files, sockets, and on-disk artifacts restrictive-by-construction (#378) #698), updates counts incrementally (Reject commits whose resulting GroupContext strips required app components #704), skips no-op CHAT rebuilds ([TUI] Invites don't show new groups automatically #692), and batches the per-message lookups into the loaded window instead of N+1 (App messages invalidated by convergence silently disappear from the sender's own view #639).limit: Nonehistory scans on the hot path; lag recovery replays a bounded window keyed off the watermark (Required convergence-policy field policy_version is absent from the engine policy schema #609). Queries that already touch a window derive their aggregates from that same window.fsync/std::fspersistence ([TUI]/chats newcommand should accept quoted multi-word strings for group names #691) — runs off the per-event/per-command critical path (dedicated tasks, async I/O, or deferred scheduling) so one slow item cannot stall unrelated events.Acceptance criteria
limit: Nonehistory query remains on the hot path.seenset per pass (benchmarks or complexity tests demonstrate sub-quadratic behavior).fsyncno longer execute on the per-event/per-command critical section; a test or trace shows independent events are not head-of-line blocked.Why this should be tracked holistically
Fixed individually these read as a dozen unrelated perf nits — but #439 and #435 prove the same root cause also drops messages and strands convergence work. The unifying fix is not "optimize each query"; it is to give the receive/convergence path one contract (incremental, bounded, off-critical-path, single-source-of-truth for shared quantities) so new pipeline code inherits the property instead of reintroducing the next variant.
Additional child issues (2026-06-30 review pass)
transport_group_idtriggers an O(groups) full MlsGroup-load scan per inbound message (attacker-paced amplification) #408 — unknowntransport_group_idtriggers an O(groups) full MlsGroup-load scan per inbound eventreplay_missed_inboundloads ALL group history into memory on every broadcast-lag event #407 —replay_missed_inboundloads all group history into memory on broadcast lagroutes_foris O(accounts × groups) with URL parsing per inbound event #405 —routes_foris O(accounts × groups) with per-event URL parsingretainover the delivery batch on the push-gossip path #404 — O(n²)retainover the delivery batch on the push-gossip pathpublic_directory_usersis an unbounded 2N+1 query that loads the whole directory cache into memory #403 —public_directory_usersis an unbounded 2N+1 that loads the whole directory cache (partial / related)load_account_projection_stateissues an N+1 component query per group #402 —load_account_projection_stateissues an N+1 component query per group (partial / related)Generated by Claude Code