Hmbown/CodeWhale · error

Fleet ledger lock was replaced; reopen the workspace before…

Error message

Fleet ledger lock was replaced; reopen the workspace before continuing

What it means

The Fleet ledger holds an open lock file and re-opens it for updates, verifying via `same_file` that it still refers to the same inode as the original. If the file at the lock path was replaced (deleted and recreated, or swapped), the stale lock is considered invalid and this error is raised to force a safe reopen of the workspace.

Solutions

  1. Reopen the workspace (retry the operation from a fresh ledger instance) as the message instructs.
  2. Ensure no other process deletes/recreates the lock file while a session is open; exclude the workspace from cleaner/sync tools.
  3. Coordinate Fleet sessions so only one holder manages the ledger at a time.

Example fix

// before: stale handle keeps operating on replaced lock
let ledger = FleetLedger::open(path)?; // lock replaced underneath
ledger.update(...)?; // fails
// after: reopen on this error
let ledger = match FleetLedger::open(path) { Ok(l) => l, Err(_) => FleetLedger::reopen(path)? };
Defensive patterns

Strategy: retry

Try / catch

const MAX_RETRIES: usize = 3;
for attempt in 1..=MAX_RETRIES {
    match ledger.update(entry) {
        Err(e) if e.to_string().contains("lock was replaced") if attempt < MAX_RETRIES => {
            ledger = FleetLedger::reopen(path)?; // fresh instance picks up the new lock
            continue;
        }
        other => { other?; break; }
    }
}

Prevention

When it happens

Trigger: `open_lock_file` succeeds but `same_file(&file, &self.original_lock)` returns false — the lock file was deleted and recreated by another process, or atomically replaced while a ledger instance was open.

Common situations: Two Fleet sessions racing on the same workspace, cleanup scripts or tmpwatch deleting the lock file, syncing tools (e.g. dropbox/rsync) replacing files in the workspace, crash recovery that recreates the lock.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/94aca232dcd8dc63. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/fleet/ledger.rs:343

    #[cfg(test)]
    pub(crate) fn fail_next_start_append_after_callback(&self) {
        self.fail_start_append_after_callback
            .store(true, std::sync::atomic::Ordering::SeqCst);
    }

    #[cfg(test)]
    pub(crate) fn fail_next_restart_append_after_callback(&self) {
        self.fail_restart_append_after_callback
            .store(true, std::sync::atomic::Ordering::SeqCst);
    }

    fn open_lock_file(&self) -> Result<std::fs::File> {
        let file = self
            .lock_file
            .open_update(false, false)
            .with_context(|| format!("opening fleet ledger lock {}", self.lock_path.display()))?;
        if !same_file(&file, &self.original_lock)? {
            bail!("Fleet ledger lock was replaced; reopen the workspace before continuing");
        }
        Ok(file)
    }

    fn with_read_lock<T>(&self, action: impl FnOnce() -> Result<T>) -> Result<T> {
        let lock_file = self.open_lock_file()?;
        let lock = fd_lock::RwLock::new(lock_file);
        let _guard = lock
            .read()
            .with_context(|| format!("read-locking fleet ledger {}", self.ledger_path.display()))?;
        action()
    }

    fn with_write_lock<T>(&self, action: impl FnOnce() -> Result<T>) -> Result<T> {
        let lock_file = self.open_lock_file()?;
        let mut lock = fd_lock::RwLock::new(lock_file);
        let _guard = lock.write().with_context(|| {
            format!("write-locking fleet ledger {}", self.ledger_path.display())

View on GitHub (pinned to 433685b202)