mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why Archiving threads reads metadata from every active rollout, including unrelated compressed files. ## What changed Add `RolloutReferenceIndex::scan_unarchived_threads` and use it when archiving. Filter canonical filenames by the requested thread IDs before reading contents, while retaining metadata-based ownership checks. ## Testing Add tests covering reverted rollouts, requested descendants, compressed rollouts, exclusion of archived and unrelated files, and candidate ownership checks. GitOrigin-RevId: a64f7fa25e5493ed20312f1d8216422752272bae
169 lines
5.9 KiB
Rust
169 lines
5.9 KiB
Rust
//! Indexes direct fork references found in local rollout files.
|
|
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::collections::hash_map::Entry;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use codex_protocol::RolloutId;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::protocol::HistoryPosition;
|
|
|
|
use crate::ARCHIVED_SESSIONS_SUBDIR;
|
|
use crate::SESSIONS_SUBDIR;
|
|
use crate::compression::RolloutFile;
|
|
use crate::rollout_file_name::RolloutFileName;
|
|
|
|
/// Direct history-base edges discovered from local rollout metadata.
|
|
///
|
|
/// This indexes immutable rollout IDs, not a thread's selected lineage. Callers use it to answer
|
|
/// cheap inverse-reference questions without each reimplementing rollout discovery.
|
|
#[derive(Debug, Default)]
|
|
pub struct RolloutReferenceIndex {
|
|
rollouts_by_id: HashMap<RolloutId, IndexedRollout>,
|
|
reference_counts_by_rollout: HashMap<RolloutId, usize>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct IndexedRollout {
|
|
thread_id: ThreadId,
|
|
path: PathBuf,
|
|
history_base: Option<HistoryPosition>,
|
|
}
|
|
|
|
impl RolloutReferenceIndex {
|
|
/// Scans active and archived local rollout metadata.
|
|
pub async fn scan(codex_home: &Path) -> io::Result<Self> {
|
|
Self::scan_paths(
|
|
vec![
|
|
codex_home.join(ARCHIVED_SESSIONS_SUBDIR),
|
|
codex_home.join(SESSIONS_SUBDIR),
|
|
],
|
|
/*thread_ids*/ None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Scans only unarchived rollouts to locate files that still need to be archived.
|
|
///
|
|
/// Reference counts exclude archived history and must not be used to decide whether a
|
|
/// rollout can be deleted or compressed.
|
|
pub async fn scan_unarchived(codex_home: &Path) -> io::Result<Self> {
|
|
Self::scan_paths(
|
|
vec![codex_home.join(SESSIONS_SUBDIR)],
|
|
/*thread_ids*/ None,
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Scans unarchived files whose canonical filenames belong to the requested threads.
|
|
///
|
|
/// Skips unrelated rollout contents, including compressed files. Metadata still determines
|
|
/// ownership among the candidates. Reference counts are partial and must not be used to
|
|
/// decide whether a rollout can be deleted or compressed.
|
|
pub async fn scan_unarchived_threads(
|
|
codex_home: &Path,
|
|
thread_ids: &[ThreadId],
|
|
) -> io::Result<Self> {
|
|
let thread_ids = thread_ids.iter().copied().collect();
|
|
Self::scan_paths(vec![codex_home.join(SESSIONS_SUBDIR)], Some(&thread_ids)).await
|
|
}
|
|
|
|
async fn scan_paths(
|
|
mut stack: Vec<PathBuf>,
|
|
thread_ids: Option<&HashSet<ThreadId>>,
|
|
) -> io::Result<Self> {
|
|
let mut rollouts_by_id = HashMap::new();
|
|
while let Some(directory) = stack.pop() {
|
|
let mut entries = match tokio::fs::read_dir(directory.as_path()).await {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
|
Err(err) => return Err(err),
|
|
};
|
|
loop {
|
|
let Some(entry) = entries.next_entry().await? else {
|
|
break;
|
|
};
|
|
let path = entry.path();
|
|
let file_type = entry.file_type().await?;
|
|
if file_type.is_dir() {
|
|
stack.push(path);
|
|
continue;
|
|
}
|
|
if !file_type.is_file() {
|
|
continue;
|
|
}
|
|
let Some(rollout_file) = RolloutFile::from_path(path) else {
|
|
continue;
|
|
};
|
|
let Some(file_name) = RolloutFileName::parse(rollout_file.plain_file_name()) else {
|
|
continue;
|
|
};
|
|
if thread_ids.is_some_and(|ids| !ids.contains(&file_name.thread_id())) {
|
|
continue;
|
|
}
|
|
let rollout_id = file_name.rollout_id();
|
|
let Ok(meta) = crate::read_session_meta_line(rollout_file.path()).await else {
|
|
continue;
|
|
};
|
|
if let Entry::Vacant(entry) = rollouts_by_id.entry(rollout_id) {
|
|
entry.insert(IndexedRollout {
|
|
thread_id: meta.meta.id,
|
|
path: rollout_file.into_path(),
|
|
history_base: meta.meta.history_base,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut reference_counts_by_rollout = HashMap::new();
|
|
for (rollout_id, rollout) in &rollouts_by_id {
|
|
let Some(history_base) = rollout.history_base else {
|
|
continue;
|
|
};
|
|
if history_base.thread_id == *rollout_id {
|
|
continue;
|
|
}
|
|
*reference_counts_by_rollout
|
|
.entry(history_base.thread_id)
|
|
.or_default() += 1;
|
|
}
|
|
Ok(Self {
|
|
rollouts_by_id,
|
|
reference_counts_by_rollout,
|
|
})
|
|
}
|
|
|
|
/// Returns how many other discovered rollouts directly reference `rollout_id`.
|
|
pub fn reference_count(&self, rollout_id: RolloutId) -> usize {
|
|
self.reference_counts_by_rollout
|
|
.get(&rollout_id)
|
|
.copied()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Returns the direct history-base edge for `rollout_id`, if one was discovered.
|
|
pub fn history_base(&self, rollout_id: RolloutId) -> Option<&HistoryPosition> {
|
|
self.rollouts_by_id
|
|
.get(&rollout_id)
|
|
.and_then(|rollout| rollout.history_base.as_ref())
|
|
}
|
|
|
|
/// Returns rollout IDs and paths whose session metadata belongs to `thread_id`.
|
|
pub fn rollouts_for_thread(
|
|
&self,
|
|
thread_id: ThreadId,
|
|
) -> impl Iterator<Item = (RolloutId, &Path)> {
|
|
self.rollouts_by_id
|
|
.iter()
|
|
.filter(move |(_, rollout)| rollout.thread_id == thread_id)
|
|
.map(|(rollout_id, rollout)| (*rollout_id, rollout.path.as_path()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "rollout_reference_index_tests.rs"]
|
|
mod tests;
|