96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 1 | # macltok review, 2026-09-24 |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 2 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 3 | Second full review, taken after the restructure and Tier 0 landed. It covers |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 4 | correctness, races, robustness, duplication, antipatterns and how easy the crate |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 5 | is to build a client on, then folds in the API gaps reported by Malice, the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 6 | first real client built on macltok. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 7 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 8 | Every finding carries an id so fixes can reference it. Status starts as OPEN. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 9 | SDK claims cite matrix-sdk 0.18 source under `~/.cargo/registry` and were |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 10 | checked against the code rather than recalled. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 11 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 12 | Baseline when written. `cargo fmt --check` clean, all default-feature tests |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 13 | pass. The sqlite and clippy runs are recorded at the end. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 14 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 15 | ## C. Correctness |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 16 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 17 | ### C1. `Verify(Reject)` never rejects. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 18 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 19 | `VerificationController::reject` (`src/encryption.rs:339-389`) validates the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 20 | SAS state twice and returns `Ok(())` without ever calling `sas.mismatch()`. The |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 21 | peer is never told the emoji did not match, the local flow stays in |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 22 | comparison, and the command reports success. A user who presses "they don't |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 23 | match" gets no protection from the one action meant to provide it. No test |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 24 | exercises `Reject`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 25 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 26 | ### C2. Sync dies permanently after `SyncState::Error`. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 27 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 28 | The connection watcher maps `SyncState::Error` to `ConnectionState::Offline` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 29 | (`src/engine.rs:1338-1341`) and nothing ever calls `SyncService::start` again. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 30 | In the SDK the supervisor breaks out of its loop on error, and even offline |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 31 | mode ends in `Error` when its reachability check fails |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 32 | (`matrix-sdk-ui sync_service.rs:313-339`). `start()` is the only way back |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 33 | (`sync_service.rs:629-649`). The client sees "Offline" forever while the engine |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 34 | still reports itself healthy. `SyncState::Terminated` has the same problem, it |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 35 | becomes `ConnectionState::Stopped` while the engine keeps running, so |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 36 | `Stopped` now means two different things. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 37 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 38 | Fix direction. Restart with backoff on `Error` and `Terminated`, and report |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 39 | `Stopped` only from engine shutdown. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 40 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 41 | ### C3. A room's send queue stays disabled after any send failure. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 42 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 43 | The SDK disables a room's queue after every send error, recoverable or not |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 44 | (`matrix-sdk send_queue/mod.rs:1011-1019`), and nothing in matrix-sdk-ui turns |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 45 | it back on. macltok re-enables only inside `RetrySend` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 46 | (`src/engine.rs:2178`), and does so globally. One network blip therefore |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 47 | pauses every later message in that room until the user explicitly retries a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 48 | specific transaction. Element X re-enables queues when sync returns to |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 49 | running. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 50 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 51 | Fix direction. Re-enable on each transition to `Online`, and keep |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 52 | `RetrySend` for unwedging a single item. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 53 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 54 | ### C4. Own encrypted uploads cannot be fetched, and the media cache is split between two formats. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 55 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 56 | macltok writes ciphertext into the media store under the `Encrypted` source key |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 57 | and decrypts on every read (`src/engine.rs:2799-2821`, `2864-2874`). The SDK |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 58 | stores plaintext under that same key, both in `Media::get_media_content` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 59 | (`matrix-sdk media.rs:451-480`, `522-528`) and in the send queue. After an |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 60 | upload the send queue renames its plaintext cache entry to the remote |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 61 | `Encrypted` source (`send_queue/upload.rs:928-953`). So fetching your own sent |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 62 | image in an encrypted room reads plaintext, runs it through |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 63 | `AttachmentDecryptor`, fails the hash check and returns `MediaUnavailable`. The |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 64 | reverse also holds. Any SDK path that reads a macltok-written entry gets |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 65 | ciphertext as if it were the image. The media tests force plaintext rooms |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 66 | (`tests/media.rs:766`), which is why this passed. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 67 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 68 | Fix direction. Cache plaintext, matching the SDK, and keep the byte bound by |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 69 | decrypting in a streaming fashion before the write. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 70 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 71 | ### C5. Media single-flight is poisoned by a leader panic. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 72 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 73 | In `spawn_media_fetch` (`src/engine.rs:1870-1911`) followers push their id and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 74 | return at once, trusting the leader to answer for them. If `fetch_media` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 75 | panics, the leader's own id gets "command task panicked", the followers get |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 76 | nothing, and the `(MediaId, MediaSize)` entry is never removed. Every later |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 77 | fetch of that media then becomes a follower of a dead leader and never |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 78 | completes. Largely superseded if media fetch becomes a direct query (see |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 79 | M2), but the fix is the same either way. Use a drop guard or a shared future |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 80 | so the entry always clears. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 81 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 82 | ### C6. Back pagination can stick at "start reached" forever. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 83 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 84 | `reached_start` is a cached `AtomicBool` that is only ever set from a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 85 | pagination result (`src/engine.rs:4060-4071`, `4097`). A limited sync with a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 86 | gap makes the event cache shrink to the last chunk |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 87 | (`event_cache/caches/room/state.rs:917-924`), which the timeline applies as |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 88 | `VectorDiff::Clear`. History is unloaded again but the flag stays true, so |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 89 | every later `Paginate` emits `StartReached` without fetching anything. The |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 90 | room scrolls to a stub and stops. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 91 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 92 | Fix direction. Drop the flag and follow `Timeline::live_back_pagination_status` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 93 | (`matrix-sdk-ui timeline/pagination.rs:130-138`), which is authoritative. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 94 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 95 | ### C7. Soft logout destroys the store. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 96 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 97 | `SessionChange::UnknownToken` carries `soft_logout` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 98 | (`matrix-sdk client/mod.rs:166`), but the watcher collapses it into a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 99 | payload-free `SessionAccessRejected` (`src/engine.rs:1314`). The README and the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 100 | event's doc comment both tell the client to delete the saved record and the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 101 | store. On a soft logout the correct response is to re-authenticate and keep |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 102 | the store, so following the documentation throws away every E2EE key for |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 103 | nothing. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 104 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 105 | ### C8. Logout and rejected access leave the engine running. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 106 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 107 | `spawn_logout` emits `LoggedOut` and returns (`src/engine.rs:825-852`). The |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 108 | sync service, watchers and room actors keep running against a dead session, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 109 | and every later command fails with an authentication error. Neither logout nor |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 110 | a hard `UnknownToken` shuts the engine down. The engine should stop itself, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 111 | emitting `LoggedOut` or the rejection before `Stopped`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 112 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 113 | ### C9. `MarkRead` can move the read marker backwards and costs two requests. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 114 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 115 | `mark_read` (`src/engine.rs:2072-2095`) calls `Room::send_single_receipt` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 116 | twice. That call does no ordering check |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 117 | (`matrix-sdk room/mod.rs:2167-2201`), so a client that marks an older event |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 118 | drags both the public receipt and the fully-read marker back. The timeline API |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 119 | refuses stale receipts (`timeline/controller/mod.rs:1621-1690`), and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 120 | `Room::send_multiple_receipts` sets fully read and read in one |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 121 | `/read_markers` call (`room/mod.rs:2213-2230`). Private read receipts are not |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 122 | offered at all. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 123 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 124 | Fix direction. When the room is open, route through the actor and use |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 125 | `Timeline::send_multiple_receipts`. Otherwise use a single |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 126 | `Room::send_multiple_receipts`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 127 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 128 | ### C10. `create_direct` always creates a new room. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 129 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 130 | `Client::create_dm` never reuses an existing DM |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 131 | (`matrix-sdk client/mod.rs:1830-1849`). Starting a chat with someone you |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 132 | already talk to creates a duplicate every time. `Client::get_dm_room` exists |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 133 | for the lookup (`client/mod.rs:1852`). |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 134 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 135 | ### C11. Unregistered tasks swallow panics, contradicting the design doc. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 136 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 137 | The design doc says a watcher panic is terminal. But the timeline watcher |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 138 | (`src/engine.rs:589`), pagination worker (`446`), pagination result task |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 139 | (`472`) and verification watcher (`src/encryption.rs:276`) are bare |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 140 | `tokio::spawn`s. A panic in a timeline watcher freezes that timeline in |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 141 | silence while commands keep succeeding. The reverse mistake also exists. The |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 142 | account-data writers (`src/engine.rs:1111`, `1141`) and the per-room media |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 143 | upload watcher (`1945`) are registered as `TaskContext::Watcher`, so a panic |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 144 | in a single write kills the whole engine. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 145 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 146 | ### C12. The room actor blocks on network requests. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 147 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 148 | Each room actor runs its commands serially and awaits each one. Most sends |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 149 | only enqueue, but `Timeline::redact` on a remote event is a direct HTTP call |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 150 | (`timeline/mod.rs:630`), and so is removing a reaction |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 151 | (`controller/mod.rs:595`). `Room::make_reply_event` can fetch the replied-to |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 152 | event (`room/reply.rs:108`). A slow homeserver therefore stalls every later |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 153 | send, paginate and thread command in that room behind one redaction. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 154 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 155 | ### C13. Media registry evicts media that is still on screen. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 156 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 157 | `MediaRegistry::insert_all` (`src/engine.rs:358-370`) evicts in first-insertion |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 158 | order and does not refresh position on re-registration. One registry of 2048 |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 159 | entries is shared by every room. In a long-lived session, the first messages |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 160 | loaded in a room that is still open are the first evicted, and fetching them |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 161 | then fails with `MediaUnknown`. Closed timelines never release their entries, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 162 | so they compete with open ones. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 163 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 164 | ### C14. Smaller correctness issues. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 165 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 166 | - `CommandResultError::Failed` renders its `Display` through `Debug` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 167 | (`src/transport.rs:840`), so users see a struct dump. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 168 | - `download_bounded` builds a fresh `reqwest::Client` per fetch just to build a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 169 | request (`src/engine.rs:3181`). That reloads the TLS roots every time. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 170 | `http_client.get(...)` is enough. On a 401 it refreshes the token and then |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 171 | fails rather than retrying once. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 172 | - On `Lagged`, the session watcher assumes the dropped message was a token |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 173 | refresh (`src/engine.rs:1315`). A dropped `UnknownToken` is lost. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 174 | - Leaving a room does not close its open actor, so its timeline, typing |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 175 | subscription and room subscription outlive the membership. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 176 | - `open_room` on a room that is already open succeeds with no `TimelineReset` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 177 | (`src/engine.rs:1608`). A second frontend sharing the engine gets success and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 178 | no snapshot. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 179 | - On shutdown, in-flight commands never get a terminal result. That is |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 180 | acceptable, but the docs should say that `Stopped` fails everything still |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 181 | outstanding. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 182 | - A store path reused after a login whose record was never persisted holds a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 183 | crypto account for another device, and the SDK refuses it. Document one |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 184 | store directory per login. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 185 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 186 | ## A. API shape and client-builder friendliness |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 187 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 188 | ### A1. Three completion mechanisms, and the useful one is rationed. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 189 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 190 | A client must know which of three patterns each operation uses. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 191 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 192 | - `send` returns a `CommandId`. The client correlates it by hand against |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 193 | `CommandSucceeded`, `CommandSucceededWithRoom` or `CommandFailed` on a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 194 | shared stream. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 195 | - Five direct methods (`join_room`, `create_room`, `create_direct`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 196 | `delete_devices`, and the internal room-result path) await a `oneshot` and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 197 | return `CommandResultError`. These emit nothing on the stream. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 198 | - The account-data writes await a `oneshot` but return `ClientQueryError`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 199 | with no `CommandId` at all. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 200 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 201 | The headless example is mostly bookkeeping of ids. Malice gap 4 comes straight |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 202 | from this. The one pattern a web backend can use, an awaited result per |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 203 | request, is not offered for most operations. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 204 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 205 | ### A2. Pure reads modelled as commands, against the doc's own rule. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 206 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 207 | The design doc says operations are split by whether they change state. Media |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 208 | fetch changes no state, yet it is a `MediaCommand` whose bytes arrive as a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 209 | `MediaReady` broadcast on the shared stream. Every frontend then receives every |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 210 | other frontend's image bytes, up to 20 MiB each, and the requester has to |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 211 | filter by `MediaId`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 212 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 213 | ### A3. Every timeline item is a `Message`, with presentation strings in `body`. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 214 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 215 | `normalise_timeline_item` (`src/normalise/timeline.rs`) flattens every event |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 216 | into `TimelineMessage`. The consequences for a client are these. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 217 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 218 | - Membership changes, profile changes and room state changes arrive as |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 219 | messages whose body reads `[m.room.member]` or similar. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 220 | - Redacted and undecryptable events carry hard-coded English bodies, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 221 | `<message redacted>` and `<unable to decrypt message>`. Only UTD has a flag. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 222 | A redaction cannot be told apart from a user who typed that text. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 223 | - `msgtype` is dropped, so emotes and notices cannot be rendered differently |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 224 | from text. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 225 | - No sender avatar, though the SDK profile has one. `display_name_ambiguous` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 226 | is ignored, so two "Alice"s look identical. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 227 | - Reply references hold no preview content, and reply details are never |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 228 | fetched. The SDK leaves them `Unavailable` until the app calls |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 229 | `Timeline::fetch_details_for_event` (`timeline/event_item/content/reply.rs:38`), |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 230 | so replies to anything outside the loaded window never get a sender. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 231 | - `TimelineItem` is not `#[non_exhaustive]`, so adding a state-event variant |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 232 | later is a break. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 233 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 234 | This needs a content enum, for example `Message { msgtype, ... }`, `Redacted`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 235 | `UnableToDecrypt`, `Membership`, `ProfileChange`, `RoomState`, `Poll` and |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 236 | `Other { event_type }`. It is the largest single rendering gap. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 237 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 238 | ### A4. Timeline commands cannot target local echoes. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 239 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 240 | `Edit`, `Redact` and `ToggleReaction` take an `OwnedEventId` and wrap it in |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 241 | `TimelineEventItemId::EventId`. The SDK accepts transaction ids for all three, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 242 | but a client cannot edit, redact or react to a message it sent until the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 243 | server echo arrives. The commands should take the identifier that the model |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 244 | already exposes. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 245 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 246 | ### A5. `RoomSummary` is too thin for a room list. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 247 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 248 | Missing fields, all cheap SDK reads. Topic, canonical alias, joined and invited |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 249 | member counts, heroes, tombstone and successor, inviter for invites, a latest |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 250 | event preview, and space parents and children (Malice gap 3). |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 251 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 252 | ### A6. No incoming verification. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 253 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 254 | Only outgoing `StartVerification` exists. A request started from another |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 255 | device, which is the normal "verify this new login" flow, is never observed. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 256 | macltok registers no handler for it, and the SDK has no stream, so the app |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 257 | pattern is an event handler plus `Encryption::get_verification_request` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 258 | (`encryption/mod.rs:1060`). |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 259 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 260 | ### A7. Missing `#[non_exhaustive]`. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 261 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 262 | Adding the attribute later is itself a breaking change, so it belongs now. Output types that will |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 263 | grow are `TimelineItem`, `CommandKind`, `RoomMembership`, `VerificationState`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 264 | `HistoryState`, `KeyBackupState` and `RecoveryState`. Input structs that will |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 265 | grow need a constructor plus `#[non_exhaustive]`. These are |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 266 | `CreateRoomOptions`, `PasswordLogin`, `OAuthLogin` and `UploadThumbnail`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 267 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 268 | ### A8. Smaller API issues. OPEN |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 269 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 270 | - `MediaCommand::Upload` duplicates `UploadAttachment`, and the handle exposes |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 271 | both `upload_media` and `upload_attachment`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 272 | - Convenience methods cover room and account commands but not timeline send, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 273 | edit, paginate, mark read, typing, recovery or verification, so a client uses |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 274 | two styles side by side. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 275 | - `HistoryState::Failed(String)` and `RoomPowerLevels::events: Vec<(String, i64)>` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 276 | are stringly typed where typed values exist. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 277 | - `rooms_filtered` re-implements the search half of `RoomListFilter::matches`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 278 | which itself lowercases the needle once per room. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 279 | - `RoomsChanged` is sent once per notable room-info update with no coalescing |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 280 | at source. A sync touching fifty rooms queues fifty invalidations, each of |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 281 | which prompts a full `rooms()` rebuild in a naive client. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 282 | - The encryption defaults (`auto_enable_backups`, `OneShot` backup download) |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 283 | are hard-coded with no override. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 284 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 285 | ## D. Duplication and antipatterns |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 286 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 287 | Line references are from the mechanical inventory taken during this review. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 288 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 289 | - `RoomActor::run` is about 700 lines. The "queue while the thread is opening, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 290 | else QueueFull" block appears four times (`src/engine.rs:3367-3402`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 291 | `3518-3549`, `3625-3644`), and emit-then-`break 'actor` about twenty times. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 292 | A thread registry type with one `route(id, root, command)` method removes |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 293 | most of it. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 294 | - Six `spawn_*` helpers share one shape of clone, spawn, run, emit |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 295 | (`src/engine.rs:825-960`). `spawn_rename_device` hand-rolls the same thing |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 296 | without `emit_command_result`. `CommandFailure` is built by hand in nine |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 297 | places. The reply-or-event branch appears nine times. One `spawn_command(id, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 298 | kind, future)` plus one completion sink covers all of them, and A1 removes |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 299 | the branch entirely. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 300 | - `media_http_request` (`src/engine.rs:2991-3175`) is six near-identical |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 301 | branches that differ by endpoint type and token mode. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 302 | - Twenty-one `client.get_room(...)` lookups with hand-written `RoomUnknown` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 303 | errors, ten of them consecutive match arms in `execute_room_command`. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 304 | - `media_from_message` and `media_registration` walk `MessageType` twice for |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 305 | the same item. The descriptor can be derived from the registration. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 306 | - The verification controller repeats its "done, then cancelled, then error" |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 307 | terminal check eight times. It also carries an unused `_client` parameter, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 308 | and the encryption worker builds a throwaway `VerificationController` to run |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 309 | `Recover`. `EncryptionWork` is a single-variant enum. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 310 | - `catch_unwind` with a "... panicked" string appears six times. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 311 | - Test helpers `receive_until`, `event_messages`, `diff_items`, `start_engine` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 312 | and `stop` are copied across up to five test files. A shared |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 313 | `tests/support/mod.rs` removes them. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 314 | - `model.rs` has its test module in the middle of the file, with types |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 315 | defined after it. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 316 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 317 | ## T. Test coverage |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 318 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 319 | No test touches the whole encryption domain (recovery, backups, verification), |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 320 | all notification commands, invite, kick, ban and unban, power levels, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 321 | `MarkRead`, `SetTyping`, `Redact`, `ToggleReaction`, `CloseThread`, `Logout`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 322 | `RenameDevice`, `DeleteDevices`, `resolve_push_notification` or |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 323 | `rooms_filtered`. No test uses an encrypted room, which is how C4 got through. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 324 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 325 | ## M. Malice gaps, synthesised |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 326 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 327 | Malice reported four gaps. Each one traces back to a finding above, and fixing |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 328 | the underlying finding closes the gap without special-casing it. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 329 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 330 | | Malice gap | Root cause | Proposed shape | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 331 | | --- | --- | --- | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 332 | | Own profile | Not exposed at all | `ClientHandle::own_profile()`, a direct read via `Account::fetch_user_profile` (`account.rs:353`), returning a `Profile { user_id, display_name, avatar }`. Add `user_profile(user_id)` alongside. Setting the name and avatar become commands. | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 333 | | MXC avatar download | Media is reachable only through registered `MediaId`s (A2) | `ClientHandle::mxc_media(uri, MediaRequest)` as a bounded direct query, sharing the download, bound and cache code with timeline media. | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 334 | | Space children | `RoomSummary` gap (A5) | Wrap `matrix_sdk_ui::spaces::SpaceService` (`spaces/mod.rs`), which offers top-level joined spaces, `joined_parents_of_child` and a paginated `/hierarchy` `SpaceRoomList`. | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 335 | | Timeline media bytes per request | A1 and A2 | `ClientHandle::media(media_id, size)` as an awaited direct query. Remove `MediaCommand::Fetch` and `Event::MediaReady`. | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 336 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 337 | Malice's rule, that no `matrix-sdk` use remains in the app, holds once these |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 338 | land. None of the four needs an escape hatch. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 339 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 340 | ## Discussion items |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 341 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 342 | These are genuine trade-offs rather than fixes, each with a recommendation. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 343 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 344 | ### Q1. How far to take awaited results |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 345 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 346 | Option 1. Keep `send` and add `handle.execute(command).await -> Result<CommandOutput, CommandFailure>` |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 347 | by carrying an optional `oneshot` in `CommandEnvelope`. Stream result events |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 348 | continue for `send`. This is additive, but it leaves two mechanisms and every |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 349 | engine path still branches on reply versus event. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 350 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 351 | Option 2. Make awaited results the only completion path. Remove `CommandId`, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 352 | `CommandSucceeded*`, `CommandFailed` and `InteractiveAuthRequired` from the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 353 | stream, which then carries only state changes. Fire-and-forget callers spawn a |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 354 | task. Every engine path produces one `Result` and the nine reply-or-event |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 355 | branches disappear. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 356 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 357 | The cost of option 2 is ordering. Today a `TimelineReset` is guaranteed to |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 358 | precede the `CommandSucceeded` for its `Open` or `Resync` on the same stream. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 359 | Across a stream and a `oneshot` the guarantee weakens to "enqueued before", so |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 360 | a client draining the stream in another task can see the future resolve first. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 361 | Either `Open` and `Resync` return the snapshot in their result and stop |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 362 | emitting `TimelineReset`, which then needs an epoch on `TimelineChanged` so |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 363 | stale diffs from the old watcher can be discarded, or the contract states that |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 364 | the snapshot arrives on the stream at some point after success. A second |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 365 | frontend sharing the engine also stops seeing the other's outcomes, though it |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 366 | still sees every resulting state change. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 367 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 368 | Recommendation. Option 2 with snapshot-in-result and an epoch. It is the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 369 | larger break, but it is the shape every client ends up wrapping the current API |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 370 | into, and breakage is free now. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 371 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 372 | ### Q2. Space children source |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 373 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 374 | `SpaceService` gives the full graph, including unjoined children, over the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 375 | network via `/hierarchy`. Local `m.space.child` state is instant but lists only |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 376 | what sync delivered and cannot show rooms the user has not joined. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 377 | Recommendation. Own a `SpaceService` in the engine, as with `SyncService`. Put |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 378 | `parent_spaces` and `child_rooms` on `RoomSummary` from local state for the |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 379 | sidebar, and add a paginated hierarchy query for browsing. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 380 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 381 | ### Q3. Timeline content model depth |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 382 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 383 | A3 can be closed with a coarse enum (message, redacted, UTD, state, other) or |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 384 | with typed variants for membership and profile changes, room name and topic, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 385 | polls and calls. Coarse is quick but pushes raw event parsing onto every |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 386 | client. Recommendation. Typed variants for membership, profile, name, topic, |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 387 | avatar and encryption changes, which cover what Element shows inline, plus |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 388 | `Other { event_type }` as the escape. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 389 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 390 | ## Verification results |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 391 | |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 392 | - `cargo fmt -- --check` clean. |
96fdf90aacd6 | atagen | 2026-09-08 08:24 UTC | 393 | - `cargo test --all-targets` 49 passed, none failed. |