Hmbown/CodeWhale · error · std::io::Error
AlreadyExists
AlreadyExists
Error message
immutable artifact handle already contains different bytes
What it means
write_session_relative_immutable publishes write-once artifacts: if the target already exists and its bytes differ from `content`, it fails closed with AlreadyExists; identical bytes are an idempotent Ok. This guarantees a replayed or concurrent session cannot silently change what an earlier artifact handle points to — the artifact contract is one immutable payload per (session, relative path).
Source
Thrown at crates/tui/src/artifacts.rs:181
/// A duplicate replay with identical bytes is idempotent; a different payload
/// for the same relative path fails closed.
pub fn write_session_relative_immutable(
session_id: &str,
relative_path: &Path,
content: &[u8],
) -> io::Result<PathBuf> {
let absolute_path =
session_artifact_absolute_path(session_id, relative_path).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid session artifact path")
})?;
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent)?;
}
if absolute_path.exists() {
return if std::fs::read(&absolute_path)? == content {
Ok(absolute_path)
} else {
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"immutable artifact handle already contains different bytes",
))
};
}
let file_name = absolute_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("artifact");
let temp_path = absolute_path.with_file_name(format!(
".{file_name}.{}.{}.tmp",
std::process::id(),
uuid::Uuid::new_v4()
));
let publish = (|| -> io::Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)View on GitHub (pinned to 0c42157ee5)
Solutions
- Derive artifact ids from content identity (hash) or the unique tool_call_id so different bytes always take different paths
- If the existing artifact is stale, delete that session's artifacts directory (or the single file) before re-recording
- Make the payload deterministic for replays (strip volatile fields like timestamps before publishing)
- Treat the error as a caller-side contract violation: audit who wrote the first payload and why it differs
Example fix
// before
let rel = session_artifact_relative_path(&format!("art_{name}")); // fixed name, changing bytes
let abs = write_session_relative_immutable(sid, &rel, bytes)?; // AlreadyExists
// after: content-addressed id, collisions are now identical bytes
let digest = sha256(&bytes);
let rel = session_artifact_relative_path(&format!("art_{name}_{digest}"));
let abs = write_session_relative_immutable(sid, &rel, bytes)?; Defensive patterns
Strategy: fallback
Validate before calling
let target = session_artifact_absolute_path(sid, &rel);
if let Some(abs) = &target {
if let Ok(existing) = std::fs::read(abs) {
if existing != bytes {
// choose a new unique relative path now, before the failing write
}
}
} Try / catch
match write_session_relative_immutable(sid, &rel, bytes) {
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
// same path, different bytes: republish under a fresh content-derived id
// do NOT delete+overwrite in place unless you own the whole session
}
other => other?,
} Prevention
- Make artifact ids content-derived (hash) or tool-call-unique so different bytes never share a path
- Keep payloads deterministic across replays
- Treat AlreadyExists-with-different-bytes as a caller contract violation to log, not to paper over
When it happens
Trigger: Two different payloads written under the same session_id + relative_path: a tool regenerating output under a stable artifact name, a resumed session producing divergent content, or two distinct raw ids colliding after sanitize_id_component maps them to the same sanitized name (e.g. 'a/b' and 'a_b').
Common situations: Nondeterministic tool output (timestamps, random ids) reused under a fixed artifact name; replaying an edited session; duplicate tool-call ids from a buggy provider stream.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- server '{server_name}' is not registered
- Trigger '{trigger_id}' cannot be canceled (status: {:?})
- DSH is already connected; use `{CLI_COMMAND} update` to rewr
- MCP server '{name}' already exists in {}. Use `codewhale mcp
- Dynamic tool call '{}' is already pending
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/25d3aece1d73e271.
Report an issue: GitHub.