tui: recover local state db startup failures (#22734)

## Why

#22580 made app-server startup fail when the local SQLite state database
cannot be initialized. Embedded/local TUI startup still continued on the
permissive path, which left the CLI inconsistent and could hide a real
startup problem behind unrelated UI. This brings local TUI startup onto
the same fail-closed behavior while keeping recovery humane for the two
failure modes we are seeing in practice: damaged database files and
startup stalls caused by another process holding the database write
lock.

## What changed

- Embedded TUI startup now uses `state_db::try_init(...)` and returns a
typed `LocalStateDbStartupError` that preserves the affected database
path plus the underlying failure detail.
- CLI startup handles that failure before entering the interactive TUI:
- lock-contention failures tell users to quit other Codex processes and
try again
- failures consistent with a broken local database offer a safe repair
that backs up Codex-owned SQLite files, rebuilds local database files,
and retries startup once
- declined or unsuccessful repairs print concise guidance plus technical
details
- Shared startup error plumbing lives in `tui/src/startup_error.rs`,
while CLI recovery policy and focused recovery tests live in
`cli/src/state_db_recovery.rs`.

## Verification

- `cargo test -p codex-tui
embedded_state_db_failure_is_typed_for_cli_recovery`
- `cargo test -p codex-cli state_db_recovery`
- Manually held an exclusive SQLite lock on `state_5.sqlite` and
confirmed the CLI shows lock-specific guidance without offering repair.
- Manually exercised the repair path with a deliberately invalid
`sqlite_home` and confirmed it backs up the blocking path and resumes
startup.
This commit is contained in:
Eric Traut
2026-05-14 18:51:36 -07:00
committed by GitHub
Unverified
parent 3c6d727810
commit 3a23e87e20
6 changed files with 310 additions and 16 deletions
+43 -7
View File
@@ -52,6 +52,7 @@ mod doctor;
mod marketplace_cmd;
mod mcp_cmd;
mod plugin_cmd;
mod state_db_recovery;
#[cfg(not(windows))]
mod wsl_paths;
@@ -59,6 +60,7 @@ use crate::mcp_cmd::McpCli;
use crate::plugin_cmd::PluginCli;
use crate::plugin_cmd::PluginSubcommand;
use doctor::DoctorCommand;
use state_db_recovery as local_state_db;
use codex_config::LoaderOverrides;
use codex_core::build_models_manager;
@@ -1980,13 +1982,47 @@ async fn run_interactive_tui(
};
*slot = Some(auth_token);
}
codex_tui::run_main(
interactive,
arg0_paths,
codex_config::LoaderOverrides::default(),
remote_endpoint,
)
.await
let start_tui = || {
codex_tui::run_main(
interactive.clone(),
arg0_paths.clone(),
codex_config::LoaderOverrides::default(),
remote_endpoint.clone(),
)
};
let mut attempted_repair = false;
loop {
let err = match start_tui().await {
Ok(exit_info) => return Ok(exit_info),
Err(err) => err,
};
let Some(startup_error) = local_state_db::startup_error(&err) else {
return Err(err);
};
if local_state_db::is_locked(startup_error.detail()) {
local_state_db::print_locked_guidance(startup_error);
return Ok(AppExitInfo::fatal(startup_error.to_string()));
}
if attempted_repair {
local_state_db::print_diagnostic_guidance(startup_error);
return Ok(AppExitInfo::fatal(startup_error.to_string()));
}
if !local_state_db::confirm_repair(startup_error)? {
local_state_db::print_diagnostic_guidance(startup_error);
return Ok(AppExitInfo::fatal(startup_error.to_string()));
}
match local_state_db::repair_files(startup_error).await {
Ok(backups) => local_state_db::print_repair_backups(&backups),
Err(repair_err) => {
local_state_db::print_diagnostic_guidance(startup_error);
return Ok(AppExitInfo::fatal(format!(
"failed to repair Codex local data automatically: {repair_err}"
)));
}
}
attempted_repair = true;
}
}
fn confirm(prompt: &str) -> std::io::Result<bool> {