{"record":{"id":"cb67cbbc163aa80d","repo":"gitbutlerapp/gitbutler","slug":"changed-while-preparing-the-rewrite-leaving-it","errorCode":null,"errorMessage":"{} changed while preparing the rewrite; leaving it alone","messagePattern":"(.+?) changed while preparing the rewrite; leaving it alone","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/but/src/command/agent/cleanup.rs","lineNumber":133,"sourceCode":"    path: &Path,\n    read_content: &str,\n    new_content: &str,\n    original: &std::fs::Metadata,\n) -> Result<()> {\n    use std::io::Write as _;\n    let dir = path\n        .parent()\n        .with_context(|| format!(\"No parent directory for {}\", path.display()))?;\n    let mut temp = tempfile::NamedTempFile::new_in(dir)?;\n    temp.write_all(new_content.as_bytes())?;\n    // Temp files default to owner-only permissions; keep the original mode.\n    temp.as_file().set_permissions(original.permissions())?;\n    temp.as_file().sync_all()?;\n    // The rename below replaces the whole file, so an edit that landed after\n    // our read would be silently thrown away with it. Back off if the content\n    // moved under us — a later command re-runs the sweep on the fresh content.\n    if std::fs::read_to_string(path)? != read_content {\n        anyhow::bail!(\n            \"{} changed while preparing the rewrite; leaving it alone\",\n            path.display()\n        );\n    }\n    temp.persist(path)\n        .with_context(|| format!(\"Failed to replace {}\", path.display()))?;\n    Ok(())\n}\n\n/// Replace every line-anchored occurrence of the retired bullet that sits\n/// inside a managed block. Returns `None` when nothing needs rewriting.\nfn replace_retired_bullet(content: &str) -> Option<String> {\n    // The cheap check first: on the steady-state clean file this skips the\n    // marker scan and allocation entirely.\n    if !content.contains(RETIRED_FAST_PATH_BULLET) {\n        return None;\n    }\n    // A malformed block (unmatched marker) is not ours to interpret: skip.","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but/src/command/agent/cleanup.rs#L115-L151","documentation":"replace_file_contents rewrites agent-related files via a temp file + atomic persist. To avoid silently discarding concurrent edits, it re-reads the file before the rename; if the content changed since the original read, it backs off and leaves the file alone so a later sweep re-runs on the fresh content.","triggerScenarios":"cleanup_file-driven rewrite of a file that was modified between the initial read_to_string and the pre-rename check — a concurrent process, editor auto-save, formatter, or another cleanup invocation wrote to the path in that window.","commonSituations":"An agent session editing the same instructions/notes file while the cleanup sweep runs; linters-on-save firing during the rewrite; two cleanup processes racing on the same file; users editing files by hand while an automated sweep is in flight.","solutions":["Treat this as an expected race: simply re-run the cleanup — the error is designed so a later sweep processes the fresh content","Serialize edits: hold a lock (or the same editor/session) so no concurrent writer touches the file during cleanup","Disable auto-save/formatters for files under sweep, or run the sweep while those tools are idle","Check mtime before initiating a rewrite and skip files modified recently"],"exampleFix":"// before: blind retry that may keep failing\nloop { match cleanup_file(path) { Ok(_) => break, Err(_) => continue } }\n// after: back off and retry on fresh content after a delay\nmatch cleanup_file(path) {\n    Err(e) if e.to_string().contains(\"changed while preparing the rewrite\") => {\n        std::thread::sleep(Duration::from_millis(250));\n        cleanup_file(path)?; // re-reads the fresh content\n    }\n    r => r,\n}","handlingStrategy":"retry","validationCode":"// re-check mtime before starting the rewrite\nlet before = std::fs::metadata(path)?.modified()?;\nif before.elapsed()?.as_secs() < 1 { /* recently written — wait or skip */ }","typeGuard":null,"tryCatchPattern":"match replace_file_contents(path, new_content) {\n    Err(e) if e.to_string().contains(\"changed while preparing the rewrite\") => {\n        std::thread::sleep(Duration::from_millis(250));\n        replace_file_contents(path, compute_new_content(path)?) // re-read fresh\n    }\n    r => r,\n}","preventionTips":["Recompute the new content from a fresh read on each attempt","Serialize writers with a file lock or single cleanup owner","Disable autosave/formatters for files under automated sweeps","Back off and retry rather than forcing the write"],"tags":["concurrency","race-condition","file-write","cleanup"],"backgroundTag":"file-write-failed","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}