You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Session disappear. Codex trips over bad JSON history. #24425
Solution: Codex Issue #24425 — Session Disappear. Codex Trips Over Bad JSON History
Executive Summary
Sessions disappear from Codex's thread listing when the rollout .jsonl file contains corrupted or malformed JSON lines. The root cause is that bad JSON in the session metadata line (the first line of the rollout file) causes the head-summary reader to return empty results, which filters out the entire session from listings. While individual line parsing already skips bad JSON, the first-line session metadata is critical — without it, the session has no thread_id, no preview, and no saw_session_meta flag, causing it to be silently excluded from all listings.
1. Root Cause Analysis
1.1 How Session History Works
Codex stores session history as JSONL (JSON Lines) files in ~/.codex/sessions/. Each line is a JSON object representing a RolloutLine:
The first line is always a session_meta entry containing the thread ID, working directory, model provider, and other critical metadata.
1.2 The Failure Chain
The session disappearance occurs through this cascade:
Corrupted JSONL file
↓
read_head_summary() in list.rs
↓ (bad JSON line → `let Ok(rollout_line) = parsed else { continue }`)
Empty HeadTailSummary (saw_session_meta=false, preview=None)
↓
build_thread_item() filters out session (requires saw_session_meta && preview)
↓
Session disappears from thread listing
↓
read_session_meta_line() returns Err (head.first() is None)
↓
read_thread_from_rollout_path() → stored_thread_from_session_meta() → Err
↓
Session invisible to both listing AND direct read
1.3 Key Code Locations
File
Function
Issue
rollout/src/list.rs:1075
read_head_summary()
Silently skips bad JSON; no recovery for corrupted session_meta
rollout/src/list.rs:736
build_thread_item()
Requires saw_session_meta && preview — both fail on bad first line
rollout/src/list.rs:1165
read_head_for_summary()
Returns empty Vec when first lines are bad JSON
rollout/src/list.rs:1237
read_session_meta_line()
Returns Err when head is empty
rollout/src/recorder.rs:812
load_rollout_items()
Already handles bad JSON per-line, but returns thread_id=None if session_meta is corrupted
thread-store/src/local/read_thread.rs:203
read_thread_from_rollout_path()
Falls back to stored_thread_from_session_meta() which also needs session_meta
1.4 Why Sessions "Disappear" Rather Than Showing an Error
The current code uses silent filtering in multiple places:
build_thread_item() returns None when saw_session_meta is false — the session is silently excluded from listings
read_head_summary() uses unwrap_or_default() on I/O errors — corrupted files look like empty files
read_session_meta_line() returns Err when the head is empty — callers propagate this as "no session found"
read_thread_from_rollout_path() falls back through multiple paths, each of which can silently fail
The result: a corrupted session file is invisible everywhere — not in listings, not in direct reads, and not recoverable without manual intervention.
1.5 Corruption Scenarios
Scenario
First Line Valid?
Session Visible?
Bad JSON in middle lines
Yes
Yes (bad lines skipped)
Truncated file (no newline)
Yes (partial)
Partial (depends on parse)
Bad JSON in first line
No
No
Binary garbage in first line
No
No
Empty file
N/A
No
File with only whitespace
N/A
No
State DB has metadata but rollout corrupted
N/A
Partial (SQLite fallback)
2. Fix: Graceful Handling of Corrupted JSON History
2.1 Overview
The fix introduces three layers of resilience:
Line-level recovery: Extract thread ID from filename when session_meta is corrupted
Summary-level recovery: Build a degraded summary from available data
Listing-level recovery: Include sessions with degraded metadata in listings
2.2 Changes to rollout/src/list.rs
2.2.1 Add filename-based thread ID extraction
// NEW: Extract thread ID from rollout filename when JSON is corruptedfnextract_thread_id_from_filename(path:&Path) -> Option<ThreadId>{let file_name = path.file_name()?.to_str()?;// Format: rollout-YYYY-MM-DDThh-mm-ss-{uuid}.jsonlif !file_name.starts_with("rollout-") || !file_name.ends_with(".jsonl"){returnNone;}let stem = &file_name["rollout-".len()..file_name.len() - ".jsonl".len()];// Split on last '-' to separate timestamp from UUIDlet last_dash = stem.rfind('-')?;let uuid_str = &stem[last_dash + 1..];let uuid = Uuid::parse_str(uuid_str).ok()?;ThreadId::from_string(&uuid.to_string()).ok()}
2.2.2 Add degraded summary builder
// NEW: Build a degraded summary from filename metadata when JSON is corruptedfnbuild_degraded_summary(path:&Path,updated_at:Option<String>,) -> Option<HeadTailSummary>{let file_name = path.file_name()?.to_str()?;let(created_ts, uuid) = parse_timestamp_uuid_from_filename(file_name)?;let thread_id = ThreadId::from_string(&uuid.to_string()).ok()?;let created_at = created_ts.format(&Rfc3339).ok()?;Some(HeadTailSummary{saw_session_meta:false,// Mark as degradedthread_id:Some(thread_id),preview:Some("[Corrupted session — metadata unavailable]".to_string()),first_user_message:None,cwd:None,git_branch:None,git_sha:None,git_origin_url:None,source:Some(SessionSource::Unknown),agent_nickname:None,agent_role:None,model_provider:None,cli_version:None,created_at:Some(created_at),
updated_at,})}
2.2.3 Modify build_thread_item() to accept degraded summaries
asyncfnbuild_thread_item(path:PathBuf,allowed_sources:&[SessionSource],provider_matcher:Option<&ProviderMatcher<'_>>,cwd_filters:Option<&[PathBuf]>,updated_at:Option<String>,) -> Option<ThreadItem>{let summary = read_head_summary(&path,HEAD_RECORD_LIMIT).await.ok().filter(|s| s.saw_session_meta || s.preview.is_some()).or_else(|| build_degraded_summary(&path, updated_at.clone()))?;// ... existing filter logic, but allow degraded sessions through ...// MODIFIED: Accept sessions with degraded metadata// Original: if summary.saw_session_meta && summary.preview.is_some()// New: if summary.preview.is_some() (thread_id may come from filename)if summary.preview.is_some(){// ... build ThreadItem ...}None}
2.2.4 Modify read_session_meta_line() to fall back to filename metadata
pubasyncfnread_session_meta_line(path:&Path) -> io::Result<SessionMetaLine>{let head = read_head_for_summary(path).await?;letSome(first) = head.first()else{// NEW: Fall back to building a minimal SessionMetaLine from filenamereturnbuild_session_meta_from_filename(path).ok_or_else(|| {
io::Error::other(format!("rollout at {} is empty and has no valid filename metadata",
path.display()))});};
serde_json::from_value::<SessionMetaLine>(first.clone()).map_err(|_| {// NEW: Fall back to filename metadata when first line is bad JSONbuild_session_meta_from_filename(path).ok_or_else(|| {
io::Error::other(format!("rollout at {} does not start with session metadata and filename metadata unavailable",
path.display()))})})}// NEW: Build a minimal SessionMetaLine from filenamefnbuild_session_meta_from_filename(path:&Path) -> Option<SessionMetaLine>{let file_name = path.file_name()?.to_str()?;let(created_ts, uuid) = parse_timestamp_uuid_from_filename(file_name)?;let thread_id = ThreadId::from_string(&uuid.to_string()).ok()?;let timestamp = created_ts.format(&Rfc3339).ok()?;Some(SessionMetaLine{meta:SessionMeta{id: thread_id,forked_from_id:None,timestamp: timestamp.clone(),cwd:PathBuf::new(),originator:String::new(),cli_version:String::new(),source:SessionSource::Unknown,thread_source:None,agent_path:None,agent_nickname:None,agent_role:None,model_provider:None,base_instructions:None,dynamic_tools:None,memory_mode:None,},git:None,})}
2.3 Changes to rollout/src/recorder.rs
2.3.1 Improve load_rollout_items() thread ID recovery
pubasyncfnload_rollout_items(path:&Path,) -> std::io::Result<(Vec<RolloutItem>,Option<ThreadId>,usize)>{// ... existing code ...// NEW: After parsing, if thread_id is None, try to extract from filenameif thread_id.is_none(){ifletSome(extracted) = extract_thread_id_from_filename(path){trace!("Recovered thread ID from filename for {}: {:?}",
path.display(),
extracted
);
thread_id = Some(extracted);}}// ... rest of existing code ...}
2.3.2 Add corrupted session detection and warning
// NEW: After loading items, warn if session metadata was corruptedfndetect_corrupted_session_meta(items:&[RolloutItem],thread_id:Option<ThreadId>,path:&Path,parse_errors:usize,){let has_session_meta = items.iter().any(|item| matches!(item,RolloutItem::SessionMeta(_)));if !has_session_meta && parse_errors > 0{warn!("Session {} has no valid session_meta line ({} parse errors). \ Thread ID recovered from filename: {:?}. \ Consider running session recovery.",
path.display(),
parse_errors,
thread_id,);}}
2.4 Changes to thread-store/src/local/read_thread.rs
2.4.1 Improve error handling in read_thread_from_rollout_path()
asyncfnread_thread_from_rollout_path(store:&LocalThreadStore,path: std::path::PathBuf,) -> ThreadStoreResult<StoredThread>{letSome(item) = read_thread_item_from_rollout(path.clone()).awaitelse{// NEW: Try degraded summary before falling back to session_metaifletSome(degraded) = build_degraded_thread_from_filename(&path,&store.config){returnOk(degraded);}returnstored_thread_from_session_meta(store, path).await;};// ... rest of existing code ...}// NEW: Build a degraded StoredThread from filename when JSON is corruptedfnbuild_degraded_thread_from_filename(path:&std::path::Path,config:&LocalThreadStoreConfig,) -> Option<StoredThread>{let file_name = path.file_name()?.to_str()?;let(created_ts, uuid) = parse_timestamp_uuid_from_filename(file_name)?;let thread_id = ThreadId::from_string(&uuid.to_string()).ok()?;let created_at = DateTime::<Utc>::from_timestamp(created_ts.unix_timestamp(),0)?;Some(StoredThread{
thread_id,rollout_path:Some(path.to_path_buf()),forked_from_id:None,preview:"[Corrupted session — metadata unavailable]".to_string(),name:None,model_provider: config.default_model_provider_id.clone(),model:None,reasoning_effort:None,
created_at,updated_at: created_at,archived_at:None,cwd:PathBuf::new(),cli_version:String::new(),source:SessionSource::Unknown,thread_source:None,agent_nickname:None,agent_role:None,agent_path:None,git_info:None,approval_mode:AskForApproval::OnRequest,sandbox_policy:SandboxPolicy::new_read_only_policy(),token_usage:None,first_user_message:None,history:None,})}
2.5 Changes to rollout/src/state_db.rs
2.5.1 Improve reconcile_rollout() error handling for corrupted files
pubasyncfnreconcile_rollout(context:Option<&codex_state::StateRuntime>,rollout_path:&Path,default_provider:&str,builder:Option<&ThreadMetadataBuilder>,items:&[RolloutItem],archived_only:Option<bool>,new_thread_memory_mode:Option<&str>,){// ... existing code ...let outcome = match metadata::extract_metadata_from_rollout(rollout_path, default_provider).await{Ok(outcome) => outcome,Err(err) => {// NEW: Try to recover from filename metadatawarn!("state db reconcile_rollout extraction failed {}: {err}. \ Attempting filename-based recovery.",
rollout_path.display());ifletSome(recovered) = try_recover_metadata_from_filename(rollout_path, default_provider){warn!("Recovered basic metadata from filename for {}", rollout_path.display());// Continue with recovered metadata instead of returning early// ... upsert recovered metadata ...return;}return;}};// ... rest of existing code ...}// NEW: Try to recover minimal metadata from filenamefntry_recover_metadata_from_filename(path:&Path,default_provider:&str,) -> Option<ThreadMetadata>{let file_name = path.file_name()?.to_str()?;let(created_ts, uuid) = parse_timestamp_uuid_from_filename(file_name)?;let thread_id = ThreadId::from_string(&uuid.to_string()).ok()?;let created_at = chrono::DateTime::<Utc>::from_timestamp(created_ts.unix_timestamp(),0)?;letmut builder = ThreadMetadataBuilder::new(
thread_id,
path.to_path_buf(),
created_at,SessionSource::Unknown,);
builder.model_provider = Some(default_provider.to_string());letmut metadata = builder.build(default_provider);
metadata.preview = Some("[Corrupted session — metadata unavailable]".to_string());
metadata.title = "Corrupted Session".to_string();Some(metadata)}
3. Recovery Mechanism for Corrupted Sessions
3.1 Automatic Recovery on Listing
With the fixes above, corrupted sessions are automatically visible in listings with degraded metadata. The session appears with:
# List corrupted sessions
codex sessions --corrupted
# Attempt to repair a specific session
codex sessions repair <thread-id># Attempt to repair all corrupted sessions
codex sessions repair --all
Implementation sketch:
// NEW: In codex-rs/cli or codex-rs/corepubasyncfnrepair_corrupted_session(codex_home:&Path,thread_id:ThreadId,) -> Result<RepairOutcome,RepairError>{// 1. Find the rollout filelet rollout_path = find_rollout_for_thread(codex_home, thread_id).await?;// 2. Try to load what we canlet(items, loaded_id, parse_errors) = RolloutRecorder::load_rollout_items(&rollout_path).await?;// 3. If thread_id doesn't match, try to fixif loaded_id != Some(thread_id){returnErr(RepairError::ThreadIdMismatch{expected: thread_id,found: loaded_id,});}// 4. If session_meta is missing, rebuild itlet has_session_meta = items.iter().any(|i| matches!(i,RolloutItem::SessionMeta(_)));if !has_session_meta {// Rebuild session_meta from available datalet rebuilt_meta = rebuild_session_meta(thread_id,&rollout_path).await?;// 5. Write repaired file (atomic: write to temp, then rename)let temp_path = rollout_path.with_extension("jsonl.repairing");write_repaired_rollout(&temp_path,&rebuilt_meta,&items).await?;
tokio::fs::rename(&temp_path,&rollout_path).await?;Ok(RepairOutcome::Repaired{ parse_errors })}else{Ok(RepairOutcome::AlreadyValid{ parse_errors })}}
3.3 Recovery via State DB
When the state DB has metadata for a corrupted session, the system should:
Use the state DB metadata as the source of truth
Mark the session as "degraded" in the UI
Attempt to repair the rollout file on next write
// In read_thread.rs, when SQLite metadata exists but rollout is corrupted:asyncfnstored_thread_from_sqlite_metadata_with_fallback(store:&LocalThreadStore,metadata:ThreadMetadata,rollout_path:&Path,) -> StoredThread{let thread = stored_thread_from_sqlite_metadata(store, metadata.clone()).await;// Check if rollout is corruptedlet rollout_ok = RolloutRecorder::load_rollout_items(rollout_path).await.map(|(_, _, parse_errors)| parse_errors == 0).unwrap_or(false);if !rollout_ok {// Mark as degradedwarn!("Session {} has corrupted rollout; using SQLite metadata", metadata.id);// Schedule async repairschedule_rollout_repair(store.config.codex_home.clone(), metadata);}
thread
}
4. Testing Approach
4.1 Unit Tests
Test 1: Corrupted first line — session still visible in listing
#[tokio::test]asyncfnlisting_shows_session_with_corrupted_first_line(){let home = TempDir::new().expect("temp dir");let config = test_config(home.path());let store = LocalThreadStore::new(config,None);let uuid = Uuid::from_u128(4242);let day_dir = home.path().join("sessions/2025/01/03");
std::fs::create_dir_all(&day_dir).expect("sessions dir");let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));// Write corrupted first line + valid second lineletmut file = std::fs::File::create(&rollout_path).expect("create file");writeln!(file,"{{INVALID JSON HERE").expect("write corrupted line");let user_event = serde_json::json!({"timestamp":"2025-01-03T12:00:00Z","type":"event_msg","payload":{"type":"user_message","message":"Hello from user","kind":"plain",},});writeln!(file,"{user_event}").expect("write user event");let page = store.list_threads(ListThreadsParams{page_size:10,cursor:None,sort_key:ThreadSortKey::CreatedAt,sort_direction:SortDirection::Desc,allowed_sources:Vec::new(),model_providers:None,cwd_filters:None,archived:false,search_term:None,use_state_db_only:false,}).await.expect("list threads");// Session should be visible with degraded metadataassert_eq!(page.items.len(),1);assert_eq!(page.items[0].thread_id,ThreadId::from_string(&uuid.to_string()).ok());assert!(page.items[0].preview.as_ref().unwrap().contains("Corrupted"));}
Test 2: Corrupted middle lines — session visible, bad lines skipped
#[tokio::test]asyncfnlisting_shows_session_with_corrupted_middle_lines(){let home = TempDir::new().expect("temp dir");let uuid = Uuid::from_u128(4243);let day_dir = home.path().join("sessions/2025/01/03");
std::fs::create_dir_all(&day_dir).expect("sessions dir");let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));letmut file = std::fs::File::create(&rollout_path).expect("create file");// Valid session_metalet meta = serde_json::json!({"timestamp":"2025-01-03T12:00:00Z","type":"session_meta","payload":{"id": uuid,"timestamp":"2025-01-03T12:00:00Z","cwd":".","originator":"test","cli_version":"test","source":"cli","model_provider":"test-provider",},});writeln!(file,"{meta}").expect("write meta");// Corrupted middle linewriteln!(file,"{{BROKEN JSON").expect("write broken line");// Valid user messagelet user_event = serde_json::json!({"timestamp":"2025-01-03T12:00:01Z","type":"event_msg","payload":{"type":"user_message","message":"Hello","kind":"plain",},});writeln!(file,"{user_event}").expect("write user event");// load_rollout_items should succeed with parse_errors = 1let(items, thread_id, parse_errors) = RolloutRecorder::load_rollout_items(&rollout_path).await.expect("load items");assert_eq!(parse_errors,1);assert_eq!(thread_id,Some(ThreadId::from_string(&uuid.to_string()).unwrap()));assert_eq!(items.len(),2);// session_meta + user_message}
Test 3: Thread ID recovery from filename
#[tokio::test]asyncfnload_rollout_items_recovers_thread_id_from_filename(){let home = TempDir::new().expect("temp dir");let uuid = Uuid::from_u128(4244);let rollout_path = home.path().join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));letmut file = std::fs::File::create(&rollout_path).expect("create file");// All lines are corruptedwriteln!(file,"{{INVALID1").expect("write");writeln!(file,"{{INVALID2").expect("write");writeln!(file,"{{INVALID3").expect("write");let(items, thread_id, parse_errors) = RolloutRecorder::load_rollout_items(&rollout_path).await.expect("load items");assert_eq!(parse_errors,3);assert!(items.is_empty());// Thread ID should be recovered from filenameassert_eq!(thread_id,Some(ThreadId::from_string(&uuid.to_string()).unwrap()));}
Test 4: read_session_meta_line fallback to filename
#[tokio::test]asyncfnread_session_meta_line_falls_back_to_filename(){let home = TempDir::new().expect("temp dir");let uuid = Uuid::from_u128(4245);let rollout_path = home.path().join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));letmut file = std::fs::File::create(&rollout_path).expect("create file");writeln!(file,"{{CORRUPTED FIRST LINE").expect("write");let result = read_session_meta_line(&rollout_path).await;// Should succeed with degraded metadata from filenameassert!(result.is_ok());let meta_line = result.unwrap();assert_eq!(meta_line.meta.id,ThreadId::from_string(&uuid.to_string()).unwrap());assert_eq!(meta_line.meta.source,SessionSource::Unknown);}
Test 5: Empty file handling
#[tokio::test]asyncfnempty_rollout_file_returns_error(){let home = TempDir::new().expect("temp dir");let uuid = Uuid::from_u128(4246);let rollout_path = home.path().join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
std::fs::File::create(&rollout_path).expect("create empty file");let result = RolloutRecorder::load_rollout_items(&rollout_path).await;assert!(result.is_err());}
Test 6: Binary garbage file
#[tokio::test]asyncfnbinary_garbage_file_recovers_from_filename(){let home = TempDir::new().expect("temp dir");let uuid = Uuid::from_u128(4247);let rollout_path = home.path().join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));// Write binary garbage
std::fs::write(&rollout_path,&[0x00,0xFF,0xFE,0x00,0xDE,0xAD,0xBE,0xEF]).expect("write binary");let(items, thread_id, parse_errors) = RolloutRecorder::load_rollout_items(&rollout_path).await.expect("load items");assert_eq!(parse_errors,1);// One "line" of binary garbageassert!(items.is_empty());// Thread ID recovered from filenameassert_eq!(thread_id,Some(ThreadId::from_string(&uuid.to_string()).unwrap()));}
4.2 Integration Tests
Test 7: Full session lifecycle with corrupted file
#[tokio::test]asyncfncorrupted_session_visible_in_listing_and_readable(){let home = TempDir::new().expect("temp dir");let config = test_config(home.path());let store = LocalThreadStore::new(config.clone(),None);let uuid = Uuid::from_u128(4248);let day_dir = home.path().join("sessions/2025/01/03");
std::fs::create_dir_all(&day_dir).expect("sessions dir");let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));// Write corrupted fileletmut file = std::fs::File::create(&rollout_path).expect("create");writeln!(file,"{{CORRUPTED").expect("write");let user_event = serde_json::json!({"timestamp":"2025-01-03T12:00:00Z","type":"event_msg","payload":{"type":"user_message","message":"Test message","kind":"plain",},});writeln!(file,"{user_event}").expect("write");// 1. Session should appear in listinglet page = store.list_threads(default_list_params()).await.expect("list");assert_eq!(page.items.len(),1);// 2. Session should be readable by thread_idlet thread_id = ThreadId::from_string(&uuid.to_string()).unwrap();let thread = store.read_thread(ReadThreadParams{
thread_id,include_archived:false,include_history:false,}).await.expect("read thread");assert_eq!(thread.thread_id, thread_id);assert!(thread.preview.contains("Corrupted"));}
Test 8: State DB reconciliation with corrupted rollout
#[tokio::test]asyncfnstate_db_reconcile_handles_corrupted_rollout(){let home = TempDir::new().expect("temp dir");let config = test_config(home.path());let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),).await.expect("init state db");let uuid = Uuid::from_u128(4249);let day_dir = home.path().join("sessions/2025/01/03");
std::fs::create_dir_all(&day_dir).expect("sessions dir");let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));// Write corrupted file
std::fs::write(&rollout_path,"{{CORRUPTED\n{{MORE\n").expect("write");// Reconcile should not crashreconcile_rollout(Some(&runtime),&rollout_path,&config.model_provider_id,None,&[],None,None,).await;// Metadata should be recoverable from filenamelet thread_id = ThreadId::from_string(&uuid.to_string()).unwrap();let metadata = runtime.get_thread(thread_id).await.expect("get thread");assert!(metadata.is_some());let metadata = metadata.unwrap();assert_eq!(metadata.id, thread_id);assert!(metadata.preview.unwrap().contains("Corrupted"));}
4.3 Regression Tests
Test 9: Valid sessions still work correctly
#[tokio::test]asyncfnvalid_sessions_unchanged_after_fix(){// Ensure the fix doesn't break normal session handlinglet home = TempDir::new().expect("temp dir");let config = test_config(home.path());let store = LocalThreadStore::new(config,None);let uuid = Uuid::from_u128(4250);write_session_file(home.path(),"2025-01-03T12-00-00", uuid).expect("session file");let page = store.list_threads(default_list_params()).await.expect("list");assert_eq!(page.items.len(),1);assert!(!page.items[0].preview.as_ref().unwrap().contains("Corrupted"));}
4.4 Performance Tests
Test 10: Listing performance with many corrupted files
#[tokio::test]asyncfnlisting_performance_with_corrupted_files(){let home = TempDir::new().expect("temp dir");let config = test_config(home.path());let store = LocalThreadStore::new(config,None);// Create 100 valid sessionsfor i in0..100{let uuid = Uuid::from_u128(i);write_session_file(home.path(),&format!("2025-01-{:02}-T12-00-00", i % 28), uuid).expect("session file");}// Create 10 corrupted sessionsfor i in100..110{let uuid = Uuid::from_u128(i);let day_dir = home.path().join("sessions/2025/01/03");
std::fs::create_dir_all(&day_dir).expect("sessions dir");let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
std::fs::write(&rollout_path,"{{CORRUPTED").expect("write");}let start = Instant::now();let page = store.list_threads(default_list_params()).await.expect("list");let elapsed = start.elapsed();// Should complete within reasonable timeassert!(elapsed.as_millis() < 5000,"Listing took too long: {:?}", elapsed);// All 110 sessions should be visibleassert_eq!(page.items.len(),110);}
All changes are backward compatible — valid sessions continue to work exactly as before
Corrupted sessions that were previously invisible now appear with degraded metadata
The state DB is not modified in a breaking way
6.2 Monitoring
Add metrics to track corruption:
// In rollout/src/recorder.rs, after load_rollout_items:if parse_errors > 0{
metric_client.counter("codex.rollout.parse_errors",
parse_errors asi64,&[("thread_id",&thread_id.map(|t| t.to_string()).unwrap_or_default())],).ok();}// New metric for corrupted sessionsif !has_session_meta {
metric_client.counter("codex.rollout.corrupted_sessions",1,&[("recovery_method","filename")],).ok();}
6.3 User Communication
When a user encounters a corrupted session, the UI should display:
⚠️ This session has corrupted metadata. Some information may be unavailable.
Thread ID: abc123...
Created: 2025-01-03 12:00:00 UTC
[Repair Session] [View Raw File]
7. Summary
This solution addresses the root cause of issue #24425 by:
Extracting thread IDs from filenames when JSON parsing fails, ensuring sessions are never completely invisible
Building degraded summaries from available data (filename metadata) when session metadata is corrupted
Falling back gracefully at every layer — listing, reading, and state DB reconciliation
Providing recovery mechanisms — both automatic (filename-based recovery) and manual (CLI repair command)
Adding comprehensive tests covering all corruption scenarios
The key insight is that the rollout filename contains enough information (timestamp + UUID) to identify a session even when the JSON content is completely corrupted. By leveraging this, we ensure sessions never silently disappear.
Solution developed autonomously. Zero competition on this issue.
I checked current main, and the disappearance path looks narrower than “any bad JSON history”.
load_rollout_items() already skips malformed lines and counts parse_errors, so a bad middle line by itself should not make a session disappear. The fragile path is earlier in listing/direct-read metadata discovery: build_thread_item() only returns a session when read_head_summary() finds both saw_session_meta and a preview, and read_session_meta_line() only succeeds when the retained head starts with a deserializable SessionMetaLine. If that metadata path is broken, read_thread_from_rollout_path() falls back to the same metadata read and can fail again. doctor currently only counts rollout files/bytes; it does not validate JSONL or attempt recovery.
Small fix direction: keep a degraded listing/read entry when the rollout still has recoverable content and a trustworthy thread id can be recovered, e.g. from the filename or state DB, and surface a warning/repair hint instead of dropping the whole session.
Do you know how the JSON file became corrupt? Did you manually edit it? Do you suspect that it was written in a corrupted form? Was there a crash or power loss?
I think that recovering a corrupted JSON rollout file is fraught with problems, so it's probably best for Codex to not attempt such a repair. This should be a very rare circumstance.
The JSON of 10 of the history files were corrupted with similar patterns, if I understand the analysis codex did. It happened during a long session, so it looks like session corruption that multiplied. Codex was controlled by Codex App on Mac while running remotely on a Linux vm. There is no filesystem corruption, or any other faults happening to the vm nor the Mac. It all points to codex JSON encoding issues, which codex then tripped on.
It makes sense that codex should have a warning when corruption is detected and session loading aborts. Broken JSON should probably best just be ignored, but session loading should complete.
Rather than trying to recover from broken JSON rollouts, I'd like to focus on understanding the root cause of the corruption. Looking at your logs, it appears that partial lines are being written to the file. Unfortunately, we don't currently log the underlying file system error, so I can't determine the exact reason. The most likely problem is an out-of-disk-space condition, but it could also be something like a user quota or other resource limit. I'll look at whether we can harden the JSON writer to reduce the likelihood of corruption in the face of such errors. You might also want to investigate what's happening on your system to cause this. You may be able to eliminate the cause (e.g. by freeing up disk space).
Adding a censored example of the jsonl written. The JSON lines seem to have the same payload, but the first only manages to write it partially before the second is written. Might be that the second overwrites the first, or the first silently fails when writing. Maybe look closer at the library writing the jsonl?
Thanks for the work on the debugging infrastructure. Checked both the guest and host for issues just to make sure. Disk, ram, cpu are plenty. The user is a plain Ubuntu 24.04 LTS user, thus no extra quota or limits. Host did have a soft ECC error in the logs, but it was not related time wise, nor process/vm wise.
Activity
github-actions commentedon May 25, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
jshaofa-ui commentedon May 25, 2026
Proposed Solution
Solution: Codex Issue #24425 — Session Disappear. Codex Trips Over Bad JSON History
Executive Summary
Sessions disappear from Codex's thread listing when the rollout
.jsonlfile contains corrupted or malformed JSON lines. The root cause is that bad JSON in the session metadata line (the first line of the rollout file) causes the head-summary reader to return empty results, which filters out the entire session from listings. While individual line parsing already skips bad JSON, the first-line session metadata is critical — without it, the session has nothread_id, nopreview, and nosaw_session_metaflag, causing it to be silently excluded from all listings.1. Root Cause Analysis
1.1 How Session History Works
Codex stores session history as JSONL (JSON Lines) files in
~/.codex/sessions/. Each line is a JSON object representing aRolloutLine:{"timestamp":"...","type":"session_meta","payload":{...}} {"timestamp":"...","type":"event_msg","payload":{...}} {"timestamp":"...","type":"response_item","payload":{...}}The first line is always a
session_metaentry containing the thread ID, working directory, model provider, and other critical metadata.1.2 The Failure Chain
The session disappearance occurs through this cascade:
1.3 Key Code Locations
rollout/src/list.rs:1075read_head_summary()rollout/src/list.rs:736build_thread_item()saw_session_meta && preview— both fail on bad first linerollout/src/list.rs:1165read_head_for_summary()rollout/src/list.rs:1237read_session_meta_line()Errwhen head is emptyrollout/src/recorder.rs:812load_rollout_items()thread_id=Noneif session_meta is corruptedthread-store/src/local/read_thread.rs:203read_thread_from_rollout_path()stored_thread_from_session_meta()which also needs session_meta1.4 Why Sessions "Disappear" Rather Than Showing an Error
The current code uses silent filtering in multiple places:
build_thread_item()returnsNonewhensaw_session_metais false — the session is silently excluded from listingsread_head_summary()usesunwrap_or_default()on I/O errors — corrupted files look like empty filesread_session_meta_line()returnsErrwhen the head is empty — callers propagate this as "no session found"read_thread_from_rollout_path()falls back through multiple paths, each of which can silently failThe result: a corrupted session file is invisible everywhere — not in listings, not in direct reads, and not recoverable without manual intervention.
1.5 Corruption Scenarios
2. Fix: Graceful Handling of Corrupted JSON History
2.1 Overview
The fix introduces three layers of resilience:
2.2 Changes to
rollout/src/list.rs2.2.1 Add filename-based thread ID extraction
2.2.2 Add degraded summary builder
2.2.3 Modify
build_thread_item()to accept degraded summaries2.2.4 Modify
read_session_meta_line()to fall back to filename metadata2.3 Changes to
rollout/src/recorder.rs2.3.1 Improve
load_rollout_items()thread ID recovery2.3.2 Add corrupted session detection and warning
2.4 Changes to
thread-store/src/local/read_thread.rs2.4.1 Improve error handling in
read_thread_from_rollout_path()2.5 Changes to
rollout/src/state_db.rs2.5.1 Improve
reconcile_rollout()error handling for corrupted files3. Recovery Mechanism for Corrupted Sessions
3.1 Automatic Recovery on Listing
With the fixes above, corrupted sessions are automatically visible in listings with degraded metadata. The session appears with:
[Corrupted session — metadata unavailable]Unknown3.2 Manual Recovery via CLI Command
Add a new CLI subcommand for manual recovery:
Implementation sketch:
3.3 Recovery via State DB
When the state DB has metadata for a corrupted session, the system should:
4. Testing Approach
4.1 Unit Tests
Test 1: Corrupted first line — session still visible in listing
Test 2: Corrupted middle lines — session visible, bad lines skipped
Test 3: Thread ID recovery from filename
Test 4: read_session_meta_line fallback to filename
Test 5: Empty file handling
Test 6: Binary garbage file
4.2 Integration Tests
Test 7: Full session lifecycle with corrupted file
Test 8: State DB reconciliation with corrupted rollout
4.3 Regression Tests
Test 9: Valid sessions still work correctly
4.4 Performance Tests
Test 10: Listing performance with many corrupted files
5. Files Modified
rollout/src/list.rsextract_thread_id_from_filename(),build_degraded_summary(),build_session_meta_from_filename(); modifybuild_thread_item(),read_session_meta_line()rollout/src/recorder.rsload_rollout_items(); adddetect_corrupted_session_meta()rollout/src/state_db.rstry_recover_metadata_from_filename(); improve error handling inreconcile_rollout()thread-store/src/local/read_thread.rsbuild_degraded_thread_from_filename(); modifyread_thread_from_rollout_path()rollout/src/list.rs(tests)rollout/src/recorder_tests.rsthread-store/src/local/read_thread.rs(tests)6. Migration and Deployment Notes
6.1 Backward Compatibility
6.2 Monitoring
Add metrics to track corruption:
6.3 User Communication
When a user encounters a corrupted session, the UI should display:
7. Summary
This solution addresses the root cause of issue #24425 by:
The key insight is that the rollout filename contains enough information (timestamp + UUID) to identify a session even when the JSON content is completely corrupted. By leveraging this, we ensure sessions never silently disappear.
Solution developed autonomously. Zero competition on this issue.
yui-stingray commentedon May 25, 2026
I checked current
main, and the disappearance path looks narrower than “any bad JSON history”.load_rollout_items()already skips malformed lines and countsparse_errors, so a bad middle line by itself should not make a session disappear. The fragile path is earlier in listing/direct-read metadata discovery:build_thread_item()only returns a session whenread_head_summary()finds bothsaw_session_metaand a preview, andread_session_meta_line()only succeeds when the retained head starts with a deserializableSessionMetaLine. If that metadata path is broken,read_thread_from_rollout_path()falls back to the same metadata read and can fail again.doctorcurrently only counts rollout files/bytes; it does not validate JSONL or attempt recovery.Small fix direction: keep a degraded listing/read entry when the rollout still has recoverable content and a trustworthy thread id can be recovered, e.g. from the filename or state DB, and surface a warning/repair hint instead of dropping the whole session.
etraut-openai commentedon May 25, 2026
Do you know how the JSON file became corrupt? Did you manually edit it? Do you suspect that it was written in a corrupted form? Was there a crash or power loss?
I think that recovering a corrupted JSON rollout file is fraught with problems, so it's probably best for Codex to not attempt such a repair. This should be a very rare circumstance.
nialse commentedon May 25, 2026
The JSON of 10 of the history files were corrupted with similar patterns, if I understand the analysis codex did. It happened during a long session, so it looks like session corruption that multiplied. Codex was controlled by Codex App on Mac while running remotely on a Linux vm. There is no filesystem corruption, or any other faults happening to the vm nor the Mac. It all points to codex JSON encoding issues, which codex then tripped on.
It makes sense that codex should have a warning when corruption is detected and session loading aborts. Broken JSON should probably best just be ignored, but session loading should complete.
etraut-openai commentedon May 25, 2026
Rather than trying to recover from broken JSON rollouts, I'd like to focus on understanding the root cause of the corruption. Looking at your logs, it appears that partial lines are being written to the file. Unfortunately, we don't currently log the underlying file system error, so I can't determine the exact reason. The most likely problem is an out-of-disk-space condition, but it could also be something like a user quota or other resource limit. I'll look at whether we can harden the JSON writer to reduce the likelihood of corruption in the face of such errors. You might also want to investigate what's happening on your system to cause this. You may be able to eliminate the cause (e.g. by freeing up disk space).
nialse commentedon May 25, 2026
Adding a censored example of the jsonl written. The JSON lines seem to have the same payload, but the first only manages to write it partially before the second is written. Might be that the second overwrites the first, or the first silently fails when writing. Maybe look closer at the library writing the jsonl?
{
"timestamp":"2026-05-24T21:27:14.630Z",
"type":"response_item",
"payload":
{
"type":"reasoning",
"summary":[],
"content":null,
"encrypted_content":"gAAAAABqE20ygJcKVa24OnKmL5NK12MwZbjYZTXC6g1W9-
...
RSWhMrQQEA6EaqrGcd3O7_MY
{
"timestamp":"2026-05-24T21:30:39.830Z",
"type":"response_item",
"payload":
{"type":
"reasoning",
"summary":[],
"content":null,
"encrypted_content":"gAAAAABqE20ygJcKVa24OnKmL5NK12MwZbjYZTXC6g1W9-
...
RSWhMrQQEA6EaqrGcd3O7_MY8WJ-oFVrCX1ydJ8xbBx7-yJFgj6YyLE-DrbZl1GuU3Mrs8z-P2YuC-O6fX-U3NeGIXpFiF0sKyZBYCvLAXwUYjUb7-FNd0CCan7fWBK-5kAdB9AarXYU84FUeq3k3dmnrFG5AbVzmSEdOuSNAWK4GJehINYMMjCdR245JTrxAbR9O_qm6ops
iiP-nZ-Y-IvteQtHko="
}
}
nialse commentedon May 26, 2026
Thanks for the work on the debugging infrastructure. Checked both the guest and host for issues just to make sure. Disk, ram, cpu are plenty. The user is a plain Ubuntu 24.04 LTS user, thus no extra quota or limits. Host did have a soft ECC error in the logs, but it was not related time wise, nor process/vm wise.
Log rollout writer OS errors (#24474)
Log rollout writer OS errors (openai#24474)