ultraworkers/claw-code · error · SessionError

session file was removed during write (possible concurrent m

Error message

session file was removed during write (possible concurrent modification): {io_err}

What it means

Returned by Session::save_to_path (session.rs:247-258, issue #112) when write_atomic fails with ENOENT during the actual write phase. write_atomic (session.rs:1340) does create_dir_all(parent), writes a temp file session.jsonl.tmp-<ts>-<n> next to the target, then renames it into place; a NotFound at this stage means the freshly created temp file or the parent directory disappeared between those syscalls — the signature of something concurrently deleting the session directory. The wrapper converts the raw ENOENT into this explicit concurrent-modification message.

Source

Thrown at rust/crates/runtime/src/session.rs:251

        let snapshot = self.render_jsonl_snapshot()?;
        // #112: wrap ENOENT during rotate as concurrent modification
        match rotate_session_file_if_needed(path) {
            Ok(()) => {}
            Err(SessionError::Io(ref io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => {
                return Err(SessionError::Io(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!(
                        "session file was removed during save (possible concurrent modification): {io_err}"
                    ),
                )));
            }
            Err(e) => return Err(e),
        }
        write_atomic(path, &snapshot).map_err(|e| {
            // #112: wrap ENOENT during write as concurrent modification
            match &e {
                SessionError::Io(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => {
                    SessionError::Io(std::io::Error::new(
                        std::io::ErrorKind::NotFound,
                        format!("session file was removed during write (possible concurrent modification): {io_err}"),
                    ))
                }
                _ => e,
            }
        })?;
        cleanup_rotated_logs(path)?;
        Ok(())
    }

    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, SessionError> {
        let path = path.as_ref();
        let contents = fs::read_to_string(path)?;
        let session = match JsonValue::parse(&contents) {
            Ok(value)
                if value
                    .as_object()

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Guarantee the session directory outlives every save: keep the TempDir alive until save tasks are joined (drop order matters in tests)
  2. Store sessions outside /tmp and any cleaner-managed location (use the dedicated .claw sessions area)
  3. Verify the parent dir still exists and re-create + retry once if your workload tolerates it: std::fs::create_dir_all(path.parent().unwrap()) then save again
  4. If concurrent claw instances are possible, route all saves through a single owner process or advisory lock

Example fix

// before — tempdir dropped while autosave task may still run
let dir = tempfile::tempdir()?;
let handle = spawn_autosave(session.clone(), dir.path().join("s.jsonl"));
drop(dir); // directory unlinked; autosave hits ENOENT

// after — join the saver before dropping the dir
let dir = tempfile::tempdir()?;
let handle = spawn_autosave(session.clone(), dir.path().join("s.jsonl"));
handle.join().expect("autosave panicked")?;
drop(dir);
Defensive patterns

Strategy: retry

Validate before calling

fn ensure_session_dir(path: &std::path::Path) -> std::io::Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?; // recreate if a cleaner removed it
        std::fs::symlink_metadata(parent)?; // fail fast if it vanishes again
    }
    Ok(())
}

ensure_session_dir(&path)?;
session.save_to_path(&path)?;

Try / catch

match session.save_to_path(&path) {
    Ok(()) => {}
    Err(e @ SessionError::Io(ref io))
        if io.kind() == std::io::ErrorKind::NotFound
            && io.to_string().contains("removed during write") => {
        std::fs::create_dir_all(path.parent().unwrap())?; // one bounded retry after recreating the dir
        session.save_to_path(&path).map_err(|_| e)?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Session::save_to_path racing against: deletion of the session directory (another process, test tempdir teardown, tmp cleaner), or on platforms where rename(2) fails with ENOENT because the destination parent was removed after create_dir_all. Two savers using the same path are safe against each other (unique temp names) — the trigger is an external unlink/rmdir of the parent or temp file.

Common situations: Background autosave thread still running when a test's tempfile::TempDir drops and unlinks the tree; session dir on a network mount with aggressive cache expiry; cleanup scripts removing 'stale' session dirs while a long-lived claw process periodically saves; nested test runs sharing a session directory prefix.

Related errors


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/89413649362f90fb. Report an issue: GitHub.