mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
b6cddbf6d5
commit
c38a60ded2
8 files changed
+976
-193
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user