{"record":{"id":"08aefb174dd6d7b8","repo":"atuinsh/atuin","slug":"output-capture-read-task-panicked","errorCode":null,"errorMessage":"output-capture read task panicked","messagePattern":"output-capture read task panicked","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs","lineNumber":98,"sourceCode":"        .expect(\"output-capture write task panicked\")\n    }\n\n    async fn get(&self, id: HistoryId) -> Result<Option<CommandCapture>, GetOutputError> {\n        let keyspace = self.keyspace.clone();\n        let key = ActiveSchema::serialize_key(id).expect(\"history id serialization is infallible\");\n\n        tokio::task::spawn_blocking(move || {\n            match keyspace.get(key).map_err(|err| GetOutputError::Storage(Box::new(err)))? {\n                Some(slice) => {\n                    let capture = ActiveSchema::deserialize_value(slice.to_vec())\n                        .expect(\"stored value is a valid CommandCapture\");\n                    Ok(Some(capture))\n                }\n                None => Ok(None),\n            }\n        })\n        .await\n        .expect(\"output-capture read task panicked\")\n    }\n\n    async fn remove(&self, ids: Vec<HistoryId>) -> Result<(), DeleteOutputError> {\n        let keys: Vec<_> = ids\n            .into_iter()\n            .map(|id| {\n                ActiveSchema::serialize_key(id).expect(\"history id serialization is infallible\")\n            })\n            .collect();\n        if keys.is_empty() {\n            return Ok(());\n        }\n\n        let db = self.db.clone();\n        let keyspace = self.keyspace.clone();\n        let dirty = self.dirty.clone();\n        tokio::task::spawn_blocking(move || {\n            let mut tx = db.write_tx().map_err(|err| DeleteOutputError::Storage(Box::new(err)))?;","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/atuinsh/atuin/blob/c0c717ab04c881764bcad4b3d169a507e2432643/crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs#L80-L116","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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)"],"exampleFix":"// before (inner closure in get)\nlet capture = ActiveSchema::deserialize_value(slice.to_vec())\n    .expect(\"stored value is a valid CommandCapture\");\n// after: fail the read instead of panicking the blocking task\nlet capture = ActiveSchema::deserialize_value(slice.to_vec())\n    .map_err(|err| GetOutputError::Storage(Box::new(err)))?;","handlingStrategy":"try-catch","validationCode":"// Before reading, confirm the store opens and the id exists;\n// corruption is only detectable at read time, so scope the blast radius.\nif capture_store.get(id).is_err() {\n    tracing::error!(?id, \"output capture unreadable; skipping\");\n}","typeGuard":"fn is_readable_result(r: &Result<Option<CommandCapture>, GetOutputError>) -> bool {\n    matches!(r, Ok(_))\n}","tryCatchPattern":"// The expect panics the caller's task, so isolate it and treat\n// panic as an unreadable-record signal:\nlet capture = tokio::spawn(backend.get(id)).await;\nmatch capture {\n    Ok(Ok(Some(c))) => use_capture(c),\n    Ok(Ok(None)) | Err(_) => handle_missing_or_corrupt(id),\n}","preventionTips":["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"],"tags":["panic","storage","fjall","tokio","deserialization"],"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"}