{"record":{"id":"ddf1b42118beb8a1","repo":"Hmbown/CodeWhale","slug":"config-changed-after-it-was-loaded-reload-and","errorCode":null,"errorMessage":"config changed after it was loaded; reload {} and retry instead of overwriting concurrent changes","messagePattern":"config changed after it was loaded; reload (.+?) and retry instead of overwriting concurrent changes","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/config_document.rs","lineNumber":101,"sourceCode":"/// acquired. This closes the `exists()`/create race in first-run writers.\npub fn create_config_document(path: &Path, body: &str) -> Result<()> {\n    replace_config_document_if_unchanged(path, None, body)\n}\n\n/// Replace a full typed snapshot only when on-disk bytes still equal the\n/// snapshot the caller originally loaded. `None` means the file was absent.\npub fn replace_config_document_if_unchanged(\n    path: &Path,\n    expected: Option<&str>,\n    body: &str,\n) -> Result<()> {\n    with_config_write_lock(path, |path| {\n        let current = read_optional_config(path)?;\n        if current.as_deref() == Some(body) {\n            return Ok(());\n        }\n        if current.as_deref() != expected {\n            bail!(\n                \"config changed after it was loaded; reload {} and retry instead of overwriting concurrent changes\",\n                crate::quote_os_path(path)\n            );\n        }\n        persist_locked(path, current.as_deref(), body.as_bytes())\n    })\n}\n\n/// Set a value at `segments`, creating implicit parent tables while preserving\n/// existing key/value decor.\npub fn set_config_document_value(\n    doc: &mut toml_edit::DocumentMut,\n    segments: &[&str],\n    value: impl Into<toml_edit::Value>,\n) -> Result<()> {\n    let (key, parents) = segments\n        .split_last()\n        .context(\"config value path must not be empty\")?;","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/config_document.rs#L83-L119","documentation":"Config writes use optimistic concurrency: replace_config_document_if_unchanged() holds the write lock, re-reads the file, and requires it to still equal the snapshot the caller loaded. If the on-disk content differs from that expected snapshot (and is not already the desired body), the write is refused so concurrent edits are never overwritten; the caller must reload and retry.","triggerScenarios":"Two Codewhale processes (e.g. a TUI session and a CLI command, or two agents) load config.toml, both modify it, and the second one calls replace_config_document_if_unchanged() after the first already persisted — current != expected triggers the bail.","commonSituations":"Long-lived TUI session open while a background agent or second terminal mutates settings; scripted config edits racing an interactive session; editors with auto-save touching config.toml between load and write.","solutions":["Reload config.toml and retry the same mutation — the standard recovery named in the message","Close or pause the other Codewhale session that wrote config.toml before retrying","Serialize your config edits (apply them from one process at a time) instead of racing concurrent writers","If it recurs with a single process, check for external tools (sync agents, editors) rewriting the file underneath Codewhale"],"exampleFix":"// before (write once with a stale snapshot)\nreplace_config_document_if_unchanged(&path, Some(&loaded), &new_body)?; // bails if file changed\n\n// after (reload and retry)\nmatch replace_config_document_if_unchanged(&path, Some(&loaded), &new_body) {\n    Err(_) if config_changed_on_disk(&path) => {\n        let loaded = std::fs::read_to_string(&path)?; // reload\n        replace_config_document_if_unchanged(&path, Some(&loaded), &new_body)?;\n    }\n    other => other?,\n}","handlingStrategy":"retry","validationCode":"// Compare the on-disk document with your snapshot immediately before writing:\nlet current = std::fs::read_to_string(&path).ok();\nif current.as_deref() != Some(&loaded_snapshot) {\n    // someone else wrote in between: reload now instead of failing later\n    loaded_snapshot = current.unwrap_or_default();\n    // re-apply your edit to the fresh snapshot before calling replace\n}","typeGuard":null,"tryCatchPattern":"// classic optimistic-lock retry loop:\nfor _ in 0..3 {\n    let snapshot = read_optional_config(&path)?;\n    let body = apply_edit(&snapshot, &edit)?;\n    match replace_config_document_if_unchanged(&path, snapshot.as_deref(), &body) {\n        Ok(()) => return Ok(()),\n        Err(e) if e.to_string().contains(\"config changed after it was loaded\") => continue,\n        Err(e) => return Err(e),\n    }\n}\nbail!(\"config kept changing; retry later\")","preventionTips":["Run one config-mutating Codewhale process at a time","Reload-and-retry immediately on this error instead of overwriting","Keep external editors/sync tools away from config.toml during sessions"],"tags":["config","concurrency","optimistic-lock","file-write"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}