Diagnosed and fixed. This comment carries the measurement and the three hypotheses that are **wrong**, so nobody spends the morning I spent on them.
## It is the test clock, not the product
Figures, pasted from the runs. The window is the one this issue reports — send, wait `unread == 1` with `first { }`, `markRead`, wait `unread == 0` — repeated N times in one process, which is how a sub-second flake gets a denominator better than one.
first { } at the 10 s budget the test uses 13 of 30 rounds stuck
re-reading the shared value (the fix) 0 of 30 rounds stuck
first { } on REAL dispatchers, real time 0 of 30 rounds stuck
Earlier arms at a 5 s budget, for shape: 17 of 30, 16 of 30, 18 of 30.
The real-dispatcher column is the one that settles whether anybody's chat list is at risk. It is not. The app and the desktop are not on `StandardTestDispatcher`.
## Three hypotheses, measured and dead
- **Not the subscription lapsing between the two waits.** `first { }` unsubscribes the moment it matches, so the `markRead` write lands with the shared upstream possibly in its `WhileSubscribed` stop window. An arm that keeps one collector attached to `b.groups` for the whole run sticks just the same: `groups saw []` after `markRead`.
- **Not the replay cache.** Same arm.
- **Not #c791680d's `StaticConnectionManager`.** This was my favourite and it is wrong. `DriverFactory().create(null)` is `JdbcSqliteDriver.IN_MEMORY`, which gets one shared `transaction` field where a file url gets `ThreadedConnectionManager` — but a **file-backed** driver gives 17 of 30, the same population. The connection manager is not in it.
## What is true at a stuck round
PROBE round 6 STUCK AT unread==0: store lastRead=7 lastCursor=7 newestInbound=7; COLUMN unread=0 lastMsgCursor=7; list unread=1; inbound=[2/k9, 3/k9, 4/k9, 5/k9, 6/k9, 7/k9]
The store is correct and stays correct: `lastRead` has reached the newest inbound cursor, so `recomputeUnread`'s `m.cursor > last_read_cursor` makes the column 0, and a **fresh collection of the same query reads 0** while the shared list carries 1. `applyIngestion` is one transaction, so the store cannot be caught half-updated, and all five of `groupListFlow`'s inputs are StateFlows, so a restart cannot starve the `combine`.
## The mechanism, and the fix that follows
`AccountSession.groups` is `shareIn(scope, WhileSubscribed(stopTimeoutMillis = 1_000, replayExpirationMillis = 0), replay = 1)` over that `combine`. `first { predicate }` therefore waits for an **emission**, not for a value — and on the test scheduler the emission carrying the value can be folded away before a collector is attached to see it, after which the collector waits for one that is never sent again. Re-reading cannot miss it, because a value that has arrived stays.
The comment already above the failing line said the right thing — "each count is waited for, not read once" — the intent was right and the instrument was wrong.
Worth knowing: the same pattern was in **four** places, not one. `SessionTest` twice, `ChatListOrderTest` twice, `NotificationReplyTest` once. The other three have 20 s budgets, which is why they had not fired; fixing only this one would have left the flake to return under another name.
Also worth knowing for anyone doing this again: app-core's test sources are deliberately on virtual time and the build enforces it — `VirtualTimeCheck`, the hourglass plan's part 5, refuses `runBlocking`, `Thread.sleep`, `Dispatchers.Default` and `Dispatchers.IO`, with `gradle/real-time-tests.txt` as the exception list. The real-dispatcher arm below needs a line there, and that line is scratch.
Measured by cowboy (nprofile…qk7x) in the pr-trough room.