atuinsh/atuin · critical
output-capture read task panicked
Error message
output-capture read task panicked
What it means
This is not a storage error itself: it is the panic surfaced by `.expect()` on the `JoinHandle` of the `tokio::task::spawn_blocking` closure that reads a captured command output from the fjall keyspace (crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs:98). It fires only if that blocking task panicked — e.g. the `.expect("stored value is a valid CommandCapture")` inside the closure when a stored value fails to deserialize, or an unexpected panic in fjall's read path. By the time this message appears, the daemon's async task has already aborted with a panic.
Source
Thrown at crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs:98
.expect("output-capture write task panicked")
}
async fn get(&self, id: HistoryId) -> Result<Option<CommandCapture>, GetOutputError> {
let keyspace = self.keyspace.clone();
let key = ActiveSchema::serialize_key(id).expect("history id serialization is infallible");
tokio::task::spawn_blocking(move || {
match keyspace.get(key).map_err(|err| GetOutputError::Storage(Box::new(err)))? {
Some(slice) => {
let capture = ActiveSchema::deserialize_value(slice.to_vec())
.expect("stored value is a valid CommandCapture");
Ok(Some(capture))
}
None => Ok(None),
}
})
.await
.expect("output-capture read task panicked")
}
async fn remove(&self, ids: Vec<HistoryId>) -> Result<(), DeleteOutputError> {
let keys: Vec<_> = ids
.into_iter()
.map(|id| {
ActiveSchema::serialize_key(id).expect("history id serialization is infallible")
})
.collect();
if keys.is_empty() {
return Ok(());
}
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)))?;View on GitHub (pinned to c0c717ab04)
Solutions
- Back up and inspect the fjall data directory for the offending key; delete the corrupt record for the failing history id so the read returns None instead of a bad value
- Check whether the data dir was written by a different atuin-daemon schema version; migrate or start with a fresh data dir (move/remove the old output-capture data directory)
- Update atuin-daemon — if the panic stems from deserialize strictness or a fjall bug, a newer release may handle legacy/corrupt values gracefully
- Reproduce with tracing/logging enabled to capture the inner panic message (JoinError panic payload), which names the real fault (deserialize vs fjall I/O)
Example fix
// before (inner closure in get)
let capture = ActiveSchema::deserialize_value(slice.to_vec())
.expect("stored value is a valid CommandCapture");
// after: fail the read instead of panicking the blocking task
let capture = ActiveSchema::deserialize_value(slice.to_vec())
.map_err(|err| GetOutputError::Storage(Box::new(err)))?; Defensive patterns
Strategy: try-catch
Validate before calling
// Before reading, confirm the store opens and the id exists;
// corruption is only detectable at read time, so scope the blast radius.
if capture_store.get(id).is_err() {
tracing::error!(?id, "output capture unreadable; skipping");
} Type guard
fn is_readable_result(r: &Result<Option<CommandCapture>, GetOutputError>) -> bool {
matches!(r, Ok(_))
} Try / catch
// The expect panics the caller's task, so isolate it and treat
// panic as an unreadable-record signal:
let capture = tokio::spawn(backend.get(id)).await;
match capture {
Ok(Ok(Some(c))) => use_capture(c),
Ok(Ok(None)) | Err(_) => handle_missing_or_corrupt(id),
} Prevention
- Run `atuin doctor` / verify the fjall data dir integrity after crashes before heavy reads
- Do not share or downgrade an output-capture data directory across atuin-daemon schema versions
- Back up the fjall data directory; delete only the specific corrupt record instead of the whole store
- Keep atuin-daemon and its fjall dependency versions aligned with a single release line
When it happens
Trigger: Calling the backend's `get(id)` (directly or via the output-capture API) when: (1) a stored value at the history-id key fails `ActiveSchema::deserialize_value` — typically corrupt or schema-version-mismatched data on disk; (2) the fjall keyspace `.get()` call itself panics inside the blocking pool; (3) the blocking runtime is being torn down and refuses to spawn the task (JoinError::cancelled is also hit by expect).
Common situations: Upgrading atuin-daemon across a storage schema change where an old fjall data directory contains records written by a previous `SchemaV` format; disk corruption or truncated segment files in the fjall data dir; killing the daemon mid-write leaving a torn value; running on a filesystem with failing sectors.
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 delete 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/08aefb174dd6d7b8.
Report an issue: GitHub.