1use crate::session::{SessionState, ReadRecord, EditRecord, ErrorRecord};
2use rustc_hash::FxHashMap;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5
6pub fn parse_session_file(path: &str) -> Result<SessionState, String> {
7 let file = File::open(path).map_err(|e| format!("Cannot open {}: {}", path, e))?;
8 let reader = BufReader::new(file);
9 let mut state = SessionState::default();
10 let mut tool_calls: FxHashMap<String, (String, serde_json::Value)> = FxHashMap::default();
11 let mut edit_counter: FxHashMap<String, usize> = FxHashMap::default();
12 let mut read_counter: FxHashMap<String, usize> = FxHashMap::default();
13 let mut edit_files: FxHashMap<String, String> = FxHashMap::default();
15
16 for line in reader.lines() {
17 let line = line.map_err(|e| format!("Read error: {}", e))?;
18 let v: serde_json::Value = match serde_json::from_str(&line) { Ok(v) => v, Err(_) => continue };
19 let typ = match v.get("type").and_then(|t| t.as_str()) { Some(t) => t, None => continue };
20 if typ == "message" {
21 let msg = match v.get("message") { Some(m) => m, None => continue };
22 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
23
24 if role == "assistant" {
26 if let Some(content) = msg.get("content").and_then(|c| c.as_array()) {
27 for item in content {
28 if item.get("type").and_then(|t| t.as_str()) == Some("toolCall") {
29 let tool_id = item.get("id").and_then(|s| s.as_str()).unwrap_or("").to_string();
30 let tool_name = item.get("name").and_then(|s| s.as_str()).unwrap_or("").to_string();
31 let args = item.get("arguments").cloned().unwrap_or(serde_json::Value::Null);
32 tool_calls.insert(tool_id.clone(), (tool_name.clone(), args.clone()));
33 if tool_name == "edit" {
35 if let Some(obj) = args.as_object() {
36 if let Some(file) = obj.get("file").and_then(|f| f.as_str()) {
37 edit_files.insert(tool_id.clone(), file.to_string());
38 }
39 }
40 }
41 }
42 }
43 }
44 }
45
46 if role == "user" {
47 state.turn_count += 1;
49 }
50
51 if role == "toolResult" {
52 let call_id = msg.get("toolCallId").and_then(|s| s.as_str()).unwrap_or("");
53 let tool_name = msg.get("toolName").and_then(|s| s.as_str()).unwrap_or("");
54
55 let has_content = msg.get("content").and_then(|c| c.as_array()).map(|a| !a.is_empty()).unwrap_or(false);
56
57 if let Some((_name, args)) = tool_calls.remove(call_id) {
59 match tool_name {
60 "read" => {
61 let path = args.get("path").and_then(|p| p.as_str()).unwrap_or("").to_string();
62 if !path.is_empty() && has_content {
63 *read_counter.entry(path.clone()).or_insert(0) += 1;
64 let is_rerun = read_counter.get(&path).copied().unwrap_or(0) > 1;
65 let content = msg.get("content").and_then(|c| c.as_array());
66 let total_size: usize = content.map(|arr| {
67 arr.iter().filter_map(|b| b.get("text").and_then(|t| t.as_str()).map(|s| s.len())).sum()
68 }).unwrap_or(0);
69 state.reads.push(ReadRecord { path, size: total_size, hash: format!("{:x}", total_size), is_rerun });
70 }
71 }
72 "bash" => {
73 let cmd = args.get("command").and_then(|c| c.as_str()).unwrap_or("");
74 if cmd.contains("cargo test") || cmd.contains("pytest") {
75 if let Some(content) = msg.get("content").and_then(|c| c.as_array()) {
76 for block in content {
77 if block.get("type").and_then(|t| t.as_str()) == Some("text") {
78 let text = block.get("text").and_then(|t| t.as_str()).unwrap_or("");
79 let is_fail = text.contains("FAILED") || text.contains("failures:");
81 let is_pass = !is_fail && text.contains("test result:");
82 if is_fail || is_pass {
83 state.last_test_output = Some(text.chars().take(200).collect());
84 state.last_test_pass = is_pass;
85 }
86 if is_fail {
87 state.errors.push(ErrorRecord {
88 turn: state.turn_count,
89 summary: extract_error_summary(text),
90 });
91 }
92 }
93 }
94 }
95 }
96 }
97 "edit" => {
98 let file = edit_files.remove(call_id).unwrap_or_default();
99 if let Some(edits_arr) = args.get("edits").and_then(|e| e.as_array()) {
100 for edit in edits_arr {
101 let old = edit.get("oldText").and_then(|e| e.as_str()).unwrap_or("").to_string();
102 let new = edit.get("newText").and_then(|e| e.as_str()).unwrap_or("").to_string();
103 let cnt = edit_counter.entry(file.clone()).or_insert(0);
104 *cnt += 1;
105 state.edits.push(EditRecord {
106 file: file.clone(),
107 line: old.lines().next().unwrap_or("").to_string(),
108 attempt: *cnt,
109 old_snippet: old.chars().take(40).collect(),
110 new_snippet: new.chars().take(40).collect(),
111 success: false,
112 });
113 }
114 }
115 }
116 _ => {}
117 }
118 }
119 }
120 }
121 }
122
123 if state.last_test_pass {
125 if let Some(last) = state.edits.last_mut() {
126 last.success = true;
127 }
128 }
129
130 Ok(state)
131}
132
133fn extract_error_summary(output: &str) -> String {
134 for line in output.lines() {
135 let t = line.trim();
136 if t.contains("FAILED") || t.contains("panicked at") || t.contains("assertion") {
137 return t.chars().take(120).collect();
138 }
139 if t.contains("expected `true`, got `false`") {
140 return t.chars().take(120).collect();
141 }
142 }
143 for line in output.lines() {
145 if line.contains("failure") {
146 return line.chars().take(120).collect();
147 }
148 }
149 output.lines().next().unwrap_or("").chars().take(120).collect()
150}