BigPizzaV3/CodexPlusPlus · error
.codex-global-state.json changed while deleting thread
Error message
.codex-global-state.json changed while deleting thread {thread_id} What it means
When deleting a thread from .codex-global-state.json, the function re-reads the file after performing in-memory removal and bails if its bytes differ from the snapshot taken at the start, refusing to write. This optimistic-concurrency check prevents clobbering concurrent modifications (e.g. codex CLI updating sidebar state) with a stale snapshot.
Solutions
- Retry the deletion: re-read the file fresh and re-apply the removal to the new state.
- Close/pause the codex app or CLI that is concurrently writing .codex-global-state.json and retry.
- Serialize deletes through a single process (file lock or in-process mutex) to avoid racing writers.
- If the change is expected, refresh the snapshot and merge removals instead of failing.
Example fix
// before
if fs::read(&path)? != original_bytes {
anyhow::bail!(".codex-global-state.json changed while deleting thread {thread_id}");
}
// after
let mut current_bytes = original_bytes.to_vec();
for _ in 0..3 {
match try_delete_from_bytes(¤t_bytes, thread_id) {
Ok(result) => return Ok(result),
Err(StaleState) => current_bytes = fs::read(&path)?, // re-read and retry
}
} Defensive patterns
Strategy: retry
Validate before calling
let before = fs::read(&global_state_path)?;
// ... perform delete ...
if fs::read(&global_state_path)? != before {
eprintln!("global state is being modified concurrently; retry later");
} Try / catch
for attempt in 0..3 {
match delete_thread(codex_home, thread_id) {
Ok(n) => break Ok(n),
Err(e) if e.to_string().contains("changed while deleting thread") => {
std::thread::sleep(Duration::from_millis(200 * (attempt + 1)));
}
Err(e) => break Err(e),
}
} Prevention
- Close the codex CLI/TUI while running manager-side thread deletions
- Hold a lock file around .codex-global-state.json mutations
- Serialize all global-state writes through a single process
- Re-read the file immediately before writing and keep the compare-and-swap window short
When it happens
Trigger: Calling the thread-deletion API while codex (CLI/app) concurrently writes .codex-global-state.json between the initial read and the write; another process (watcher, sync tool) touches the file during the delete; two manager instances deleting different threads at once.
Common situations: Running the manager app while codex TUI is open and recording new thread activity; backup agents or file watchers modifying global state; two undo/delete operations racing.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- global state changed while provider sync was being written
- Concurrent adapter upgrade
- Concurrent runtime change
- bridge context lock poisoned
- Candidate backup conflict
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/a2a903847abab41e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-data/src/provider_sync.rs:3109
.keys()
.filter(|key| {
*key == &client_id
|| *key == &client_id_encoded
|| *key == &reference_capability
|| *key == &reference_capability_encoded
})
.cloned()
.collect::<Vec<_>>();
for key in keys {
atom.remove(&key);
removed += 1;
}
}
if removed == 0 {
return Ok(0);
}
if fs::read(&path)? != original_bytes {
anyhow::bail!(".codex-global-state.json changed while deleting thread {thread_id}");
}
codex_plus_core::settings::atomic_write(&path, serde_json::to_string_pretty(&state)?.as_bytes())?;
Ok(removed)
}
fn remove_thread_from_catalog_dbs(codex_home: &Path, thread_id: &str) -> anyhow::Result<usize> {
let mut removed_total = 0usize;
for path in codex_plus_core::codex_sqlite::codex_thread_reference_db_paths_from_home(codex_home) {
if !path.exists() {
continue;
}
let mut db = Connection::open(&path)?;
db.busy_timeout(std::time::Duration::from_millis(500))?;
let tx = db.transaction()?;
let mut removed = 0usize;
for table in [
"local_thread_catalog",
"thread_timeline_ledger",View on GitHub (pinned to b1ed92e5e4)