Commit 722b4c3d0a09
tarball2 files changed+248-0
@@ -1 +1,2 @@
11
target/2
+/result@@ -0,0 +1,247 @@
1
+# macltok review, 2026-10-252
+3
+Read-only review of everything that landed on 2026-10-25. In macltok that is4
+event context pagination, the encrypted mentions inbox, bounded history repair,5
+segmented history and the pack fixes, as published on `vapour` 8fd8dcf6. In the6
+sdk fork it is the sliding sync commit fence, reseeding, room-owned history7
+views, replacement snapshots, segmented history and cinny reaction shortcodes,8
+up to 9b2c17fb1.9
+10
+Nothing was built or run. Every finding was read against the code, and the11
+ones marked verified were reread by hand after the reviewer reported them.12
+Ids carry a status that starts as OPEN.13
+14
+## S. sdk fork correctness15
+16
+### S1. A limited sync can lose its gap. verified, OPEN17
+18
+`handle_sync` takes `older_boundary` from the in-memory linked chunk only19
+(`event_cache/caches/room/state.rs:949`). After auto-shrink, or a restart with20
+lazy loading, a room whose last chunk is the trailing gap from a recent load21
+holds no events in memory. A limited sync for that room then records an22
+ordinary gap and no `HistoryGap`. On reopen the retained events and the new23
+ones render as continuous, which is the lost interval malice reported.24
+25
+Fix direction. Take the boundary from the store's newest event when memory has26
+none.27
+28
+### S2. An empty fill page panics when the next chunk is a gap. verified, OPEN29
+30
+`apply_page` always passes a gap to `push_backwards_pagination_events`31
+(`room/history.rs:549`). With no events, `replace_gap_at` removes the gap and32
+returns the next chunk's first position. If that chunk is itself a gap,33
+`insert_gap_at` returns `ChunkIsAGap` and the `.expect` at34
+`event_linked_chunk.rs:403` fires. On native the room's history coordinator35
+dies and every later history call for the room returns `ClientDropped`. On wasm36
+it likely takes the worker with it.37
+38
+### S3. A limited sync during a recent fetch leaves two adjacent gaps. OPEN39
+40
+The recent fetch captures its anchor before the network request41
+(`room/history.rs:221`). A limited sync that lands meanwhile installs its own42
+gap, and the fetch then inserts its events and gap after it. The stale sync gap43
+shows as a gap row over continuous history, and filling it walks into S2.44
+45
+### S4. Every room open persists a gap chunk. OPEN46
+47
+The recent load persists a gap whenever an anchor exists, `Joined` included48
+(`room/history.rs:284-319`). macltok runs it on every open, so joined gaps pile49
+up in storage, in `history_gaps()` and in every gap publication. The trailing50
+gap left behind is also what produces the empty in-memory chunk S1 needs. The51
+`RecentHistoryOutcome.gap` doc says the field is absent when nothing needs52
+joining, and it is not.53
+54
+### S5. `history_candidate` decrypts the whole room twice per operation. OPEN55
+56
+It walks every stored chunk under the state write lock, which also blocks the57
+room's sync (`room/state.rs:805-825`). A recent load and each fill page do it58
+twice, and the commit then holds the whole history in memory until shrink. The59
+insertion loop at `history.rs:301-312` is also O(n·m).60
+61
+### S6. sqlite event cache reads moved onto the write connection. OPEN62
+63
+72db3bb7d moved `load_all_chunks`, `load_all_chunks_metadata`,64
+`load_last_chunk`, `load_previous_chunk`, `filter_duplicated_events` and65
+`find_event_relations` into `with_immediate_transaction` so that66
+`register_linked_chunk_owner` can do a one-time `INSERT OR IGNORE`. Reads now67
+serialise behind every write. Plausibly part of the slow sqlite login.68
+69
+### S7. Pinned and event-focused views die after a clear. OPEN70
+71
+`clear_loaded_views` drops the pinned cache and drains event-focused caches,72
+while threads are cleared and kept (`room/state.rs:866-877`). An open pinned73
+timeline gets a Clear and then a closed stream. An event-focused timeline gets74
+a Clear, then `MissingCache` on paginate. An interrupted event-focused75
+pagination reports `hit_end_of_timeline`.76
+77
+### S8. A room `Update::Clear` on error paths wipes view chunks under live caches. plausible, OPEN78
+79
+Room Clear now deletes thread and pinned chunks and standalone events on all80
+three backends. The pagination and shrink error paths send it without calling81
+`clear_loaded_views`, so loaded thread and pinned caches keep writing against82
+chunk ids that no longer exist.83
+84
+### S9. Reset on the live timeline drops receipts and identities. OPEN85
+86
+The Reset arm of the timeline state transaction clears `read_receipts` and87
+`fully_read_event` and never repopulates them88
+(`timeline/controller/state_transaction.rs:216-231`). It also discards89
+`recycled_timeline_ids`, so every surviving row gets a new id.90
+91
+### S10. A commit hook that never resolves stalls sync for good. OPEN92
+93
+The hook runs inside `handle_response` holding the position lock94
+(`sliding_sync/mod.rs:338`). Nothing can cancel it, and every later95
+`sync_once` and `expire_session` waits on that lock. macltok's hook always96
+resolves today, so this is a hazard in the api, not a live bug.97
+98
+### S11. Smaller sdk findings. OPEN99
+100
+- `visited_cursors` grows by one cursor per page, forever, and is re-encrypted on every gap rewrite.101
+- `RoomEventCache::clear` and `reseed` `.expect` their join handles, so a task panic or runtime shutdown panics the caller.102
+- sqlite writes plain gaps as a bare string and history gaps as an object behind an untagged reader. Always writing the object would leave one reader shim.103
+- A downgrade fails to read history gaps and clears the room. Fine pre-1.0, noted.104
+- IndexedDB room Clear decrypts every chunk in the database to find one room's.105
+- The cinny `"shortcode"` field is a bare literal in three places, and `reaction_shortcode`'s doc says it only reads MSC4027.106
+- The pinned Lagged path subscribes to the new cache and keeps reading the old receiver. Upstream bug, now hit after every clear.107
+108
+## M. macltok correctness109
+110
+### M1. The mentions inbox rewrites itself on every sync response. OPEN111
+112
+`batch` writes a new checkpoint for every response, typing and receipts113
+included, and `save` serialises, encrypts and durably commits the whole inbox114
+inside the sync hook (`mentions/service.rs`). With the 64 MiB default budget115
+that is up to 64 MiB written per typing notification before `pos` may advance,116
+at roughly four times that in peak memory.117
+118
+Related costs on the same path.119
+- Every background completion also does a full save. Permanently undecryptable candidates are never dropped and retry forever with backoff capped at 256s, and any decryption report resets every candidate's retry.120
+- `filter_ignored` reads the ignore list from the state store once per entry per batch.121
+- Budget eviction drops one window and re-encrypts the whole inbox before checking again.122
+- Candidate eviction removes the smallest key, which means lexicographic room id, not the oldest.123
+124
+### M2. A mentions failure stops sync. OPEN125
+126
+A failed save fails the hook and the hook stops room list sync. An IndexedDB127
+quota error means no messages at all. A corrupt mentions store fails128
+`Account::build`, and the only way out is `StoreConfig::delete`, which also129
+drops the crypto store.130
+131
+### M3. A crash between hook ack and `pos` write raises a false global gap. verified, OPEN132
+133
+The hook records `next_pos` P2 durably, then the process dies before the sdk134
+writes `pos`. On restart the sdk replays with `previous_pos` P1, the checkpoint135
+says P2, and `batch` treats it as a discontinuity, marking every observed room136
+unresolved (`mentions/service.rs:404`). Keeping the previous checkpoint and137
+accepting a replay from it would fix it.138
+139
+### M4. Event context lost what the timeline gave it. OPEN140
+141
+ywsvysot moved `EventContext` from an event-focused sdk timeline to raw142
+`/messages` through `normalise_observed`. That fixed real bugs in the focused143
+path, but items now have no display names or avatars, edits arrive as separate144
+messages, reactions and redactions arrive as `Other`, replies stay145
+`ReplyDetails::Pending` forever and UTDs never retry.146
+147
+Also in context.148
+- Truncating a page to `limit` while returning the server's `end` token skips whatever was cut, and `limit: 0` is accepted for pages.149
+- Fetch awaits `before` and `after` one after the other.150
+- Context media go into the shared 2048-entry registry and are never released, so heavy context paging can evict media for rows still on screen.151
+152
+### M5. Cancel is reported as failure. OPEN153
+154
+A user Cancel and the cancel that runs on room close both publish155
+`RecentHistoryState::Failed("…interrupted")` and a report with a failure and no156
+stop (`engine/repair.rs:284`, `:347`). `HistoryRepairStop::Cancelled` exists157
+and is never sent.158
+159
+### M6. Network failures report as store errors. OPEN160
+161
+`cache_error` maps every `EventCacheError` to `ErrorKind::Store`162
+(`engine/repair.rs:444`), `PaginationError` included. The `Failed => Network`163
+arm cannot be reached because the sdk returns the error after committing164
+`Failed`.165
+166
+### M7. Every history completion sends a full reset. OPEN167
+168
+`publish_history` flushes and then always resyncs (`engine/timeline.rs:134-145`),169
+so every completion, failures and close included, sends a `TimelineReset`, and170
+every room open produces two. The segmented history doc promises incremental171
+insertions. `tests/engine.rs` ignores resets during recent loading, which hides172
+it.173
+174
+### M8. Smaller macltok findings. OPEN175
+176
+- The mirror never removes `recent_history` or `repairs` entries when a room closes, and `RecentHistoryState::Idle` is never emitted.177
+- `initial_history_limit` no longer applies to room timelines, and a failed recent load leaves a room with no initial history at all.178
+- An `Updated` result for an already joined gap still counts a page.179
+- The `Joined` gap arm in timeline normalisation cannot be reached, because the sdk drops joined gaps.180
+- A finished recent load publishes both `RecentHistoryChanged(Loaded)` and a `HistoryRepairReport` for the same edge.181
+- `evict_one` sets an entry's revision to the current one, not the next one, so revision-keyed consumers miss the change.182
+- Mentions in a room left within the same response are dropped by the joined check.183
+184
+## D. dead api and design185
+186
+### D1. Most of the history repair api does nothing. OPEN187
+188
+`Start.budget` is validated and ignored. The policy is echoed and stored in the189
+mentions inbox and never read. `Continue` always errors. `StartRecent`190
+duplicates the open-time load. `GetPolicy` and `SetPolicy` in the actor are191
+unreachable because the engine handles them first. Never emitted are192
+`HistoryRepairStrategy::Overlap`, `HistoryRepairStop::Cancelled` and `NoAnchor`,193
+`HistoryRepairPhase::FetchingRecent`, `coverage_duration_ms` and194
+`threshold_evaluated`. Pre-1.0, so delete rather than keep them for serde.195
+196
+### D2. `HistoryGapLifecycle::Fetching` is never emitted. OPEN197
+198
+A consumer has to match a `Bridging` report's `gap_id` to know a gap is199
+filling, then infer the end from a later report. Either emit it on the gap row200
+while a fill runs or delete it.201
+202
+### D3. Mentions shapes. OPEN203
+204
+- `MentionGap.reason` is a string.205
+- `MentionCoverage.incomplete_history` is always true.206
+- Every newly seen room adds a gap and the list caps at 128, so `truncated` is true after the first sync on any account with more than 128 rooms.207
+- The store is a generic multi-record store with HMAC'd keys that only ever holds `"inbox"`.208
+- `MentionsCommand` is consumer-built and missing `#[non_exhaustive]`, and so are `HistoryRepairCommand`, `HistoryRepairPolicy` and `HistoryRepairBudget`.209
+210
+### D4. Pack shims. OPEN211
+212
+`clone_pack_image_with_usage`, `clone_sticker_with_usage` and213
+`clone_pack_with_usage` sit beside wrappers under the old names. `known_usage`214
+drops usage values it does not know, so clone stops being faithful for msc2545215
+extensions, and it duplicates the default-usage rule in216
+`normalise::image_pack::usage`. `Some(vec![])` silently means inherit.217
+218
+### D5. `reseed` has no caller. OPEN219
+220
+`RoomEventCache::reseed`, its state method, `reseed.md` and its tests are about221
+500 lines of fork surface. macltok uses `fill_history_gap_once` and the222
+segmented history doc says it does not reseed. The generation fencing stays,223
+`clear` and `commit_history_candidate` need it.224
+225
+### D6. Event context shape. OPEN226
+227
+`EventContext.event` is optional but always present on success.228
+`EventContextPage.end_reached` repeats `next_token.is_none()`. The transport229
+takes `u32` and clamps to 50 while the engine rejects anything over 50.230
+231
+## H. housekeeping232
+233
+- H1. sdk 9b2c17fb1 is only on the fork's `segmented-history` branch. The `macltok` branch is at 9b9b16982. Fast-forward it before tidying branches or cargo cannot fetch the pin. OPEN234
+- H2. lyvnurlv d1313f47 is an empty change merged into vapour, and the segmented history doc cites it. At xnzknouu the pins are split three ways. OPEN235
+- H3. `repair.rs` landed without rustfmt. OPEN236
+- H4. `docs/segmented-history.md` says the lost interval drops are an earlier path, and both `prev_batch` drops are still at `state.rs:943`. It says concurrent history operations return `RoomBusy`, and only a fill does. Much of it is handoff status that will rot. OPEN237
+- H5. `docs/mentions-discovery.md` says replies need a notifying highlight while the code accepts `direct` alone, and says context capture runs after the commit while the watch publication runs before the ack. OPEN238
+- H6. American spellings in sqlite.rs and the mentions doc. OPEN239
+240
+## Checked and sound241
+242
+- Gap metadata and the mentions inbox are encrypted at rest on sqlite and IndexedDB. The inbox head compare-and-swap holds.243
+- The sliding sync `pos` advances only after the hook acks, on both stores.244
+- Coordinator cancellation, shared recent fetches and atomic event, gap and cursor commits.245
+- The old timeline watcher stops before the reset goes out, and the mirror ack precedes the command reply.246
+- Both merges keep both sides. All nine sdk pins and both lockfiles agree on 9b2c17fb1.247
+- The spare sdk worktrees hold nothing that is not already committed.