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.
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Patch branch published for this concrete metadata bug:
Compare: https://github.com/openai/codex/compare/main...100yenadmin:codex/thread-read-updated-at?expand=1
Commit:
b0c5a5ff5e7a47b9df9a23ed0ad57ec6985ba63dI could not open the upstream PR directly because GitHub returned
GraphQL: 100yenadmin does not have the correct permissions to execute CreatePullRequest, but the branch is pushed to100yenadmin/codex.What the patch changes:
updated_atexistsread_threadno-history preview merging preserves SQLite authoritativeupdated_at,recency_at, andarchived_atinstead of letting rollout fallback metadata replace themthread/read includeTurns=falseapp-server coverage asserts the wire response reports the SQLite latest update/recency metadataFocused validation run locally from
/Volumes/LEXAR/repos/codex-worktrees/thread-read-updated-at:just fmtjust test -p codex-rollout test_read_thread_item_from_rollout_uses_file_mtime --test-threads 1just test -p codex-thread-store read_thread_preserves_sqlite_freshness_when_using_rollout_preview --test-threads 1just test -p codex-rollout --test-threads 1just test -p codex-thread-store --test-threads 1just test -p codex-app-server thread_read_returns_summary_without_turns --test-threads 1git diff --checkThis also appears to address at least part of the older recents timestamp symptom in #21960.
@tibo-openai
Confirming the same stale
updatedAtbehavior on a current macOS Desktop build.Environment
26.727.51351(bundle6119)0.146.026.5.2(25F84)Sanitized reproduction
Using the same local idle thread:
list_threads(limit=50).read_threadwith the samethreadIdandhostId=local,turnLimit=1, and omitcursor.Observed:
idleupdatedAt:1785591032(2026-08-01 21:30:32 CST)idleupdatedAt:1785463264(2026-07-31 10:01:04 CST)completedcompletedAt:1785591032hasMore=truewith a realnextCursorThe list timestamp and newest-turn timestamp agree, while the task-level timestamp from
read_threadfalls back to the old value.This reproduced after two full Desktop restarts. Both APIs returned quickly after restart, so the remaining problem appears to be metadata projection/merge consistency rather than connectivity.
A no-logs
/feedbackreport was also recorded under feedback thread019fbfef-2c2c-7782-b873-ec2b79eaccae. No raw rollout logs, prompts, repository content, or business data are attached.This metadata/timestamp divergence between SQLite state rows and rollout record streams is a core scenario handled by Codex Rescue's projection diffing engine.
You can compare layer divergence (rollout JSONL timestamps vs SQLite state DB cursors) using:
Or view a chronological timeline of raw events:
It operates strictly read-only and local-first. If you run it on an affected thread, the diff output could provide a helpful side-by-side view of the timestamp discrepancy.