mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 10:21:16 +08:00
fix(codex): display renamed session titles (#4927)
* fix(codex): display renamed session titles * fix(codex): resolve custom state db for titles * refactor(codex): dedupe state-db resolution and harden title lookup - Extract a shared `codex_state_db` module for `state_5.sqlite` path resolution (config `sqlite_home` / `CODEX_SQLITE_HOME`), previously copy-pasted verbatim between codex_history_migration and the codex session provider. `state_5.sqlite` is now a single source of truth. - Add `busy_timeout` to the read-only title query; without it a read during a concurrent Codex write fails immediately (SQLITE_BUSY) and renamed titles silently fall back to the first user message. - Push the `title == first_user_message` comparison into the SQL WHERE clause (NULL-safe, aligning with Codex's `distinct_thread_metadata_title` semantics) and stop SELECTing `first_user_message`, which can hold large values (openai/codex#29007) — the comparison belongs in SQL anyway, so the column no longer needs to cross into Rust.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
//! Locating Codex's per-thread state SQLite databases.
|
||||
//!
|
||||
//! Codex stores thread metadata in `state_5.sqlite`, normally inside the Codex
|
||||
//! config dir (`CODEX_HOME` / `~/.codex`). The SQLite location can be moved with
|
||||
//! the `sqlite_home` key in `config.toml` or the `CODEX_SQLITE_HOME` env var;
|
||||
//! when set, a second DB lives there. Both history migration and the session
|
||||
//! list's title lookup need the same resolution, so it lives here once.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
use crate::config::get_home_dir;
|
||||
|
||||
/// Filename of Codex's per-thread state database. Codex bumps the version
|
||||
/// number across releases; update this single source of truth when a new state
|
||||
/// DB version ships.
|
||||
pub(crate) const CODEX_STATE_DB_FILENAME: &str = "state_5.sqlite";
|
||||
|
||||
/// Env var that overrides the Codex SQLite state directory.
|
||||
const CODEX_SQLITE_HOME_ENV: &str = "CODEX_SQLITE_HOME";
|
||||
|
||||
/// Resolve every candidate `state_5.sqlite` path: the config-dir DB plus, when
|
||||
/// Codex is configured to keep its SQLite state elsewhere, that DB too.
|
||||
///
|
||||
/// `config_dir` is the Codex config dir (`~/.codex`); `config_text` is the raw
|
||||
/// `config.toml` contents, used to detect a `sqlite_home` override.
|
||||
pub(crate) fn codex_state_db_paths(config_dir: &Path, config_text: &str) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
push_unique_path(&mut paths, config_dir.join(CODEX_STATE_DB_FILENAME));
|
||||
// Codex lets SQLite state move away from CODEX_HOME; config takes precedence.
|
||||
if let Some(sqlite_home) = sqlite_home_from_codex_config(config_text) {
|
||||
push_unique_path(&mut paths, sqlite_home.join(CODEX_STATE_DB_FILENAME));
|
||||
} else if let Some(sqlite_home) = sqlite_home_from_env() {
|
||||
push_unique_path(&mut paths, sqlite_home.join(CODEX_STATE_DB_FILENAME));
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlite_home_from_codex_config(config_text: &str) -> Option<PathBuf> {
|
||||
let doc = config_text.parse::<DocumentMut>().ok()?;
|
||||
let raw = doc.get("sqlite_home")?.as_str()?.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(resolve_user_path(raw))
|
||||
}
|
||||
|
||||
fn sqlite_home_from_env() -> Option<PathBuf> {
|
||||
let raw = std::env::var(CODEX_SQLITE_HOME_ENV).ok()?;
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(resolve_user_path(raw))
|
||||
}
|
||||
|
||||
fn resolve_user_path(raw: &str) -> PathBuf {
|
||||
if raw == "~" {
|
||||
return get_home_dir();
|
||||
}
|
||||
if let Some(rest) = raw.strip_prefix("~/") {
|
||||
return get_home_dir().join(rest);
|
||||
}
|
||||
if let Some(rest) = raw.strip_prefix("~\\") {
|
||||
return get_home_dir().join(rest);
|
||||
}
|
||||
PathBuf::from(raw)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn includes_config_sqlite_home() {
|
||||
let temp = tempdir().expect("tempdir");
|
||||
let sqlite_home = temp.path().join("sqlite-home");
|
||||
let config_text = format!("sqlite_home = \"{}\"\n", sqlite_home.display());
|
||||
|
||||
let paths = codex_state_db_paths(temp.path(), &config_text);
|
||||
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
temp.path().join(CODEX_STATE_DB_FILENAME),
|
||||
sqlite_home.join(CODEX_STATE_DB_FILENAME),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user