atuinsh/atuin · error

still present: re-checked under the finalization mutex every

Error message

still present: re-checked under the finalization mutex every remover holds

What it means

In the history journal's `cancel()`, an entry for `history_id` is expected to exist in `active_cmds` because prior logic re-checks presence under the finalization mutex that all removers hold. Panicking here means that internal invariant was violated — something removed the entry without holding the same lock or the re-check has a logic bug.

Source

Thrown at crates/atuin-daemon/src/history_journal.rs:439

            .ok_or(CmdCancelError::NotFound(history_id))?;
        let _guard = lock.lock().await;

        if !self.active_cmds.contains_key(&history_id) {
            return Err(CmdCancelError::NotFound(history_id));
        }

        if let Err(err) = self.output_capture.remove([history_id]).await {
            tracing::error!(
                %history_id,
                ?err,
                "failed to discard the captured output of a cancelled command"
            );
        }

        let (_id, cmd) = self
            .active_cmds
            .remove(&history_id)
            .expect("still present: re-checked under the finalization mutex every remover holds");

        let _ = self.broadcast.send(CmdEvent::Cancelled(cmd.history));

        Ok(())
    }

    /// Delete the given history entries from Atuin's memory completely, including any captured
    /// output they have, and refuse output for them from then on.
    ///
    /// `search_settings` is needed to rebuild the search index's frecency map after the deletion,
    /// so the swapped-in index has correct rankings immediately rather than after the next refresh.
    ///
    /// Returns how many history entries Atuin forgot.
    ///
    /// This function is serialized on the **bundle** of the `ids` you're passing in, eg. if you
    /// pass `ids = [1, 2]` and then in parallel another delete call with `ids = [2, 3]`, the latter
    /// will wait for the former request to completely go through.
    pub async fn delete(

View on GitHub (pinned to c0c717ab04)

Solutions

  1. Report this as a bug to Atuin — it indicates an internal locking invariant violation
  2. Check recently modified daemon code paths that touch `active_cmds` for missed mutex acquisition
  3. Capture a stack trace with RUST_BACKTRACE=1 and attach it to the issue report
  4. As a workaround, restart the daemon to clear corrupted in-memory state

Example fix

// before
let (_id, cmd) = self.active_cmds.remove(&history_id)
    .expect("still present: re-checked under the finalization mutex...");
// after
let Some((_id, cmd)) = self.active_cmds.remove(&history_id) else {
    return Err(DaemonError::Internal("active command vanished despite finalization mutex".into()));
};
Defensive patterns

Strategy: try-catch

Try / catch

// Treat as an internal bug; surface it instead of panicking:
let Some((_id, cmd)) = self.active_cmds.remove(&history_id) else {
    tracing::error!("cancel: active_cmds entry missing for {history_id}");
    return Err(DaemonError::Internal("lost active command".into()));
};

Prevention

When it happens

Trigger: Calling `cancel(history_id)` for a command whose active-cmd entry was concurrently removed by another path, despite the invariant stating removers must hold the finalization mutex first.

Common situations: A race between cancel and finish/cleanup paths in the daemon; a bug introduced by modifying one remover to bypass the mutex; duplicated cancel requests processed in parallel.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of atuinsh/atuin@c0c717ab04 (2026-09-12). Data as JSON: /api/errors/1c73314462598fd6. Report an issue: GitHub.