claude_codex/providers/codex/translate/
read_rewrite.rs1use std::collections::{HashMap, VecDeque};
2use std::sync::Mutex;
3
4use once_cell::sync::Lazy;
5use serde_json::Value;
6
7const MAX_REWRITE_NOTES: usize = 4_096;
8const READ_OFFSET_REWRITE_THRESHOLD: i64 = 1_000_000;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ReadOffsetRewrite {
12 pub offset: i64,
13 pub file_path: Option<String>,
14}
15
16#[derive(Debug, Default)]
17struct RewriteStore {
18 order: VecDeque<String>,
19 entries: HashMap<String, ReadOffsetRewrite>,
20}
21
22static READ_OFFSET_REWRITES: Lazy<Mutex<RewriteStore>> =
23 Lazy::new(|| Mutex::new(RewriteStore::default()));
24
25pub fn sanitize_read_args(name: &str, args: &str, call_id: Option<&str>) -> String {
26 if name != "Read" || args.is_empty() {
27 return args.to_string();
28 }
29
30 let parsed: Value = match serde_json::from_str(args) {
31 Ok(v) => v,
32 Err(_) => return args.to_string(),
33 };
34 let obj = match parsed.as_object() {
35 Some(o) => o,
36 None => return args.to_string(),
37 };
38
39 let mut sanitized = obj.clone();
40 let mut changed = false;
41
42 let has_empty_pages = obj
43 .get("pages")
44 .and_then(|v| v.as_str())
45 .is_some_and(|s| s.is_empty());
46 if has_empty_pages {
47 sanitized.remove("pages");
48 changed = true;
49 }
50
51 if let Some(offset) = obj.get("offset").and_then(|v| v.as_i64())
52 && offset >= READ_OFFSET_REWRITE_THRESHOLD
53 {
54 sanitized.remove("offset");
55 changed = true;
56 if let Some(call_id) = call_id.filter(|id| !id.is_empty()) {
57 record_read_offset_rewrite(
58 call_id,
59 ReadOffsetRewrite {
60 offset,
61 file_path: obj
62 .get("file_path")
63 .and_then(|v| v.as_str())
64 .map(str::to_string),
65 },
66 );
67 }
68 }
69
70 if changed {
71 serde_json::to_string(&sanitized).unwrap_or_else(|_| args.to_string())
72 } else {
73 args.to_string()
74 }
75}
76
77pub fn read_offset_rewrite(call_id: &str) -> Option<ReadOffsetRewrite> {
78 READ_OFFSET_REWRITES
79 .lock()
80 .ok()
81 .and_then(|store| store.entries.get(call_id).cloned())
82}
83
84fn record_read_offset_rewrite(call_id: &str, note: ReadOffsetRewrite) {
85 let Ok(mut store) = READ_OFFSET_REWRITES.lock() else {
86 return;
87 };
88
89 if !store.entries.contains_key(call_id) {
90 store.order.push_back(call_id.to_string());
91 }
92 store.entries.insert(call_id.to_string(), note);
93
94 while store.entries.len() > MAX_REWRITE_NOTES {
95 let Some(oldest) = store.order.pop_front() else {
96 break;
97 };
98 store.entries.remove(&oldest);
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn sanitize_read_args_removes_empty_pages() {
108 let args = r#"{"file_path":"/tmp/a","pages":""}"#;
109 let sanitized = sanitize_read_args("Read", args, None);
110 let parsed: Value = serde_json::from_str(&sanitized).unwrap();
111 assert!(parsed.get("pages").is_none());
112 assert_eq!(
113 parsed.get("file_path").and_then(|v| v.as_str()),
114 Some("/tmp/a")
115 );
116 }
117
118 #[test]
119 fn sanitize_read_args_drops_and_records_absurd_offset() {
120 let args = r#"{"file_path":"/tmp/a","offset":1300000,"limit":20}"#;
121 let sanitized = sanitize_read_args("Read", args, Some("call_rewrite_test"));
122 let parsed: Value = serde_json::from_str(&sanitized).unwrap();
123 assert!(parsed.get("offset").is_none());
124 assert_eq!(parsed.get("limit").and_then(|v| v.as_i64()), Some(20));
125
126 let note = read_offset_rewrite("call_rewrite_test").unwrap();
127 assert_eq!(note.offset, 1_300_000);
128 assert_eq!(note.file_path.as_deref(), Some("/tmp/a"));
129 }
130
131 #[test]
132 fn sanitize_read_args_keeps_normal_offset() {
133 let args = r#"{"file_path":"/tmp/a","offset":1300,"limit":20}"#;
134 let sanitized = sanitize_read_args("Read", args, Some("call_keep_test"));
135 let parsed: Value = serde_json::from_str(&sanitized).unwrap();
136 assert_eq!(parsed.get("offset").and_then(|v| v.as_i64()), Some(1_300));
137 assert!(read_offset_rewrite("call_keep_test").is_none());
138 }
139}