astrid-runtime/astrid · error · std::io::Error::AlreadyExists

legacy and operator REPL histories differ; refusing to merge

Error message

legacy and operator REPL histories differ; refusing to merge

What it means

When both legacy and operator history files exist, the migration refuses to silently merge or overwrite: it reads both (bounded) and requires them to be byte-identical. If they differ, it fails with AlreadyExists and the message that it refuses to merge, forcing the operator to resolve the ambiguity manually instead of losing commands.

Source

Thrown at crates/astrid-cli/src/repl.rs:245

    if !legacy_exists {
        return Ok(());
    }
    let legacy = read_history_bounded(legacy_path)?;
    match std::fs::symlink_metadata(new_path) {
        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "REPL history path is not a regular file: {}",
                    new_path.display()
                ),
            ));
        },
        Ok(_) => {
            astrid_core::platform_fs::verify_no_redirects(new_path)?;
            let current = read_history_bounded(new_path)?;
            if current != legacy {
                return Err(io::Error::new(
                    io::ErrorKind::AlreadyExists,
                    "legacy and operator REPL histories differ; refusing to merge",
                ));
            }
        },
        Err(error) if error.kind() == io::ErrorKind::NotFound => {
            astrid_core::platform_fs::atomic_write_private_file(new_path, &legacy)?;
        },
        Err(error) => return Err(error),
    }
    std::fs::remove_file(legacy_path)?;
    Ok(())
}

fn read_history_bounded(path: &Path) -> io::Result<Vec<u8>> {
    astrid_core::platform_fs::verify_no_redirects(path)?;
    let mut file = File::open(path)?;
    let mut bytes = Vec::new();

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect both files and manually merge the commands you want to keep into the operator history file, then delete the legacy file
  2. If the legacy file is obsolete, delete or archive it so migration proceeds
  3. If the operator file is the stale one, replace it with the legacy contents before starting the REPL

Example fix

// before: both exist with different bytes -> refuses to merge
// after
cat ~/.astrid/legacy-history >> ~/.astrid/operator-history && rm ~/.astrid/legacy-history
Defensive patterns

Strategy: try-catch

Validate before calling

// Detect the conflict before launching the REPL
fn histories_conflict(legacy: &std::path::Path, new: &std::path::Path) -> std::io::Result<bool> {
    let l = std::fs::read(legacy)?;
    let n = std::fs::read(new)?;
    Ok(legacy.exists() && new.exists() && l != n)
}

Try / catch

match Repl::new(...) {
    Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists
        && e.to_string().contains("refusing to merge") => {
        eprintln!("Merge {} into {} manually, then delete the legacy file.", legacy_path.display(), new_path.display());
    }
    Err(e) => return Err(e.into()),
    Ok(repl) => repl,
}

Prevention

When it happens

Trigger: migrate_legacy_history encounters both an existing legacy history and an existing new-path history whose contents differ (after verify_no_redirects passes).

Common situations: User ran both an old and a new CLI build concurrently, so each wrote its own history; a backup copy was restored to the legacy path after the operator history had advanced; version upgrade moved history but old file lingered with different bytes.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/aee3e2a04ecc37a6. Report an issue: GitHub.