Bug: UpdatedAt pagination skips valid sessions — sort key (mtime) and cursor comparison key diverge in should_skip()
Summary
When listing threads sorted by UpdatedAt, the pagination logic in codex-rs/rollout/src/list.rs can silently skip valid sessions on page 2+. The root cause is a mismatch between the sort key used to order candidates (file mtime) and the key used in the cursor anchor comparison (should_skip()), which breaks the transitivity assumption the comparison relies on.
Root Cause
File: codex-rs/rollout/src/list.rs
Functions: traverse_directories_for_paths_updated(), traverse_flat_paths_updated(), AnchorState::should_skip(), build_next_cursor()
Candidates are sorted by mtime descending:
candidates.sort_by_key(|candidate| {
let ts = candidate.updated_at.unwrap_or(OffsetDateTime::UNIX_EPOCH);
(Reverse(ts), Reverse(candidate.id))
});
should_skip() then compares each candidate's (ts, id) against the cursor anchor, assuming that once a candidate with ts < cursor.ts is seen, all subsequent candidates also satisfy that condition (i.e. the sequence is transitive). This holds only if the sort key and the comparison key are the same value.
But build_next_cursor() for UpdatedAt builds the cursor from last.updated_at parsed from the file contents — not directly from the mtime used during sorting. If those two values differ for any session (e.g. metadata written slightly after the file was last touched, clock skew, or a backfill operation), the cursor anchor and the sort order are no longer aligned.
Concrete Example
- Session A: mtime =
2025-01-03T15:30:00Z, updated_at in file =2025-01-03T15:29:58Z - Session B: mtime =
2025-01-02T14:00:00Z, updated_at in file =2025-01-02T14:00:00Z
Page 1 returns [A]. Cursor is built from A's updated_at: (2025-01-03T15:29:58Z, A_uuid).
Page 2 anchor scan hits B:
should_skip(2025-01-02T14:00:00Z, B_uuid)→ts < cursor.ts→ true, skip- But B has never been returned — it should be on this page.
B is permanently invisible in paginated results.
Impact
- Paginated
thread/listwithsort=UpdatedAtcan return incomplete results - Sessions are silently dropped — no error, no indication pages are incomplete
- Affects both
traverse_directories_for_paths_updated()andtraverse_flat_paths_updated() - Related but distinct from #16545 (scan cap bug, same file)
Proposed Fix
The sort key and cursor comparison key must be the same value. Two options:
- Build the cursor from the same mtime used during sort — store the raw mtime on the candidate struct and use it in
build_next_cursor()instead of re-parsingupdated_atfrom file contents - Sort by the parsed
updated_atfield and ensure that field is always populated before sorting, so the sort key and cursor key are guaranteed identical
8 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Not a duplicate. #16624 is a user report about the VS Code extension task picker dropping sessions client-side. This issue is about a server-side logic bug in
codex-rs/rollout/src/list.rs— specifically thatshould_skip()in theUpdatedAtpagination path can skip valid sessions because the sort key (mtime) and the cursor anchor comparison key (updated_at parsed from file contents) are not guaranteed to be the same value, breaking the transitivity assumption the anchor scan relies on. The bug exists in the Rust pagination code regardless of which client is consuming the API.Can you provide more details as requested in the bug report template? Which Codex client are you using (TUI, app, extension)? Which version? What are the repro steps?
I appreciate your analysis of the problem, but it's useful if you start with the actual description of the problem so we can assess user impact (and therefore priority).
Version: codex-cli 0.118.0
Platform: Darwin 25.3.0 arm64 arm
What issue are you seeing?
When paginating
thread/listsorted byUpdatedAt, valid sessions are silently skipped on page 2+. No error is returned — the page just comes back missing sessions that should appear.What steps can reproduce the bug?
updated_atvalue in the file to differ slightly from its mtime (this happens naturally viareconcile_rollout())thread/listwithsort=UpdatedAtandlimit=1— A is returned, cursor is built from A'supdated_atvalueshould_skip()using its mtime. SinceB.mtime < cursor.ts(A'supdated_at), it is skipped even though it was never returnedWhat is the expected behavior?
B should appear on page 2.
Additional information:
Root cause is in
codex-rs/rollout/src/list.rs— the sort key (mtime) and cursor comparison key (updated_atparsed from file) are not always the same value, breaking the transitivity assumption inshould_skip(). Same applies totraverse_flat_paths_updated().Are you using the TUI, or are you using some other custom app server client?
TUI, no custom client.
Can you provide a set of steps to reproduce the bug in the TUI? The steps you list above reference internal implementation details and code paths, but I'm interested in understanding what steps a user would take to see this bug.
I've looked into your analysis, and I don't think it pans out.
UpdatedAtsorting, anchor checks, and the fallbackThreadItem.updated_atall come from file mtime in list.rs:500, and read_head_summary() never fills summary.updated_at, so the claimed “mtime sort vs parsed updated_at cursor” split isn’t actually there. There's also a test (test_updated_at_uses_file_mtime) that specifically validates this condition.