ultraworkers/claw-code · error · SessionError
session file was removed during save (possible concurrent mo
Error message
session file was removed during save (possible concurrent modification): {io_err} What it means
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.
Source
Thrown at rust/crates/runtime/src/session.rs:238
#[must_use]
pub fn workspace_root(&self) -> Option<&Path> {
self.workspace_root.as_deref()
}
#[must_use]
pub fn persistence_path(&self) -> Option<&Path> {
self.persistence.as_ref().map(|value| value.path.as_path())
}
pub fn save_to_path(&self, path: impl AsRef<Path>) -> Result<(), SessionError> {
let path = path.as_ref();
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,View on GitHub (pinned to 08106b0c37)
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
Example fix
// before — retry the same save against a deleted file, loops forever
loop {
if session.save_to_path(&path).is_ok() { break; }
}
// after — on NotFound, accept the loss and rebind to a fresh path
match session.save_to_path(&path) {
Ok(()) => {}
Err(SessionError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
session = Session::load_from_path(&new_path) // or Session::new()
.with_persistence_path(new_path.clone());
session.save_to_path(&new_path)?;
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: retry
Validate before calling
fn session_path_writable(path: &std::path::Path) -> bool {
std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)
&& std::fs::metadata(path.parent().unwrap_or(path)).map(|m| m.is_dir()).unwrap_or(false)
} Try / catch
fn is_concurrent_modification(err: &SessionError) -> bool {
matches!(err, SessionError::Io(e)
if e.kind() == std::io::ErrorKind::NotFound
&& e.to_string().contains("concurrent modification"))
}
// usage: on this error, reload from the last known-good state or rebind
// to a new path and save once — the old file is gone, blind retries cannot win. Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- session file was removed during write (possible concurrent m
- 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/ca10a6cf92c542c6.
Report an issue: GitHub.