Hmbown/CodeWhale · error

member ended before its recorded size of bytes; the source…

Error message

member {name} ended before its recorded size of {expected} bytes; the source changed during export

What it means

append_sized writes an archive member using reader.take(expected) and then verifies the take() limit reached zero, i.e. the source yielded exactly the expected byte count recorded in the tar header. If the file shrank (or was truncated) between stat and read, the member is short and the export fails with UnexpectedEof rather than writing a corrupt archive.

Solutions

  1. Re-run the export now that the source has settled — a transient concurrent write is the usual cause.
  2. Stop or quiesce the process writing to artifacts/ (or export an idle/completed session) before archiving.
  3. Copy the artifacts to a stable staging directory first, then export from the snapshot.
  4. Check the specific member named in the message for corruption or truncation and repair it.

Example fix

// before
export_session_archive(&sessions_dir, live_session_id, out)  // logs still being written
// after
let snapshot = copy_dir_to_tmp(artifacts_dir)?;
export_session_archive(&snapshot_sessions_dir, session_id, out)?;
Defensive patterns

Strategy: retry

Validate before calling

let expected = fs::metadata(path)?.len();
let actual = fs::metadata(path)?.len(); // re-check just before archiving
if expected != actual { /* source is changing; wait or snapshot first */ }

Try / catch

match export_session_archive(...) {
    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => retry_after_quiescing(3),
    other => other,
}

Prevention

When it happens

Trigger: Calling append_sized (via append_member in write_session_archive) when the artifact file's size at header-build time is larger than the bytes actually readable — the file was truncated, deleted, or rewritten concurrently during export. Also exercised directly by the forkguard test feeding a short reader.

Common situations: Another process (a running agent, log rotator, editor save) modified or truncated the artifact while the tar was being written; exporting a live session whose logs are still being appended/rotated; NFS or network filesystems serving stale size metadata.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/943b2ce78b72b420. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/session_export.rs:421

}

/// Append exactly `expected` bytes of `reader` under `name`. The bounded read
/// keeps the tar header and the member payload consistent when the source
/// file changes size mid-export: growth is capped at the snapshot size
/// (valid archive, prefix content), while shrinkage — which would otherwise
/// silently shift every following header and corrupt the archive — fails the
/// export instead.
fn append_sized<W: Write, R: Read>(
    tar: &mut Builder<W>,
    header: &mut tar::Header,
    name: &str,
    reader: R,
    expected: u64,
) -> io::Result<()> {
    let mut limited = reader.take(expected);
    tar.append_data(header, name, &mut limited)?;
    if limited.limit() != 0 {
        return Err(io::Error::new(
            io::ErrorKind::UnexpectedEof,
            format!(
                "member {name} ended before its recorded size of {expected} bytes; the source changed during export"
            ),
        ));
    }
    Ok(())
}

fn append_member<W: Write>(
    tar: &mut Builder<W>,
    name: &str,
    contents: MemberContents<'_>,
    members: &mut Vec<SessionArchiveMember>,
) -> io::Result<()> {
    match contents {
        MemberContents::Bytes(bytes) => {
            let mut header = archive_header(bytes.len() as u64);

View on GitHub (pinned to 73e0f67d83)