{"record":{"id":"1c73314462598fd6","repo":"atuinsh/atuin","slug":"still-present-re-checked-under-the-finalization-mutex-every","errorCode":null,"errorMessage":"still present: re-checked under the finalization mutex every remover holds","messagePattern":"still present: re-checked under the finalization mutex every remover holds","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-daemon/src/history_journal.rs","lineNumber":439,"sourceCode":"            .ok_or(CmdCancelError::NotFound(history_id))?;\n        let _guard = lock.lock().await;\n\n        if !self.active_cmds.contains_key(&history_id) {\n            return Err(CmdCancelError::NotFound(history_id));\n        }\n\n        if let Err(err) = self.output_capture.remove([history_id]).await {\n            tracing::error!(\n                %history_id,\n                ?err,\n                \"failed to discard the captured output of a cancelled command\"\n            );\n        }\n\n        let (_id, cmd) = self\n            .active_cmds\n            .remove(&history_id)\n            .expect(\"still present: re-checked under the finalization mutex every remover holds\");\n\n        let _ = self.broadcast.send(CmdEvent::Cancelled(cmd.history));\n\n        Ok(())\n    }\n\n    /// Delete the given history entries from Atuin's memory completely, including any captured\n    /// output they have, and refuse output for them from then on.\n    ///\n    /// `search_settings` is needed to rebuild the search index's frecency map after the deletion,\n    /// so the swapped-in index has correct rankings immediately rather than after the next refresh.\n    ///\n    /// Returns how many history entries Atuin forgot.\n    ///\n    /// This function is serialized on the **bundle** of the `ids` you're passing in, eg. if you\n    /// pass `ids = [1, 2]` and then in parallel another delete call with `ids = [2, 3]`, the latter\n    /// will wait for the former request to completely go through.\n    pub async fn delete(","sourceCodeStart":421,"sourceCodeEnd":457,"githubUrl":"https://github.com/atuinsh/atuin/blob/c0c717ab04c881764bcad4b3d169a507e2432643/crates/atuin-daemon/src/history_journal.rs#L421-L457","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Report this as a bug to Atuin — it indicates an internal locking invariant violation","Check recently modified daemon code paths that touch `active_cmds` for missed mutex acquisition","Capture a stack trace with RUST_BACKTRACE=1 and attach it to the issue report","As a workaround, restart the daemon to clear corrupted in-memory state"],"exampleFix":"// before\nlet (_id, cmd) = self.active_cmds.remove(&history_id)\n    .expect(\"still present: re-checked under the finalization mutex...\");\n// after\nlet Some((_id, cmd)) = self.active_cmds.remove(&history_id) else {\n    return Err(DaemonError::Internal(\"active command vanished despite finalization mutex\".into()));\n};","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Treat as an internal bug; surface it instead of panicking:\nlet Some((_id, cmd)) = self.active_cmds.remove(&history_id) else {\n    tracing::error!(\"cancel: active_cmds entry missing for {history_id}\");\n    return Err(DaemonError::Internal(\"lost active command\".into()));\n};","preventionTips":["Audit every code path that mutates shared maps for consistent lock usage","Prefer returning Result over expect for map lookups in concurrent code","Run daemon tests under stress/loom-style concurrency checkers"],"tags":["concurrency","invariant","panic","internal"],"backgroundTag":"internal-invariant-violation","analyzedSha":"c0c717ab04c881764bcad4b3d169a507e2432643","analyzedAt":"2026-09-12T07:40:01.341Z","contentChangedAt":"2026-09-12T07:40:01.341Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}