use crate::sessions::SessionEventKind;
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Default)]
pub(crate) struct CodeModeAudit {
actions: BTreeMap<u64, (String, Option<bool>)>,
changed_paths: BTreeSet<String>,
changed_paths_incomplete: bool,
warnings: BTreeSet<String>,
pub(crate) contexts: Vec<Value>,
}
impl CodeModeAudit {
pub(crate) fn observe(&mut self, kind: SessionEventKind, payload: &Value) {
let sequence = payload["sequence"].as_u64().unwrap_or_default();
match kind {
SessionEventKind::CodeModeToolCall => {
self.actions.insert(
sequence,
(
payload["name"]
.as_str()
.unwrap_or("unknown")
.chars()
.take(256)
.collect(),
None,
),
);
}
SessionEventKind::CodeModeToolResult => {
if let Some(action) = self.actions.get_mut(&sequence) {
action.1 = payload["result"]["success"].as_bool();
}
if let Some(paths) = payload["changed_paths"]
.as_array()
.or_else(|| payload["execution_effects"]["paths"].as_array())
{
self.changed_paths_incomplete |=
payload["execution_effects"]["changed_path_count"]
.as_u64()
.is_some_and(|count| count > paths.len() as u64);
for path in paths.iter().filter_map(Value::as_str) {
let mut path = crate::output::redact_sensitive_text(path);
self.changed_paths_incomplete |= path.len() > 512;
let mut end = path.len().min(512);
while !path.is_char_boundary(end) {
end -= 1;
}
path.truncate(end);
if self.changed_paths.len() < 1000 {
self.changed_paths.insert(path);
} else if !self.changed_paths.contains(&path) {
self.changed_paths_incomplete = true;
}
}
}
}
SessionEventKind::CodeModeContext => self.contexts.push(payload.clone()),
SessionEventKind::CodeModeWarning => {
if let Some(warning) = payload["warning"].as_str() {
self.warnings.insert(warning.to_owned());
}
}
_ => {}
}
}
pub(crate) fn summary(&self) -> Value {
json!({
"inner_calls": self.actions.len(),
"successful": self.actions.values().filter(|(_, success)| *success == Some(true)).count(),
"recoverable_failures": self.actions.values().filter(|(_, success)| *success == Some(false)).count(),
"unknown_outcomes": self.actions.values().filter(|(_, success)| success.is_none()).count(),
"actions": self.actions.iter().take(100).map(|(sequence, (name, success))| json!({"sequence": sequence,"tool":name,"success":success})).collect::<Vec<_>>(),
"actions_omitted": self.actions.len().saturating_sub(100),
"changed_paths": self.changed_paths.iter().take(50).collect::<Vec<_>>(),
"changed_path_count": self.changed_paths.len(),
"changed_paths_incomplete": self.changed_paths_incomplete,
"warnings": self.warnings,
"effects": "Completed actions remain applied. Missing results have unknown outcomes. No rollback or automatic retry. Shell and external effects may be untracked."
})
}
}