thread/read can report stale updatedAt after replacing SQLite metadata with rollout summary

Open 💬 3 comments Opened Jun 18, 2026 by 100yenadmin

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/codex main

Local evidence

Affected thread id: 019ed1ea-d71a-7bc0-92d5-06d85ce9ba74

SQLite state row:

  • archived = 0
  • rollout_path = ~/.codex/sessions/2026/06/17/rollout-2026-06-17T02-31-29-019ed1ea-d71a-7bc0-92d5-06d85ce9ba74.jsonl
  • created_at = 2026-06-17 02:31:29 +0700
  • updated_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.043Z with payload.type = "turn_aborted"

Observed summary:

  • top-level updatedAt from 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_thread first loads SQLite metadata into stored_thread_from_sqlite_metadata(...).
  • Around read_thread.rs:49-61, for no-history reads, it may replace that DB-derived StoredThread with rollout_thread from read_thread_from_rollout_path(...) when the rollout-derived preview is non-empty.
  • The replacement preserves name and git_info, but not updated_at or other freshness/accounting fields from SQLite.

codex-rs/rollout/src/list.rs:

  • Around list.rs:818-825, read_thread_item_from_rollout(...) calls build_thread_item(...) with updated_at = None.
  • Around list.rs:1075-1160, read_head_summary(...) scans only the head records and does not populate summary.updated_at.
  • Around list.rs:786-790, when summary.updated_at is missing, build_thread_item(...) falls back to the passed updated_at, and then to created_at. Since read_thread_item_from_rollout(...) passes no mtime fallback, single-thread rollout summaries can end up with updatedAt = 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 an updated_at fallback, 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.
  • updatedAt semantics 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗