diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md
index ef1868508..f59c8fd5e 100644
--- a/LOTUS_TODO.md
+++ b/LOTUS_TODO.md
@@ -67,35 +67,30 @@ Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then gr
Agent-surveyed + TPVR-verified correctness / a11y / tech-debt fixes (DP1βDP18) are implemented, review-fixed, and gate-green (tsc + eslint + 737 tests + build). Verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md) Β§R, then remove this note. Full detail in git history β commits `8eb961b6` `db864326` `6cf18c3b` `165714e1` `8c0e2b42` `e545706c` `b1ee3ada` `4fc3f7a3` `101e4116` `4fa4327a` `c2598d21`.
-### π Discovery pass 2 (2026-07) β perf / security / correctness (agent-surveyed, single-pass β NOT yet TPVR'd)
+### β
Discovery pass 2 (2026-07) β perf / security / correctness β DONE
-Three agents swept fresh lenses (performance, security/privacy, correctness in under-covered subsystems); each verified its own findings against the code + SDK. `Sev`/`Eff` = S/M/L. Confirm before fixing (no independent TPVR yet).
+Agent-surveyed findings, each **verified against the code before fixing**, then implemented and gate-green (tsc + eslint + prettier + 857 tests + build), with **two review agents on every staged diff before commit**. Verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then remove this note.
-**Performance / rendering:**
+- [x] **PERF-1 β presence: 3 client listeners PER avatar β one shared presence store** (3 listeners total). `8a154051`.
+- [x] **PERF-2 β `#`-mention autocomplete mutated + re-sorted the shared `allRoomsAtom` every keystroke** β copy + `useMemo` (also fixed a real shared-array mutation hitting ~27 consumers). `4708a179`.
+- [x] **PERF-3 β read-receipt rows: ~6 global `Members` listeners per row β one shared member-change store** (`useRoomMemberChange`). `1b8f5545`.
+- [x] **PERF-4 β message-search room filter re-sorted every render** β `useMemo`. `4708a179`.
+- [x] **PERF-5 β DM-preview `Decrypted` listener ran for every nav item** β gated on `direct`. `4708a179`.
+- [x] **SEC-1 / SEC-2 β scheduled-message plaintext + recent searches survived logout** β `clearPlaintextCaches()` on both logout paths, extended to the whole `recent_*` family + nav-paths. `726cefb5`.
+- [x] **SEC-3 β `window.open(_blank)` without `noopener`** β `noopener,noreferrer` at 5 sites (SSOStage excluded β needs the handle). `3e1106b2`.
+- [x] **SEC-4 β `/acl` self-lockout footgun** β shared `serverAcl.ts` validation, no-brick allow default, fail-closed self-ban guard. `3e1106b2`.
+- [x] **COR-1 β space-child UNLINK over-deleted** β targeted `UNLINK` reducer action. `fd3b8b42`.
+- [x] **COR-2 β `useCallJoined` stuck true on 2nd-call embed swap** β re-seed on `[embed]`. `1f80d1d1`.
+- [x] **COR-3 β incoming-call lifetime guard only corrected future clock-skew** β `Math.abs(...)` (Β±20s). `ab01d27a`.
+- [x] **COR-4 β shared notify-dedupe slot double-notified** β key by `roomId|threadId`. `1f80d1d1`.
+- [x] **COR-5 β upload cancel ignored during retry back-off** β `AbortSignal` threaded into the retry loop. `ab01d27a`.
+- [x] **COR-6 β `CallControl.forceState` dropped `screenshareAudioMuted`** β passes it. `ab01d27a`.
-- [ ] **PERF-1 β Presence: 3 global client listeners registered PER avatar (Sev High Β· Eff M).** `hooks/useUserPresence.ts:43-53` (via `PresenceRingAvatar.tsx:19`, `MembersDrawer.tsx:142`) β every timeline/member avatar adds `mx.on` for `Presence`/`CurrentlyActive`/`LastPresenceTs` β 100β250 global listeners; each presence event fans out across all, and fast scroll churns add/remove per Message mount. Fix: one shared presence store (context + `useSyncExternalStore` or a jotai `atomFamily`) with 3 listeners total; components select by userId.
-- [ ] **PERF-2 β `#`-mention autocomplete re-sorts AND mutates the room list every keystroke (Sev Med Β· Eff S).** `components/editor/autocomplete/RoomMentionAutocomplete.tsx:83` β `useAtomValue(allRoomsAtom).sort(factoryRoomIdByActivity(mx))` is unmemoized (O(N log N) `mx.getRoom` per compare, N=all joined rooms, per keypress) **and `.sort()` mutates the shared `allRoomsAtom` array in place.** Fix: `useMemo(() => [...rooms].sort(...), [rooms, mx])`.
-- [ ] **PERF-3 β Read-receipt rows attach per-instance global `RoomStateEvent.Members` listeners (Sev Med Β· Eff M).** `components/read-receipt-avatars/ReadReceiptAvatars.tsx:74` + `hooks/useMemberAvatar.ts:44` β up to ~6 global `Members` listeners per receipt row; fires on any membership/name/avatar change in any room. Fix: one shared room-member-change subscription fanning out by roomId+userId (shares with PERF-1).
-- [ ] **PERF-4 β Message-search room filter re-sorts full room list every render (Sev LowβMed Β· Eff S).** `features/message-search/SearchFilters.tsx:155` β `Array.from(...).sort(factoryRoomIdByAtoZ(mx))` outside `useMemo` (re-sorts as the user types). Fix: `useMemo`.
-- [ ] **PERF-5 β DM-preview hook subscribes a global `Decrypted` listener for EVERY nav item incl. non-DMs (Sev LowβMed Β· Eff S).** `hooks/useRoomLatestRenderedEvent.ts:64-67` (called at `RoomNavItem.tsx:681`) β preview is only used for `direct` rooms but the hook + its `MatrixEventEvent.Decrypted` listener run for all; wasteful, and fans out across all during bulk decryption. Fix: gate the hook/subscription on `direct`.
-- [ ] **PERF-6 β Avatar-decoration profile-fetch on first render of large member/timeline views (Sev Low Β· Eff M, likely leave).** `hooks/useAvatarDecoration.ts` β one `/profile/{userId}` per new user (well-guarded: module cache + in-flight dedupe + backoff). Network/HS-load note only; batch/skip only if it proves costly.
+**Deferred / decided (not built):**
-**Security / privacy:** _(no exploitable XSS found β `sanitize.ts` strips `javascript:`, forces mxc `
` + `rel=noopener` ``; embeds use host-allowlist + origin-scoped `postMessage`; KaTeX `trust:false`. These are storage-hygiene / URL items.)_
-
-- [ ] **SEC-1 β Scheduled-message plaintext persists in `localStorage`, survives logout (Sev Med Β· Eff S).** `state/scheduledMessages.ts:12-19` β full `IContent` (incl. E2EE cleartext `body`) under `cinny_scheduled_messages_v1`; normal logout (`removeFallbackSession`, N98) keeps it. Leak on shared/kiosk browser. Fix: clear on logout (add to `removeFallbackSession`) β or encrypt-at-rest with N97.
-- [ ] **SEC-2 β Recent search terms persist in `localStorage`, survive logout (Sev Low Β· Eff S).** `state/recentSearches.ts:4-12` β last 10 queries (often PII) under `cinny_recent_searches_v1`, survive normal logout. Fix: clear on logout.
-- [ ] **SEC-3 β `window.open(url, '_blank')` without `'noopener'` β reverse tab-nabbing (Sev Low Β· Eff S).** `components/user-profile/UserChips.tsx:117` (+ `SSOStage.tsx:23`, `settings/devices/Verification.tsx:276`, `OtherDevices.tsx:36,49`, `OidcManageAccount.tsx:23`) β `window.open` doesn't null `window.opener`. UserChips opens a user/room-derived server URL. Fix: pass `'noopener,noreferrer'` (or set `w.opener = null`).
-- [ ] **SEC-4 β `/acl` has no `*`/self-lockout guard or glob validation (Sev Low Β· Eff SβM).** `hooks/useCommands.ts:489-538` β `/acl -d *` writes `deny:['*']` (can brick the room); empty/whitespace globs pushed unvalidated into `m.room.server_acl`. PL-gated + matches upstream, but a destructive footgun. Fix: reject a `*`/empty deny matching the local server, validate glob syntax, confirm on `*`.
-- [ ] **SEC-5 β Embeds get `allow-popups-to-escape-sandbox` (Sev Low, informational Β· Eff M).** `components/url-preview/UrlPreviewCard.tsx:590-591` (`EMBED_SANDBOX`) β a hijacked provider script could spawn an un-sandboxed popup (phishing). Main-app hijack already prevented (no `allow-top-navigation`); popups arguably needed for "open in provider." Fix (if desired): drop the escape flag + verify per-provider.
-
-**Correctness (under-covered subsystems):**
-
-- [ ] **COR-1 β Space-child UNLINK nukes a room's OTHER parents + orphans descendants (Sev Med Β· Eff M).** `state/room/roomToParents.ts:102-103` (DELETE reducer `:53-66`) β removing one `m.space.child` fires the same `DELETE` used for "room left," wiping ALL of the child's parents and stripping it as a parent everywhere. Remove C from space A only β C also loses parent B, and C's own children lose C, until a full resync. Fix: a targeted "remove one parent from one child" action. (Reducer inherited from upstream.)
-- [ ] **COR-2 β `useCallJoined` stays `true` across a direct embed swap (answering a 2nd call) (Sev Med Β· Eff S).** `hooks/useCallEmbed.ts:140-158` β the reset effect only clears `joined` when `!embed`; answering call B from the banner swaps embed AβB directly, so `joined` stays true β B renders as live/joined before EC actually joins, skipping the loading/watchdog UI. Fix: `useEffect(() => setJoined(embed?.joined ?? false), [embed])`.
-- [ ] **COR-3 β Incoming-call lifetime guard only corrects FUTURE clock-skew, not past (Sev Low Β· Eff S).** `components/CallEmbedProvider.tsx:476-477` β distrusts `sender_ts` only when >20 s ahead of `origin_server_ts`; a caller clock that's behind leaves `sender_ts` in the past β `remaining β€ 0` β ring auto-dismisses/never shows for a fresh invite. Fix: `Math.abs(...) > 20000` or just use `event.getTs()`.
-- [ ] **COR-4 β Shared notify-dedupe slot double-notifies a re-fired main message after an interleaving thread reply (Sev Low Β· Eff S).** `pages/client/ClientNonUIFeatures.tsx:521,530` β `lastNotifiedEventRef` is one `Map` slot shared by the main + thread paths; a thread reply overwrites it, so a re-emitted main message (decrypt/edit re-fire, common in E2EE) mis-dedupes and notifies again. Fix: separate dedupe keys per path (or key by `roomId|eventId`).
-- [ ] **COR-5 β Upload cancel during retry back-off is silently ignored (upload resurrects) (Sev Low Β· Eff M).** `utils/matrix.ts:204-238` + `state/upload.ts:125-129` β during the 1β30 s back-off the atom holds the previous (rejected) promise, so `cancelUpload` is a no-op and the loop wakes to start the next attempt. Fix: thread an abort flag into the retry loop.
-- [ ] **COR-6 β `CallControl.forceState` resets `screenshareAudioMuted` to false (Sev Low Β· Eff S).** `plugins/call/CallControl.ts:155-162` β `forceState` passes only 5 args so the 6th defaults false; low impact today (runs at join, no screenshare yet) but silently discards the field. Fix: pass `this.screenshareAudioMuted`.
+- [DEFERRED] **PERF-6 β avatar-decoration `/profile` fetch** β well-guarded (module cache + in-flight dedupe + backoff); a network/HS-load note only. Batch/skip only if it proves costly.
+- [DEFERRED] **SEC-5 β embeds' `allow-popups-to-escape-sandbox`** β informational; main-app hijack already prevented (no `allow-top-navigation`), and popups are arguably needed for "open in provider." Revisit with per-provider verification if dropped.
+- **KE-1 preventive (`navigator.storage.persist()`)** is **already implemented** (`initClient` β `requestPersistentStorage()` + `src/index.tsx` boot). The rest of the KE cluster stays under **Encryption / E2EE** below (needs live capture).
### β
Unread/read-receipt flakiness (reported 2026-07) β FIXED (pending prod QA)