{"record":{"id":"ca10a6cf92c542c6","repo":"ultraworkers/claw-code","slug":"session-file-was-removed-during-save-possible-con","errorCode":null,"errorMessage":"session file was removed during save (possible concurrent modification): {io_err}","messagePattern":"session file was removed during save \\(possible concurrent modification\\): (.+?)","errorType":"exception","errorClass":"SessionError","httpStatus":null,"severity":"error","filePath":"rust/crates/runtime/src/session.rs","lineNumber":238,"sourceCode":"\n    #[must_use]\n    pub fn workspace_root(&self) -> Option<&Path> {\n        self.workspace_root.as_deref()\n    }\n\n    #[must_use]\n    pub fn persistence_path(&self) -> Option<&Path> {\n        self.persistence.as_ref().map(|value| value.path.as_path())\n    }\n\n    pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<(), SessionError> {\n        let path = path.as_ref();\n        let snapshot = self.render_jsonl_snapshot()?;\n        // #112: wrap ENOENT during rotate as concurrent modification\n        match rotate_session_file_if_needed(path) {\n            Ok(()) => {}\n            Err(SessionError::Io(ref io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => {\n                return Err(SessionError::Io(std::io::Error::new(\n                    std::io::ErrorKind::NotFound,\n                    format!(\n                        \"session file was removed during save (possible concurrent modification): {io_err}\"\n                    ),\n                )));\n            }\n            Err(e) => return Err(e),\n        }\n        write_atomic(path, &snapshot).map_err(|e| {\n            // #112: wrap ENOENT during write as concurrent modification\n            match &e {\n                SessionError::Io(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => {\n                    SessionError::Io(std::io::Error::new(\n                        std::io::ErrorKind::NotFound,\n                        format!(\"session file was removed during write (possible concurrent modification): {io_err}\"),\n                    ))\n                }\n                _ => e,","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/ultraworkers/claw-code/blob/08106b0c3771ef5b4a5aa176acccd460e88b7325/rust/crates/runtime/src/session.rs#L220-L256","documentation":"Returned by Session::save_to_path (session.rs:235-246, issue #112) when the rotate step fails with ENOENT. Before writing, save_to_path renames an oversized session file to a rotated *.rot-*.jsonl sibling; if that rename reports NotFound, the error is re-wrapped with this message to flag concurrent modification. In practice the ENOENT almost always means the session file (or its parent directory) vanished between the metadata check in rotate_session_file_if_needed and the rename at session.rs:1370 — i.e. another process or thread deleted it mid-save.","triggerScenarios":"Session::save_to_path called on a session whose file grew past ROTATE_AFTER_BYTES, while another claw process, a cleanup job (tmpwatch/systemd-tmpfiles), or a test's tempdir drop removes the file or its directory between rotate's fs::metadata and fs::rename. Also two instances saving the same session path concurrently, one rotating while the other unlinks.","commonSituations":"Two terminal windows running claw against the same session file; session files stored in /tmp subject to cleanup daemons; test suites that drop tempfile dirs while a background save thread is still running; external tooling (fim/tombstone scripts) pruning .jsonl files by age.","solutions":["Ensure only one process writes a given session file path; serialize saves with your own file lock if multiple writers are unavoidable","Move session storage out of /tmp or exclude *.jsonl from tmpwatch/systemd-tmpfiles cleanup rules","In tests, join/abort background save tasks before letting the tempdir drop","On receiving this error, treat it as a lost-write signal: reload via Session::load_from_path (or start a new session) instead of retrying blindly, since the on-disk state is gone"],"exampleFix":"// before — retry the same save against a deleted file, loops forever\nloop {\n    if session.save_to_path(&path).is_ok() { break; }\n}\n\n// after — on NotFound, accept the loss and rebind to a fresh path\nmatch session.save_to_path(&path) {\n    Ok(()) => {}\n    Err(SessionError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {\n        session = Session::load_from_path(&new_path) // or Session::new()\n            .with_persistence_path(new_path.clone());\n        session.save_to_path(&new_path)?;\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"retry","validationCode":"fn session_path_writable(path: &std::path::Path) -> bool {\n    std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)\n        && std::fs::metadata(path.parent().unwrap_or(path)).map(|m| m.is_dir()).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"fn is_concurrent_modification(err: &SessionError) -> bool {\n    matches!(err, SessionError::Io(e)\n        if e.kind() == std::io::ErrorKind::NotFound\n            && e.to_string().contains(\"concurrent modification\"))\n}\n\n// usage: on this error, reload from the last known-good state or rebind\n// to a new path and save once — the old file is gone, blind retries cannot win.","preventionTips":["Give each claw instance its own session file path; never share one path between processes","Keep session files out of /tmp and any directory cleaned by tmpwatch/systemd-tmpfiles","In tests, join all autosave tasks before dropping tempfile::TempDir so saves never race directory teardown","Treat any ENOENT-wrapped save error as data loss: reload or recreate the session instead of retrying the same path"],"tags":["session","persistence","concurrency","file-deleted","enoent","rust"],"backgroundTag":"concurrent-file-modification","analyzedSha":"08106b0c3771ef5b4a5aa176acccd460e88b7325","analyzedAt":"2026-08-18T00:29:38.590Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}