use crate::Result;
use agtrace_types::SpawnContext;
use std::io::{BufRead, BufReader};
use std::path::Path;
use super::parser::normalize_codex_session;
use super::schema::{CodexRecord, EventMsgPayload};
pub fn normalize_codex_file(path: &Path) -> Result<Vec<agtrace_types::AgentEvent>> {
let text = std::fs::read_to_string(path)?;
let mut records: Vec<CodexRecord> = Vec::new();
let mut session_id_from_meta: Option<String> = None;
let mut subagent_type: Option<String> = None;
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let record: CodexRecord = serde_json::from_str(line)?;
if let CodexRecord::SessionMeta(ref meta) = record {
session_id_from_meta = Some(meta.payload.id.clone());
if let super::schema::SessionSource::Subagent { subagent } = &meta.payload.source {
subagent_type = Some(subagent.clone());
}
}
records.push(record);
}
let session_id = session_id_from_meta.unwrap_or_else(|| "unknown-session".to_string());
Ok(normalize_codex_session(records, &session_id, subagent_type))
}
pub fn extract_cwd_from_codex_file(path: &Path) -> Option<String> {
let file = std::fs::File::open(path).ok()?;
let reader = BufReader::new(file);
for line in reader.lines().take(10).flatten() {
if let Ok(record) = serde_json::from_str::<CodexRecord>(&line) {
match record {
CodexRecord::SessionMeta(meta) => {
return Some(meta.payload.cwd.clone());
}
CodexRecord::TurnContext(turn) => {
return Some(turn.payload.cwd.clone());
}
_ => continue,
}
}
}
None
}
#[derive(Debug, Clone)]
pub struct SpawnEvent {
pub timestamp: String,
pub subagent_type: String,
pub spawn_context: SpawnContext,
}
#[derive(Debug)]
pub struct CodexHeader {
pub session_id: Option<String>,
pub cwd: Option<String>,
pub timestamp: Option<String>,
pub snippet: Option<String>,
pub subagent_type: Option<String>,
pub parent_session_id: Option<String>,
pub spawned_by: Option<SpawnContext>,
}
pub fn extract_codex_header(path: &Path) -> Result<CodexHeader> {
let file = std::fs::File::open(path)?;
let reader = BufReader::new(file);
let mut session_id = None;
let mut cwd = None;
let mut timestamp = None;
let mut snippet = None;
let mut subagent_type = None;
let parent_session_id = None;
for line in reader.lines().take(20).flatten() {
if let Ok(record) = serde_json::from_str::<CodexRecord>(&line) {
match &record {
CodexRecord::SessionMeta(meta) => {
if session_id.is_none() {
session_id = Some(meta.payload.id.clone());
}
if cwd.is_none() {
cwd = Some(meta.payload.cwd.clone());
}
if timestamp.is_none() {
timestamp = Some(meta.timestamp.clone());
}
if subagent_type.is_none()
&& let super::schema::SessionSource::Subagent { subagent } =
&meta.payload.source
{
subagent_type = Some(subagent.clone());
}
}
CodexRecord::TurnContext(turn) => {
if cwd.is_none() {
cwd = Some(turn.payload.cwd.clone());
}
if timestamp.is_none() {
timestamp = Some(turn.timestamp.clone());
}
}
CodexRecord::EventMsg(event) => {
if timestamp.is_none() {
timestamp = Some(event.timestamp.clone());
}
if snippet.is_none()
&& let super::schema::EventMsgPayload::UserMessage(msg) = &event.payload
{
snippet = Some(agtrace_types::truncate(&msg.message, 200));
}
}
CodexRecord::ResponseItem(response) => {
if timestamp.is_none() {
timestamp = Some(response.timestamp.clone());
}
if snippet.is_none()
&& let super::schema::ResponseItemPayload::Message(msg) = &response.payload
&& msg.role == "user"
{
let text = msg.content.iter().find_map(|c| match c {
super::schema::MessageContent::InputText { text } => {
Some(agtrace_types::truncate(text, 200))
}
super::schema::MessageContent::OutputText { text } => {
Some(agtrace_types::truncate(text, 200))
}
_ => None,
});
if let Some(t) = &text
&& !t.contains("<environment_context>")
{
snippet = text;
}
}
}
_ => {}
}
if session_id.is_some() && cwd.is_some() && timestamp.is_some() && snippet.is_some() {
break;
}
}
}
Ok(CodexHeader {
session_id,
cwd,
timestamp,
snippet,
subagent_type,
parent_session_id,
spawned_by: None, })
}
pub fn extract_spawn_events(path: &Path) -> Result<Vec<SpawnEvent>> {
let text = std::fs::read_to_string(path)?;
let mut spawn_events = Vec::new();
let mut current_turn: usize = 0;
let mut current_step: usize = 0;
let mut in_turn = false;
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let record: CodexRecord = match serde_json::from_str(line) {
Ok(r) => r,
Err(_) => continue,
};
match &record {
CodexRecord::TurnContext(_) => {
if in_turn {
current_turn += 1;
}
current_step = 0;
in_turn = true;
}
CodexRecord::EventMsg(event) => {
match &event.payload {
EventMsgPayload::UserMessage(_) => {
if in_turn {
current_turn += 1;
current_step = 0;
}
in_turn = true;
}
EventMsgPayload::EnteredReviewMode(_) => {
spawn_events.push(SpawnEvent {
timestamp: event.timestamp.clone(),
subagent_type: "review".to_string(),
spawn_context: SpawnContext {
turn_index: current_turn,
step_index: current_step,
},
});
current_step += 1;
}
_ => {
if in_turn {
current_step += 1;
}
}
}
}
CodexRecord::ResponseItem(_) if in_turn => {
current_step += 1;
}
_ => {}
}
}
Ok(spawn_events)
}
pub fn is_empty_codex_session(path: &Path) -> bool {
let Ok(file) = std::fs::File::open(path) else {
return true;
};
let reader = BufReader::new(file);
let mut line_count = 0;
let mut has_event = false;
for line in reader.lines().take(20).flatten() {
line_count += 1;
if let Ok(record) = serde_json::from_str::<CodexRecord>(&line) {
match record {
CodexRecord::SessionMeta(_) | CodexRecord::TurnContext(_) => {
has_event = true;
break;
}
CodexRecord::EventMsg(_) | CodexRecord::ResponseItem(_) => {
has_event = true;
break;
}
_ => {}
}
}
}
line_count <= 2 && !has_event
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_subagent_header() {
let tmpfile = std::env::temp_dir().join("test_subagent.jsonl");
std::fs::write(&tmpfile, r#"{"timestamp":"2025-01-01T00:00:00Z","type":"session_meta","payload":{"id":"test-id","timestamp":"2025-01-01T00:00:00Z","cwd":"/test","originator":"test","cli_version":"1.0.0","source":{"subagent":"review"}}}
"#).unwrap();
let header = extract_codex_header(&tmpfile).unwrap();
assert_eq!(header.session_id, Some("test-id".to_string()));
assert_eq!(header.subagent_type, Some("review".to_string()));
assert!(header.parent_session_id.is_none());
std::fs::remove_file(&tmpfile).unwrap();
}
}