Bug: Thread history data loss on restart — async persistence not flushed on shutdown (RolloutRecorder, SQLite WAL, session_index)

Resolved 💬 4 comments Opened Apr 3, 2026 by aamodbhatt Closed Apr 3, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

Thread history is silently lost after app restart (reported in openai/codex#16599) because three async persistence layers can all have in-flight writes when the process terminates. There is no shutdown barrier that waits for them to complete.

Root Cause

The persistence pipeline for a session looks like this:

App → RolloutRecorder (async mpsc, 256-item buffer) → background writer → JSONL
    → StateRuntime (SQLite in WAL mode) → state.db
    → session_index (append-only file)

All three writes proceed concurrently and none are guaranteed to flush before process exit.

1\. RolloutRecorder drops buffered items on shutdown

File: codex-rs/rollout/src/recorder.rs

Items are queued into a bounded 256-item tokio::sync::mpsc channel and consumed by a background writer task. If the process is killed (SIGTERM, system restart, OS interruption during a permission prompt) before that task drains the channel, the queued items are dropped. There is no sync_all() / fsync() and no mechanism to await the task's completion before exit.

2\. SQLite WAL not checkpointed on process exit

File: codex-rs/state/src/ (STATE_DB_VERSION = 5)

The state database runs in WAL mode. Writes accumulate in the -wal file and are only checkpointed into the main database on a clean close or explicit PRAGMA wal_checkpoint. An abrupt exit leaves the WAL uncheckpointed, so on next startup thread metadata (timestamps, titles, status) reverts to the last successful checkpoint — which can lag behind the actual rollout by many transactions.

3\. Incomplete session_index write breaks sub-agent thread discovery

File: codex-rs/rollout/src/session_index.rs

The session index is append-only and backward-scanned at startup by find_thread_path_by_id_str(). Sub-agent thread IDs are only discoverable if their index entry was fully written and flushed. A mid-write process exit produces a truncated or absent entry, making sub-agent threads completely invisible on restart — even though their JSONL rollout files still exist on disk. This explains why sub-agent histories disappear entirely while the main thread partially recovers.

Proposed Fix

On graceful shutdown, in order:

  1. Drop the sender half of the RolloutRecorder channel and await the writer task to fully drain before returning
  2. Call PRAGMA wal_checkpoint(TRUNCATE) on the SQLite connection before closing the pool
  3. Ensure session_index appends call sync_all() after each write, or are sequenced behind the recorder flush

For non-graceful exits, a startup recovery pass that reconciles a partial JSONL rollout via reconcile_rollout() against the WAL could recover data that's on disk but not yet indexed.

References

  • User report: openai/codex#16599

View original on GitHub ↗

4 Comments

github-actions[bot] contributor · 4 months ago

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

  • ##16599
  • ##15349
  • ##16300

Powered by Codex Action

aamodbhatt · 4 months ago

The flagged issues (#16599, #15349, #16300) are user-facing reports of the symptom (history disappeared). This issue is distinct, it identifies the root cause in the persistence layer and proposes a concrete fix.

The linked issues don't contain source level analysis. This is not a duplicate.

etraut-openai contributor · 4 months ago

Thanks for the analysis. Please add it to the existing bug report rather than open a new one. We generally prioritize bug fixes based on user interactions (upvotes, posts, etc.), so opening redundant bug reports can cause bugs to never be prioritized.

shleder · 6 days ago

Excellent root-cause breakdown — the three-layer async pipeline (RolloutRecorder mpsc buffer → SQLite WAL → session_index) with no shutdown barrier is exactly the failure mode that makes "history disappeared" reports so hard to triage.

One thing that helps when you're trying to prove which layer lost data for a specific session: you can diff the three persistence layers against each other read-only. The rollout JSONL is the canonical append-only record, the SQLite state.db is a derived projection, and session_index is a separate append log — when they disagree, the divergence pattern tells you whether the loss was a dropped mpsc tail, an uncheckpointed WAL, or a truncated index scan.

Full disclosure: I maintain codex-rescue (https://github.com/shleder/codex-rescue), a read-only-first diagnostic for exactly this. codex-rescue diff --latest reconciles rollout vs SQLite vs git state and flags projection divergence, and doctor classifies truncated-EOF vs malformed records. It never mutates the source, so it's safe to run on the affected session to confirm the loss boundary before proposing the shutdown-barrier fix. Happy to share the divergence signature if it helps the fix.