thread/read can report stale updatedAt after replacing SQLite metadata with rollout summary
Summary
thread/read / thread summary paths can report a stale updatedAt for rollout-backed threads. In a local affected thread, the state DB and rollout file both showed later activity through an aborted turn, but the thread summary reported updatedAt equal to the original creation timestamp.
This looks like a metadata projection bug where fresh SQLite thread metadata can be replaced by a rollout-head summary that does not know the file's modified time or latest event timestamp.
Environment
- Codex Desktop:
26.611.62324 - Bundle version:
4028 - Source checkout inspected:
openai/codexmain
Local evidence
Affected thread id: 019ed1ea-d71a-7bc0-92d5-06d85ce9ba74
SQLite state row:
archived = 0rollout_path = ~/.codex/sessions/2026/06/17/rollout-2026-06-17T02-31-29-019ed1ea-d71a-7bc0-92d5-06d85ce9ba74.jsonlcreated_at = 2026-06-17 02:31:29 +0700updated_at = 2026-06-18 01:14:39 +0700
Filesystem:
- rollout mtime:
2026-06-18 01:14:39 +0700 - last rollout record:
2026-06-17T18:14:39.043Zwithpayload.type = "turn_aborted"
Observed summary:
- top-level
updatedAtfrom a read-thread summary was equal to the creation timestamp rather than the DB/file/latest-event timestamp.
Relevant source pointers
codex-rs/thread-store/src/local/read_thread.rs:
- Around
read_thread.rs:33-48,read_threadfirst loads SQLite metadata intostored_thread_from_sqlite_metadata(...). - Around
read_thread.rs:49-61, for no-history reads, it may replace that DB-derivedStoredThreadwithrollout_threadfromread_thread_from_rollout_path(...)when the rollout-derived preview is non-empty. - The replacement preserves
nameandgit_info, but notupdated_ator other freshness/accounting fields from SQLite.
codex-rs/rollout/src/list.rs:
- Around
list.rs:818-825,read_thread_item_from_rollout(...)callsbuild_thread_item(...)withupdated_at = None. - Around
list.rs:1075-1160,read_head_summary(...)scans only the head records and does not populatesummary.updated_at. - Around
list.rs:786-790, whensummary.updated_atis missing,build_thread_item(...)falls back to the passedupdated_at, and then tocreated_at. Sinceread_thread_item_from_rollout(...)passes no mtime fallback, single-thread rollout summaries can end up withupdatedAt = createdAt.
codex-rs/rollout/src/recorder.rs:
- Around
recorder.rs:1010-1067,fill_missing_thread_item_metadata(...)only fills missing fields from state metadata; it does not override stale rollout-derived fields.
Expected behavior
Thread summary metadata should not regress from DB/file freshness to creation time.
At minimum:
read_thread_from_rollout_path(...)/read_thread_item_from_rollout(...)should use file mtime as anupdated_atfallback, as listing-by-updated-time already does.- If SQLite has a fresher
updated_at, the no-history read path should preserve it when replacing or merging rollout-derived preview fields. updatedAtsemantics should be consistent across thread list, thread read, and Desktop sidebar/detail views.
Why this matters
This bug may be separate from the Desktop lockup tracked in #28867, but it was found while debugging the same class of "old active threads do not reactivate correctly after update" reports. If Desktop relies on stale summary timestamps/status while separately reconstructing recent interrupted turns, it can contribute to confusing thread ordering, stale display state, or incorrect resume/reactivation behavior.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗