Harden named session lookup in the TUI (#37157)

## What changed

- Share exact-name candidate lookup between resume and archive commands.
- Prefer valid SQLite names for local sessions, then recover legacy index names without overriding newer metadata.
- Validate rollout identity, collection, source, and model provider before selecting a local session, and choose the most recently modified eligible legacy duplicate.
- Preserve server-side lookup behavior for remote workspaces.

## Testing

- Cover stale and duplicate SQLite entries, legacy-name recovery, source and provider filtering, paginated histories, and duplicate legacy names.

GitOrigin-RevId: c69f86d64ff556bc7b463d13522a9241591eb070
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-08-05 20:44:03 +00:00
1 parent b6cddbf6d5
commit c38a60ded2
8 files changed
+976 -193

No files matched your search

+48 -8
View File
@@ -7,11 +7,13 @@ use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::SystemTime;
use crate::reverse_jsonl_scanner::ReverseJsonlScanner;
use crate::reverse_jsonl_scanner::ScanOutcome;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use serde::Deserialize;
use serde::Serialize;
use tokio::io::AsyncBufReadExt;
@@ -150,19 +152,42 @@ pub async fn find_thread_names_by_ids(
Ok(names)
}
/// Locate a recorded thread rollout and read its session metadata by thread name.
/// Returns the newest indexed name that still has a readable rollout header.
/// Locate the readable rollout with the newest modification time for a recorded thread name.
pub async fn find_thread_meta_by_name_str(
codex_home: &Path,
name: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
) -> std::io::Result<Option<(PathBuf, SessionMetaLine)>> {
Ok(find_thread_meta_candidates_by_name_str(
codex_home,
name,
state_db_ctx,
/*allowed_sources*/ &[],
/*allowed_model_providers*/ &[],
)
.await?
.into_iter()
.next())
}
/// Locate readable rollouts for a recorded thread name, newest modification time first.
///
/// Empty filter slices disable their respective filters. Provider filtering only rejects explicit
/// mismatches because older rollouts omitted provider metadata. Filtering happens before ranking,
/// so an ineligible newer duplicate cannot hide an older usable session.
pub async fn find_thread_meta_candidates_by_name_str(
codex_home: &Path,
name: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
allowed_sources: &[SessionSource],
allowed_model_providers: &[String],
) -> std::io::Result<Vec<(PathBuf, SessionMetaLine)>> {
if name.trim().is_empty() {
return Ok(None);
return Ok(Vec::new());
}
let path = session_index_path(codex_home);
if !path.exists() {
return Ok(None);
return Ok(Vec::new());
}
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let name = name.to_string();
@@ -171,6 +196,7 @@ pub async fn find_thread_meta_by_name_str(
let scan =
tokio::task::spawn_blocking(move || stream_thread_ids_from_end_by_name(&path, &name, tx));
let mut candidates = Vec::new();
while let Some(thread_id) = rx.recv().await {
// Keep walking until a matching id resolves to a loadable rollout so an unsaved or partial
// rename cannot shadow an older persisted session with the same name.
@@ -181,15 +207,29 @@ pub async fn find_thread_meta_by_name_str(
)
.await?
&& let Ok(session_meta) = super::list::read_session_meta_line(&path).await
&& (allowed_sources.is_empty() || allowed_sources.contains(&session_meta.meta.source))
&& session_meta
.meta
.model_provider
.as_ref()
.is_none_or(|provider| {
allowed_model_providers.is_empty() || allowed_model_providers.contains(provider)
})
{
drop(rx);
scan.await.map_err(std::io::Error::other)??;
return Ok(Some((path, session_meta)));
let modified = tokio::fs::metadata(&path)
.await
.and_then(|metadata| metadata.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
candidates.push((modified, path, session_meta));
}
}
scan.await.map_err(std::io::Error::other)??;
candidates.sort_by(|(left, _, _), (right, _, _)| right.cmp(left));
Ok(None)
Ok(candidates
.into_iter()
.map(|(_, path, session_meta)| (path, session_meta))
.collect())
}
fn session_index_path(codex_home: &Path) -> PathBuf {