atuinsh/atuin · critical
output-capture delete task panicked
Error message
output-capture delete task panicked
What it means
This message is the `.expect()` on the `JoinHandle` returned by `spawn_blocking` inside `FjallBackendInner::remove` (crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs:132). It means the blocking task performing the batched key deletion panicked before returning its Result. The closure's own fallible paths return `DeleteOutputError::Storage`, so this panic almost always comes from the `unreachable!("a blind remove performs no reads, so it can never conflict")` branch firing on `fjall::Conflict`, or a task-abort JoinError — not from ordinary delete failures.
Source
Thrown at crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs:132
let dirty = self.dirty.clone();
tokio::task::spawn_blocking(move || {
let mut tx = db.write_tx().map_err(|err| DeleteOutputError::Storage(Box::new(err)))?;
for key in keys {
tx.remove(&keyspace, key);
}
match tx.commit().map_err(|err| DeleteOutputError::Storage(Box::new(err)))? {
Ok(()) => {
dirty.store(true, Ordering::Release);
Ok(())
}
// fjall only reports conflicts for transactions that read; this one never does.
Err(fjall::Conflict) => {
unreachable!("a blind remove performs no reads, so it can never conflict")
}
}
})
.await
.expect("output-capture delete task panicked")
}
/// Deletes the oldest stored entries until their values total at least `reclaim_bytes`,
/// returning the number of bytes actually freed.
async fn reclaim(&self, reclaim_bytes: u64) -> Result<u64, DeleteOutputError> {
if reclaim_bytes == 0 {
return Ok(0);
}
let db = self.db.clone();
let keyspace = self.keyspace.clone();
let dirty = self.dirty.clone();
tokio::task::spawn_blocking(move || {
let mut tx = db.write_tx().map_err(|err| DeleteOutputError::Storage(Box::new(err)))?;
let mut freed: u64 = 0;
for guard in keyspace.inner().iter() {
let (key, value) =View on GitHub (pinned to c0c717ab04)
Solutions
- Check the fjall crate version against the one atuin-daemon pinned; if a newer fjall changed Conflict semantics for read-free transactions, upgrade/downgrade to a compatible version
- Report/inspect the JoinError payload to confirm whether the panic is the `unreachable!()` Conflict branch — if so it is a fjall internal assumption bug, not user error
- Retry the removal after restarting the daemon; transient conflict behavior may not recur
- Delete the affected entries individually instead of in one batched transaction to isolate which key triggers the conflict
Example fix
// before
Err(fjall::Conflict) => {
unreachable!("a blind remove performs no reads, so it can never conflict")
}
// after: surface the conflict instead of panicking
Err(fjall::Conflict) => Err(DeleteOutputError::Storage(
"unexpected conflict on blind remove".into(),
)), Defensive patterns
Strategy: retry
Validate before calling
// Pre-check that the ids exist so the remove path does the minimal // blind transaction; empty ids short-circuit before any task spawn. let existing: Vec<_> = ids.into_iter().filter(|id| capture_store.get(*id).ok().flatten().is_some()).collect();
Type guard
fn is_clean_result(r: &Result<(), DeleteOutputError>) -> bool {
!matches!(r, Err(DeleteOutputError::Storage(_)))
} Try / catch
// Run the removal in a supervised child task; a panic becomes a
// retryable JoinError instead of killing the caller.
match tokio::spawn(backend.remove(ids.clone())).await {
Ok(Ok(())) => {},
Ok(Err(e)) | Err(_) => schedule_retry(ids, e),
} Prevention
- Pin the fjall version atuin-daemon was tested against; review its CHANGELOG for transaction-conflict semantics changes
- Avoid running concurrent bulk deletes and heavy writes; serialize destructive maintenance
- Verify data-dir integrity after any crash before resuming deletion workloads
- Keep the daemon shutdown graceful so blocking delete tasks are not cancelled mid-transaction
When it happens
Trigger: Calling the backend's `remove(ids)` when: (1) fjall unexpectedly reports `Conflict` on the blind (read-free) remove transaction, hitting the `unreachable!()`; (2) `tx.remove`/keyspace operations panic inside fjall; (3) the blocking task is cancelled at shutdown (JoinError) which `.expect()` also turns into this panic.
Common situations: Bulk-deleting output captures for pruned history entries; garbage collection racing heavy concurrent writers on the same keyspace under a fjall version whose optimistic transactions behave differently than assumed; daemon shutdown mid-operation.
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
- output-capture read task panicked
- output-capture reclaim task panicked
- persistence task shouldn't panic
- output-capture write task panicked
- failed to register sigterm handler
AI-assisted analysis of atuinsh/atuin@c0c717ab04 (2026-09-12).
Data as JSON: /api/errors/57a604a41eca2838.
Report an issue: GitHub.