{"record":{"id":"46b972979d9c2d87","repo":"atuinsh/atuin","slug":"output-capture-reclaim-task-panicked","errorCode":null,"errorMessage":"output-capture reclaim task panicked","messagePattern":"output-capture reclaim task panicked","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs","lineNumber":172,"sourceCode":"\n                freed = freed.saturating_add(u64::try_from(value.len()).unwrap_or(u64::MAX));\n                if freed >= reclaim_bytes {\n                    break;\n                }\n            }\n\n            match tx.commit().map_err(|err| DeleteOutputError::Storage(Box::new(err)))? {\n                Ok(()) => {\n                    dirty.store(true, Ordering::Release);\n                    Ok(freed)\n                }\n                Err(fjall::Conflict) => {\n                    unreachable!(\"reclaim performs no tracked reads, so it can never conflict\")\n                }\n            }\n        })\n        .await\n        .expect(\"output-capture reclaim task panicked\")\n    }\n}\n\n/// Task responsible for flushing fjall data buffered in memory onto the disk.\n#[derive(Debug)]\nstruct Flusher {\n    /// Handle to the background task.\n    task: JoinHandle<()>,\n}\n\nimpl Flusher {\n    /// How often to try to flush.\n    ///\n    /// We'd expect flush itself to take anywhere between 1-10ms, so this is plenty of overhead.\n    const SYNC_INTERVAL: Duration = Duration::from_secs(5);\n\n    pub fn spawn(inner: Arc<FjallBackendInner>) -> Self {\n        let task = tokio::task::spawn(async move {","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/atuinsh/atuin/blob/c0c717ab04c881764bcad4b3d169a507e2432643/crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs#L154-L190","documentation":"This is the `.expect()` on the `JoinHandle` of the `spawn_blocking` closure in `FjallBackendInner::reclaim` (crates/atuin-daemon/src/output_capture/backend/fjall/mod.rs:172), which deletes oldest entries until at least `reclaim_bytes` are freed. It surfaces a panic in that blocking task — most plausibly the `unreachable!(\"reclaim performs no tracked reads, so it can never conflict\")` branch on an unexpected `fjall::Conflict`, a panic while iterating keyspace entries, or a JoinError from task cancellation.","triggerScenarios":"Calling `reclaim(reclaim_bytes)` when disk-usage pressure triggers it (DiskUsageLimit) and: (1) the write transaction unexpectedly commits with `fjall::Conflict`, firing `unreachable!()`; (2) `guard.into_inner()` or the iterator panics on a corrupt segment; (3) the blocking task is cancelled during daemon shutdown.","commonSituations":"The daemon's automatic disk-space reclamation kicking in on a large output-capture store; concurrent GC and writer activity under a fjall release whose optimistic-transaction semantics differ from the assumption baked into the `unreachable!()`; corrupted fjall data directory.","solutions":["Check the fjall crate version for changes to Conflict/transaction semantics; align atuin-daemon with a compatible fjall release","Capture the JoinError panic payload to identify the true panic site (conflict branch vs iterator) and file/fix accordingly","Restart the daemon and retry reclaim; if it recurs, move the data dir aside and let the store rebuild","Reduce store size manually (remove old captures) so reclaim iterates fewer entries while the underlying issue is investigated"],"exampleFix":"// before\nErr(fjall::Conflict) => {\n    unreachable!(\"reclaim performs no tracked reads, so it can never conflict\")\n}\n// after: report instead of panicking\nErr(fjall::Conflict) => Err(DeleteOutputError::Storage(\n    \"unexpected conflict during reclaim\".into(),\n)),","handlingStrategy":"retry","validationCode":"// Only invoke reclaim with a meaningful target; the store itself\n// short-circuits 0, and callers should rate-limit GC passes.\nif reclaim_bytes > 0 && store.estimated_disk_usage() > high_watermark {\n    store.reclaim(reclaim_bytes).await?;\n}","typeGuard":"fn reclaimed_ok(r: &Result<u64, DeleteOutputError>) -> bool {\n    matches!(r, Ok(_))\n}","tryCatchPattern":"// Wrap the GC pass so a panic is contained and retried later:\nmatch tokio::spawn(backend.reclaim(bytes)).await {\n    Ok(Ok(freed)) => log_freed(freed),\n    Ok(Err(e)) | Err(_) => defer_gc_with_backoff(e),\n}","preventionTips":["Set DiskUsageLimit conservatively so reclaim runs before the store is very large","Pin compatible fjall versions; audit any fjall upgrade touching optimistic transactions","Run reclaim when write traffic is low to avoid concurrent-commit surprises","Check disk health; iterator panics during reclaim often follow I/O corruption"],"tags":["panic","storage","fjall","gc","tokio"],"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"}