codex_rollout/
rollout_reference_index.rs1use std::collections::HashMap;
4use std::collections::HashSet;
5use std::io;
6use std::path::Path;
7use std::time::Duration;
8use std::time::Instant;
9
10use codex_protocol::ThreadId;
11use codex_protocol::protocol::HistoryPosition;
12
13use crate::ARCHIVED_SESSIONS_SUBDIR;
14use crate::SESSIONS_SUBDIR;
15use crate::compression::RolloutFile;
16
17#[derive(Debug, Default)]
22pub struct RolloutReferenceIndex {
23 history_base_by_thread: HashMap<ThreadId, HistoryPosition>,
24 reference_counts_by_thread: HashMap<ThreadId, usize>,
25}
26
27impl RolloutReferenceIndex {
28 pub async fn scan(codex_home: &Path) -> io::Result<Self> {
30 let Some(index) = Self::scan_with_deadline(codex_home, ScanDeadline::Unlimited).await?
31 else {
32 return Err(io::Error::other(
33 "unlimited rollout reference scan exceeded a deadline",
34 ));
35 };
36 Ok(index)
37 }
38
39 pub(crate) async fn scan_until(
43 codex_home: &Path,
44 started_at: Instant,
45 max_runtime: Duration,
46 ) -> io::Result<Option<Self>> {
47 Self::scan_with_deadline(
48 codex_home,
49 ScanDeadline::Until {
50 started_at,
51 max_runtime,
52 },
53 )
54 .await
55 }
56
57 pub fn reference_count(&self, thread_id: ThreadId) -> usize {
59 self.reference_counts_by_thread
60 .get(&thread_id)
61 .copied()
62 .unwrap_or_default()
63 }
64
65 pub fn history_base(&self, thread_id: ThreadId) -> Option<&HistoryPosition> {
67 self.history_base_by_thread.get(&thread_id)
68 }
69
70 async fn scan_with_deadline(
71 codex_home: &Path,
72 deadline: ScanDeadline,
73 ) -> io::Result<Option<Self>> {
74 let mut history_base_by_thread = HashMap::new();
75 let mut seen_thread_ids = HashSet::new();
76 let mut stack = vec![
77 codex_home.join(ARCHIVED_SESSIONS_SUBDIR),
78 codex_home.join(SESSIONS_SUBDIR),
79 ];
80 while let Some(directory) = stack.pop() {
81 if deadline.expired() {
82 return Ok(None);
83 }
84 let mut entries = match tokio::fs::read_dir(directory.as_path()).await {
85 Ok(entries) => entries,
86 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
87 Err(err) => return Err(err),
88 };
89 loop {
90 if deadline.expired() {
91 return Ok(None);
92 }
93 let Some(entry) = entries.next_entry().await? else {
94 break;
95 };
96 let path = entry.path();
97 let file_type = entry.file_type().await?;
98 if file_type.is_dir() {
99 stack.push(path);
100 continue;
101 }
102 if !file_type.is_file() {
103 continue;
104 }
105 let Some(rollout_file) = RolloutFile::from_path(path) else {
106 continue;
107 };
108 let Ok(meta) = crate::read_session_meta_line(rollout_file.path()).await else {
109 continue;
110 };
111 let thread_id = meta.meta.id;
112 if !seen_thread_ids.insert(thread_id) {
113 continue;
114 }
115 if let Some(history_base) = meta.meta.history_base {
116 history_base_by_thread.insert(thread_id, history_base);
117 }
118 }
119 }
120
121 let mut reference_counts_by_thread = HashMap::new();
122 for (thread_id, history_base) in &history_base_by_thread {
123 if history_base.thread_id == *thread_id {
124 continue;
125 }
126 *reference_counts_by_thread
127 .entry(history_base.thread_id)
128 .or_default() += 1;
129 }
130 Ok(Some(Self {
131 history_base_by_thread,
132 reference_counts_by_thread,
133 }))
134 }
135}
136
137#[derive(Clone, Copy)]
138enum ScanDeadline {
139 Unlimited,
140 Until {
141 started_at: Instant,
142 max_runtime: Duration,
143 },
144}
145
146impl ScanDeadline {
147 fn expired(self) -> bool {
148 match self {
149 Self::Unlimited => false,
150 Self::Until {
151 started_at,
152 max_runtime,
153 } => started_at.elapsed() >= max_runtime,
154 }
155 }
156}
157
158#[cfg(test)]
159#[path = "rollout_reference_index_tests.rs"]
160mod tests;