Hmbown/CodeWhale · error · io::Error
immutable artifact handle already contains different bytes
Error message
immutable artifact handle already contains different bytes
What it means
Immutable artifact handles are write-once: if the destination already exists with different bytes, the write is refused to guarantee replay safety. When `publish` reports AlreadyExists, the existing file is read and compared; a mismatch produces this AlreadyExists io::Error, while identical bytes are treated as a successful idempotent write.
Solutions
- Use a fresh artifact id (or content-derived id) for the new content instead of reusing an existing handle.
- If the existing content should win, skip the write when content is unchanged and treat AlreadyExists-with-same-bytes as success (the API already does this).
- Inspect the existing file: if it's corrupted/tampered, delete it deliberately and rewrite.
- Fix id-generation logic so the same logical artifact always produces identical bytes.
Example fix
// before
let id = format!("run-{}", run_counter); // counter reused across reruns
write_session_artifact_immutable(session, &id, "json", &new_bytes)?;
// after
let id = format!("run-{}-{}", run_id, sha256_hex(&new_bytes));
write_session_artifact_immutable(session, &id, "json", &new_bytes)?; Defensive patterns
Strategy: try-catch
Validate before calling
fn id_matches_content(id: &str, content: &[u8]) -> bool {
// content-derived ids avoid reusing a handle for different bytes
let digest = sha256_hex(content);
id.contains(&digest[..8])
} Try / catch
match write_session_relative_immutable(session, rel, bytes) {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists
&& e.to_string().contains("different bytes") => {
// handle exists with other content: mint a new id or skip
}
other => other?,
} Prevention
- Derive artifact ids from content hashes so identical content maps to identical ids.
- Never reuse an artifact id across logically different runs.
- Treat idempotent re-publish of identical bytes as success, not an error.
- Don't hand-edit published immutable artifacts.
When it happens
Trigger: Writing to an artifact handle (via `write_session_artifact_immutable`, evidence publication, or `export`) where the target file already exists but its content differs from the bytes being published — e.g. reusing an artifact id for new content.
Common situations: Replaying a session after the artifact was legitimately regenerated with different output; a hash/id collision or a bug reusing ids across runs; manual tampering with the artifact file; exporting to a path that already holds unrelated data.
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
- An existing Fleet artifact contains different bytes
- AlreadyExists
- invalid session artifact path
- artifact id and extension must contain safe ASCII characters
- AutomationEditorConflict
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/9a146a258f07befb.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/artifacts.rs:177
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")
})?;
let destination = open_session_relative(session_id, relative_path, true)?;
match destination.publish(content) {
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
use std::io::Read;
let mut existing = Vec::new();
destination
.open_file()?
.take(content.len() as u64 + 1)
.read_to_end(&mut existing)?;
if existing != content {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"immutable artifact handle already contains different bytes",
));
}
}
Err(err) => return Err(err),
}
Ok(absolute_path)
}
/// The same confined session directory for mutable sidecars and immutable
/// artifacts. The saved-session owner remains responsible for session state.
pub(crate) fn open_session_relative(
session_id: &str,
relative_path: &Path,
create: bool,
) -> io::Result<crate::fleet::files::WorkspaceFile> {
session_artifact_absolute_path(session_id, relative_path).ok_or_else(|| {View on GitHub (pinned to 73e0f67d83)