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

Open 💬 5 comments Opened Jun 18, 2026 by 100yenadmin
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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 ↗

5 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #28867

Powered by Codex Action

100yenadmin · 2 months ago

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: b0c5a5ff5e7a47b9df9a23ed0ad57ec6985ba63d

I 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 to 100yenadmin/codex.

What the patch changes:

  • rollout direct reads now fall back to the rollout file mtime when no summary updated_at exists
  • thread-store read_thread no-history preview merging preserves SQLite authoritative updated_at, recency_at, and archived_at instead of letting rollout fallback metadata replace them
  • thread/read includeTurns=false app-server coverage asserts the wire response reports the SQLite latest update/recency metadata

Focused validation run locally from /Volumes/LEXAR/repos/codex-worktrees/thread-read-updated-at:

  • just fmt
  • just test -p codex-rollout test_read_thread_item_from_rollout_uses_file_mtime --test-threads 1
  • just test -p codex-thread-store read_thread_preserves_sqlite_freshness_when_using_rollout_preview --test-threads 1
  • just test -p codex-rollout --test-threads 1
  • just test -p codex-thread-store --test-threads 1
  • just test -p codex-app-server thread_read_returns_summary_without_turns --test-threads 1
  • git diff --check

This also appears to address at least part of the older recents timestamp symptom in #21960.

100yenadmin · 2 months ago

@tibo-openai

Fengql95 · 26 days ago

Confirming the same stale updatedAt behavior on a current macOS Desktop build.

Environment

  • Codex Desktop / ChatGPT app: 26.727.51351 (bundle 6119)
  • Codex CLI used for the feedback flow: 0.146.0
  • macOS: 26.5.2 (25F84)
  • Observation timezone: Asia/Shanghai

Sanitized reproduction

Using the same local idle thread:

  1. Call list_threads(limit=50).
  2. Call read_thread with the same threadId and hostId=local, turnLimit=1, and omit cursor.
  3. Compare task-level timestamps with the newest turn timestamp.

Observed:

  • list status: idle
  • list updatedAt: 1785591032 (2026-08-01 21:30:32 CST)
  • read status: idle
  • read task-level updatedAt: 1785463264 (2026-07-31 10:01:04 CST)
  • newest turn status: completed
  • newest turn completedAt: 1785591032
  • hasMore=true with a real nextCursor
  • no unavailable hosts or sources

The list timestamp and newest-turn timestamp agree, while the task-level timestamp from read_thread falls 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 /feedback report was also recorded under feedback thread 019fbfef-2c2c-7782-b873-ec2b79eaccae. No raw rollout logs, prompts, repository content, or business data are attached.

shleder · 9 days ago

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:

npx --yes codex-rescue diff --latest

Or view a chronological timeline of raw events:

npx --yes codex-rescue timeline --latest

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.