BigPizzaV3/CodexPlusPlus · error
global state changed while provider sync was being written
Error message
global state changed while provider sync was being written
What it means
write_global_state (persisting a provider-sync snapshot into .codex-global-state.json) re-reads the file before writing and bails if its contents no longer match the original bytes captured earlier, because thread deletion/undo may have updated the file in the meantime. Writing would overwrite new sidebar entries with a stale provider-sync snapshot. The constant GLOBAL_STATE_SOURCE_CHANGED_ERROR carries this message.
Solutions
- Retry: re-take the global state snapshot and re-run the provider-sync write against fresh bytes.
- Avoid running provider sync and thread delete/undo simultaneously; sequence them (await the other operation).
- Use a lock around global-state mutations so snapshot-compare-write is atomic per process.
- Merge into the current file content (re-read state, apply provider-sync keys only) rather than overwriting the whole object from a stale snapshot.
Example fix
// before
if global_state_snapshot(path)?.0 != original_bytes {
anyhow::bail!(GLOBAL_STATE_SOURCE_CHANGED_ERROR);
}
// after
let mut original_bytes = original_bytes.to_vec();
for _ in 0..3 {
match global_state_snapshot(path) {
Ok((b, state)) if b == original_bytes => return persist(path, state, extra),
Ok((b, _)) => original_bytes = b, // refresh snapshot and retry
Err(e) => return Err(e),
}
} Defensive patterns
Strategy: retry
Validate before calling
let (_snap, _state) = global_state_snapshot(path)?;
// re-check right before writing:
if fs::read(path)? != _snap {
eprintln!("global state changed; refresh snapshot before provider sync write");
} Try / catch
loop {
match save_provider_sync(codex_home) {
Ok(()) => break,
Err(e) if e.to_string().contains(GLOBAL_STATE_SOURCE_CHANGED_ERROR) => {
if retries >= 3 { break Err(e) }
retries += 1; // re-snapshot and retry
}
Err(e) => break Err(e),
}
} Prevention
- Avoid overlapping provider-sync saves with thread delete/undo operations
- Take a fresh global_state_snapshot immediately before each write
- Use a mutex/lock file for all writers of .codex-global-state.json
- Prefer merging only the provider-sync keys over overwriting the full object
When it happens
Trigger: Calling provider-sync save/apply while .codex-global-state.json was modified between global_state_snapshot() and the write — typically by a concurrent thread deletion or undo operation that rewrote the same file.
Common situations: User deletes a thread (or performs undo) in the app while a provider sync refresh is saving; automated sync interval racing a user action; two app windows performing operations on the same codex_home.
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
- .codex-global-state.json changed while deleting thread
- 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/1d209ad364a62164.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-data/src/provider_sync.rs:5356
for (key, value) in next {
state.insert(key, value);
}
write_global_state_if_unchanged(path, &original_bytes, &state)?;
}
Ok(count)
}
const GLOBAL_STATE_SOURCE_CHANGED_ERROR: &str =
"global state changed while provider sync was being written";
fn write_global_state_if_unchanged(
path: &Path,
original_bytes: &[u8],
state: &Map<String, Value>,
) -> anyhow::Result<()> {
// 会话删除/撤销也会更新此文件;不得用过期的 provider-sync 快照覆盖新侧边栏条目。
if global_state_snapshot(path)?.0 != original_bytes {
anyhow::bail!(GLOBAL_STATE_SOURCE_CHANGED_ERROR);
}
let text = serde_json::to_string_pretty(&Value::Object(state.clone()))?;
codex_plus_core::settings::atomic_write(path, text.as_bytes())?;
if let Some(parent) = path.parent() {
codex_plus_core::settings::atomic_write(
&parent.join(".codex-global-state.json.bak"),
text.as_bytes(),
)?;
}
Ok(())
}
fn path_array(value: &Value) -> Vec<String> {
if let Some(items) = value.as_array() {
items
.iter()
.filter_map(Value::as_str)
.filter(|item| !item.trim().is_empty())View on GitHub (pinned to b1ed92e5e4)