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
- Guarantee the session directory outlives every save: keep the TempDir alive until save tasks are joined (drop order matters in tests)
- Store sessions outside /tmp and any cleaner-managed location (use the dedicated .claw sessions area)
- 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
- 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
- Make the session directory's lifetime strictly longer than every task that saves into it (drop order in tests is the usual culprit)
- Pin autosave/teardown ordering: stop savers, flush, then delete directories
- Avoid network filesystems for session storage — rename-based atomic writes are ENOENT-prone there
- After this error, verify the directory still exists before retrying; a second immediate failure means an active deleter
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
- session file was removed during save (possible concurrent mo
- registry lock poisoned
- team registry lock poisoned
- cron registry lock poisoned
- worker registry lock poisoned
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/89413649362f90fb.
Report an issue: GitHub.