atagen

macltok

public
Star0
Watch0

Commit 722b4c3d0a09

tarball

atagen <boss@atagen.co>2026-10-25 10:30 UTC

2 files changed+248-0
docs: record the 2026-10-25 review
Modified file: .gitignore+1 additions
@@ -1 +1,2 @@
11 target/
2+/result
Added file: docs/review-2026-10-25.md+247 additions
@@ -0,0 +1,247 @@
1+# macltok review, 2026-10-25
2+
3+Read-only review of everything that landed on 2026-10-25. In macltok that is
4+event context pagination, the encrypted mentions inbox, bounded history repair,
5+segmented history and the pack fixes, as published on `vapour` 8fd8dcf6. In the
6+sdk fork it is the sliding sync commit fence, reseeding, room-owned history
7+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 the
11+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 correctness
15+
16+### S1. A limited sync can lose its gap. verified, OPEN
17+
18+`handle_sync` takes `older_boundary` from the in-memory linked chunk only
19+(`event_cache/caches/room/state.rs:949`). After auto-shrink, or a restart with
20+lazy loading, a room whose last chunk is the trailing gap from a recent load
21+holds no events in memory. A limited sync for that room then records an
22+ordinary gap and no `HistoryGap`. On reopen the retained events and the new
23+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 has
26+none.
27+
28+### S2. An empty fill page panics when the next chunk is a gap. verified, OPEN
29+
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 and
32+returns the next chunk's first position. If that chunk is itself a gap,
33+`insert_gap_at` returns `ChunkIsAGap` and the `.expect` at
34+`event_linked_chunk.rs:403` fires. On native the room's history coordinator
35+dies and every later history call for the room returns `ClientDropped`. On wasm
36+it likely takes the worker with it.
37+
38+### S3. A limited sync during a recent fetch leaves two adjacent gaps. OPEN
39+
40+The recent fetch captures its anchor before the network request
41+(`room/history.rs:221`). A limited sync that lands meanwhile installs its own
42+gap, and the fetch then inserts its events and gap after it. The stale sync gap
43+shows as a gap row over continuous history, and filling it walks into S2.
44+
45+### S4. Every room open persists a gap chunk. OPEN
46+
47+The recent load persists a gap whenever an anchor exists, `Joined` included
48+(`room/history.rs:284-319`). macltok runs it on every open, so joined gaps pile
49+up in storage, in `history_gaps()` and in every gap publication. The trailing
50+gap left behind is also what produces the empty in-memory chunk S1 needs. The
51+`RecentHistoryOutcome.gap` doc says the field is absent when nothing needs
52+joining, and it is not.
53+
54+### S5. `history_candidate` decrypts the whole room twice per operation. OPEN
55+
56+It walks every stored chunk under the state write lock, which also blocks the
57+room's sync (`room/state.rs:805-825`). A recent load and each fill page do it
58+twice, and the commit then holds the whole history in memory until shrink. The
59+insertion loop at `history.rs:301-312` is also O(n·m).
60+
61+### S6. sqlite event cache reads moved onto the write connection. OPEN
62+
63+72db3bb7d moved `load_all_chunks`, `load_all_chunks_metadata`,
64+`load_last_chunk`, `load_previous_chunk`, `filter_duplicated_events` and
65+`find_event_relations` into `with_immediate_transaction` so that
66+`register_linked_chunk_owner` can do a one-time `INSERT OR IGNORE`. Reads now
67+serialise behind every write. Plausibly part of the slow sqlite login.
68+
69+### S7. Pinned and event-focused views die after a clear. OPEN
70+
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 pinned
73+timeline gets a Clear and then a closed stream. An event-focused timeline gets
74+a Clear, then `MissingCache` on paginate. An interrupted event-focused
75+pagination reports `hit_end_of_timeline`.
76+
77+### S8. A room `Update::Clear` on error paths wipes view chunks under live caches. plausible, OPEN
78+
79+Room Clear now deletes thread and pinned chunks and standalone events on all
80+three backends. The pagination and shrink error paths send it without calling
81+`clear_loaded_views`, so loaded thread and pinned caches keep writing against
82+chunk ids that no longer exist.
83+
84+### S9. Reset on the live timeline drops receipts and identities. OPEN
85+
86+The Reset arm of the timeline state transaction clears `read_receipts` and
87+`fully_read_event` and never repopulates them
88+(`timeline/controller/state_transaction.rs:216-231`). It also discards
89+`recycled_timeline_ids`, so every surviving row gets a new id.
90+
91+### S10. A commit hook that never resolves stalls sync for good. OPEN
92+
93+The hook runs inside `handle_response` holding the position lock
94+(`sliding_sync/mod.rs:338`). Nothing can cancel it, and every later
95+`sync_once` and `expire_session` waits on that lock. macltok's hook always
96+resolves today, so this is a hazard in the api, not a live bug.
97+
98+### S11. Smaller sdk findings. OPEN
99+
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 correctness
109+
110+### M1. The mentions inbox rewrites itself on every sync response. OPEN
111+
112+`batch` writes a new checkpoint for every response, typing and receipts
113+included, and `save` serialises, encrypts and durably commits the whole inbox
114+inside the sync hook (`mentions/service.rs`). With the 64 MiB default budget
115+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. OPEN
125+
126+A failed save fails the hook and the hook stops room list sync. An IndexedDB
127+quota error means no messages at all. A corrupt mentions store fails
128+`Account::build`, and the only way out is `StoreConfig::delete`, which also
129+drops the crypto store.
130+
131+### M3. A crash between hook ack and `pos` write raises a false global gap. verified, OPEN
132+
133+The hook records `next_pos` P2 durably, then the process dies before the sdk
134+writes `pos`. On restart the sdk replays with `previous_pos` P1, the checkpoint
135+says P2, and `batch` treats it as a discontinuity, marking every observed room
136+unresolved (`mentions/service.rs:404`). Keeping the previous checkpoint and
137+accepting a replay from it would fix it.
138+
139+### M4. Event context lost what the timeline gave it. OPEN
140+
141+ywsvysot moved `EventContext` from an event-focused sdk timeline to raw
142+`/messages` through `normalise_observed`. That fixed real bugs in the focused
143+path, but items now have no display names or avatars, edits arrive as separate
144+messages, reactions and redactions arrive as `Other`, replies stay
145+`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. OPEN
153+
154+A user Cancel and the cancel that runs on room close both publish
155+`RecentHistoryState::Failed("…interrupted")` and a report with a failure and no
156+stop (`engine/repair.rs:284`, `:347`). `HistoryRepairStop::Cancelled` exists
157+and is never sent.
158+
159+### M6. Network failures report as store errors. OPEN
160+
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 committing
164+`Failed`.
165+
166+### M7. Every history completion sends a full reset. OPEN
167+
168+`publish_history` flushes and then always resyncs (`engine/timeline.rs:134-145`),
169+so every completion, failures and close included, sends a `TimelineReset`, and
170+every room open produces two. The segmented history doc promises incremental
171+insertions. `tests/engine.rs` ignores resets during recent loading, which hides
172+it.
173+
174+### M8. Smaller macltok findings. OPEN
175+
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 design
185+
186+### D1. Most of the history repair api does nothing. OPEN
187+
188+`Start.budget` is validated and ignored. The policy is echoed and stored in the
189+mentions inbox and never read. `Continue` always errors. `StartRecent`
190+duplicates the open-time load. `GetPolicy` and `SetPolicy` in the actor are
191+unreachable because the engine handles them first. Never emitted are
192+`HistoryRepairStrategy::Overlap`, `HistoryRepairStop::Cancelled` and `NoAnchor`,
193+`HistoryRepairPhase::FetchingRecent`, `coverage_duration_ms` and
194+`threshold_evaluated`. Pre-1.0, so delete rather than keep them for serde.
195+
196+### D2. `HistoryGapLifecycle::Fetching` is never emitted. OPEN
197+
198+A consumer has to match a `Bridging` report's `gap_id` to know a gap is
199+filling, then infer the end from a later report. Either emit it on the gap row
200+while a fill runs or delete it.
201+
202+### D3. Mentions shapes. OPEN
203+
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. OPEN
211+
212+`clone_pack_image_with_usage`, `clone_sticker_with_usage` and
213+`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 msc2545
215+extensions, and it duplicates the default-usage rule in
216+`normalise::image_pack::usage`. `Some(vec![])` silently means inherit.
217+
218+### D5. `reseed` has no caller. OPEN
219+
220+`RoomEventCache::reseed`, its state method, `reseed.md` and its tests are about
221+500 lines of fork surface. macltok uses `fill_history_gap_once` and the
222+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. OPEN
226+
227+`EventContext.event` is optional but always present on success.
228+`EventContextPage.end_reached` repeats `next_token.is_none()`. The transport
229+takes `u32` and clamps to 50 while the engine rejects anything over 50.
230+
231+## H. housekeeping
232+
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. OPEN
234+- 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. OPEN
235+- H3. `repair.rs` landed without rustfmt. OPEN
236+- 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. OPEN
237+- 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. OPEN
238+- H6. American spellings in sqlite.rs and the mentions doc. OPEN
239+
240+## Checked and sound
241+
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.