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

  1. Retry: re-take the global state snapshot and re-run the provider-sync write against fresh bytes.
  2. Avoid running provider sync and thread delete/undo simultaneously; sequence them (await the other operation).
  3. Use a lock around global-state mutations so snapshot-compare-write is atomic per process.
  4. 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

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


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)