Keep thread storage available when diagnostic logs fail (#60466)

## Why

Failure to open the diagnostic logs database should not disable thread storage or database-only history listing. Thread deletion must still preserve saved conversations when diagnostic log cleanup cannot complete.

## What changed

- Allow state initialization to continue after non-corruption errors opening the logs database, while preserving corruption recovery.
- Report the original diagnostic database error after tracing is installed in the TUI and app server, and return it from log operations.
- Reject thread deletion before destructive cleanup when diagnostic storage failed to initialize.
- Continue periodic cleanup of legacy logs when the current logs database is unavailable.

## Testing

Extend coverage for unavailable log directories, database-only history listing, startup diagnostics, deletion preserving saved conversations, corrupt log headers, and periodic legacy log cleanup during a diagnostic storage outage.

GitOrigin-RevId: a5184580ae6c39ed0c4eefa8cb48ee7731a991c3
This commit is contained in:
Dmitriy Kovalenko
2026-11-09 23:22:08 +00:00
committed by copyberry
parent e1cc96712f
commit caae161dd0
11 changed files with 257 additions and 90 deletions
@@ -5,6 +5,7 @@
//! associated rollout is removed, under the same lifecycle lock, so queued artifact mutations cannot
//! use deleted thread metadata. Host-owned data is cleaned after reference and writer checks,
//! before removing rollouts, so cleanup failures remain retryable even without a state DB.
//! Known diagnostic database failures must be rejected before any destructive cleanup.
use std::collections::HashMap;
use std::collections::HashSet;
@@ -41,11 +42,25 @@ impl LocalThreadStore {
&self,
thread_ids: &[ThreadId],
) -> ThreadStoreResult<()> {
ensure_diagnostic_logs_available(self)?;
let reference_index = scan_reference_index(self).await?;
check_attached_children(&reference_index, thread_ids)
}
}
fn ensure_diagnostic_logs_available(store: &LocalThreadStore) -> ThreadStoreResult<()> {
if let Some(error) = store
.state_db
.as_ref()
.and_then(|db| db.diagnostic_logs_error())
{
return Err(ThreadStoreError::Internal {
message: format!("cannot delete thread while diagnostic logs are unavailable: {error}"),
});
}
Ok(())
}
fn check_attached_children(
reference_index: &RolloutReferenceIndex,
thread_ids: &[ThreadId],
@@ -94,6 +109,7 @@ pub(super) async fn delete_thread(
store: &LocalThreadStore,
params: DeleteThreadParams,
) -> ThreadStoreResult<()> {
ensure_diagnostic_logs_available(store)?;
let thread_id = params.thread_id;
let _lifecycle_guard = store.live_writer_locks.lock_lifecycle(thread_id).await;
let _live_writer_guard = store.live_writer_locks.lock(thread_id).await;
@@ -129,6 +145,7 @@ pub(super) async fn delete_threads(
return Ok(());
}
ensure_diagnostic_logs_available(store)?;
let mut lock_thread_ids = thread_ids.clone();
lock_thread_ids.sort_unstable_by_key(ToString::to_string);
lock_thread_ids.dedup();