Problem
Found during Codex review of PR #265 (issue #261).
DM timestamps are stored in whole Unix seconds. The hide-filter uses strict <=:
// common/src/chat_delegate.rs::is_thread_hidden
hidden_threads
.iter()
.find(|h| &h.room_owner_vk == room_owner_vk && h.peer == peer)
.is_some_and(|h| max_message_ts <= h.hidden_at_ts)
This is correct for the message that seeded hidden_at_ts — that exact message must not revive the thread it was used to dismiss. But it has a corner case: if a peer sends an inbound DM in the same Unix second the user clicked Hide, max_message_ts == hidden_at_ts and the thread stays hidden. The peer's message is genuinely new (different content), but the filter treats it as "the message that was already there when I hid."
The outbound path explicitly works around this race by calling unhide_dm_thread after a successful send (dm_thread_modal::do_send, Codex P1 fix during PR #265 review). The inbound path has no equivalent — so a same-second inbound DM gets stranded until a later-second DM arrives.
Reproduction
- User has a DM thread with peer; latest message at ts=N.
- User clicks Hide →
hidden_at_ts = N.
- Peer sends a DM that lands with
timestamp = N (any time within the same Unix second).
- Rail filter checks
N <= N → true → thread stays hidden.
- The unread DM does NOT surface in the rail. It only surfaces when a later-second DM arrives.
Probability: real but narrow — the peer's message needs to land within the same Unix second the user clicked Hide. On a busy thread, low but non-zero. On a quiet thread the user is intentionally dismissing, also low but non-zero.
Suggested fix
Mirror the outbound-revival path. When the room synchronizer applies an inbound DM:
- For each new inbound DM message
(room, peer, ts):
- If
HIDDEN_DM_THREADS has an entry for (room, peer) with hidden_at_ts == ts, call unhide_dm_thread(room, peer).
This is symmetric with dm_thread_modal::do_send's call after save_outbound_dm. The tombstone path (RECENTLY_UNHIDDEN) already handles the cross-session race.
Alternative: change hidden_at_ts to store last_seen_ts and use strict <. Wire-format-breaking — requires a V2 store struct + migration, so the symmetric-unhide-on-inbound fix is cheaper.
Severity
P2 (Codex's own rating). Hidden DMs are recoverable — any later-second DM revives the thread, and the user can also click the peer in the members list to open the thread directly. But for the duration of the same-second window, a notification-worthy inbound DM is silently swallowed.
References
[AI-assisted - Claude]
Problem
Found during Codex review of PR #265 (issue #261).
DM timestamps are stored in whole Unix seconds. The hide-filter uses strict
<=:This is correct for the message that seeded
hidden_at_ts— that exact message must not revive the thread it was used to dismiss. But it has a corner case: if a peer sends an inbound DM in the same Unix second the user clicked Hide,max_message_ts == hidden_at_tsand the thread stays hidden. The peer's message is genuinely new (different content), but the filter treats it as "the message that was already there when I hid."The outbound path explicitly works around this race by calling
unhide_dm_threadafter a successful send (dm_thread_modal::do_send, Codex P1 fix during PR #265 review). The inbound path has no equivalent — so a same-second inbound DM gets stranded until a later-second DM arrives.Reproduction
hidden_at_ts = N.timestamp = N(any time within the same Unix second).N <= N→ true → thread stays hidden.Probability: real but narrow — the peer's message needs to land within the same Unix second the user clicked Hide. On a busy thread, low but non-zero. On a quiet thread the user is intentionally dismissing, also low but non-zero.
Suggested fix
Mirror the outbound-revival path. When the room synchronizer applies an inbound DM:
(room, peer, ts):HIDDEN_DM_THREADShas an entry for(room, peer)withhidden_at_ts == ts, callunhide_dm_thread(room, peer).This is symmetric with
dm_thread_modal::do_send's call aftersave_outbound_dm. The tombstone path (RECENTLY_UNHIDDEN) already handles the cross-session race.Alternative: change
hidden_at_tsto storelast_seen_tsand use strict<. Wire-format-breaking — requires a V2 store struct + migration, so the symmetric-unhide-on-inbound fix is cheaper.Severity
P2 (Codex's own rating). Hidden DMs are recoverable — any later-second DM revives the thread, and the user can also click the peer in the members list to open the thread directly. But for the duration of the same-second window, a notification-worthy inbound DM is silently swallowed.
References
common/src/chat_delegate.rs::is_thread_hidden— the strict-<=check.ui/src/components/direct_messages/dm_thread_modal.rs::do_send— the outbound-side mirror of the fix this issue proposes.[AI-assisted - Claude]