codetether-agent 4.5.7

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
use super::super::{Session, SessionMetadata};
use super::legacy::{extract_cwd_from_env_context, normalize_line};
use super::records::{CodexSessionMetaPayload, CodexTurnContextPayload};
use super::response::parse_response_item;
use super::title::derive_title_from_text;
use super::usage::parse_event_msg_usage;
use crate::provenance::ExecutionProvenance;
use crate::provider::Usage;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};

/// Parse a Codex rollout JSONL file into a native [`Session`].
///
/// Accepts both the modern `{timestamp, type, payload}` envelope format and
/// the legacy flat format produced by older Codex CLI versions. Legacy
/// records are normalized via [`normalize_line`]. When the legacy session
/// meta lacks `cwd`, this function recovers it from the first
/// `<environment_context>` user message.
pub(crate) fn parse_codex_session_from_path(
    path: &Path,
    title_override: Option<&str>,
) -> Result<Session> {
    let file = File::open(path)
        .with_context(|| format!("Failed to open Codex session {}", path.display()))?;
    let mut session_meta: Option<CodexSessionMetaPayload> = None;
    let mut updated_at: Option<DateTime<Utc>> = None;
    let mut messages = Vec::new();
    let mut usage = Usage::default();
    let mut model = None;
    let mut first_user_text = None;

    for line in BufReader::new(file).lines() {
        let line = line.with_context(|| format!("Failed to read {}", path.display()))?;
        if line.trim().is_empty() {
            continue;
        }
        let session_ts = session_meta.as_ref().map(|m| m.timestamp);
        let record = match normalize_line(&line, session_ts)
            .with_context(|| format!("Failed to parse {}", path.display()))?
        {
            Some(r) => r,
            None => continue,
        };
        updated_at =
            Some(updated_at.map_or(record.timestamp, |current| current.max(record.timestamp)));
        match record.kind.as_str() {
            "session_meta" => session_meta = Some(serde_json::from_value(record.payload)?),
            "turn_context" => {
                let ctx: CodexTurnContextPayload = serde_json::from_value(record.payload)?;
                if let Some(next_model) = ctx.model.filter(|value| !value.is_empty()) {
                    model = Some(next_model);
                }
            }
            "response_item" => {
                // Recover cwd from legacy <environment_context> before consuming the payload.
                if let Some(meta) = session_meta.as_mut()
                    && meta.cwd.is_empty()
                    && let Some(cwd) = extract_cwd_from_env_context(&record.payload)
                {
                    meta.cwd = cwd;
                }
                if let Some(message) = parse_response_item(record.payload, &mut first_user_text)? {
                    messages.push(message);
                }
            }
            "event_msg" => {
                if let Some(next_usage) = parse_event_msg_usage(record.payload)? {
                    usage = next_usage;
                }
            }
            _ => {}
        }
    }

    let meta =
        session_meta.with_context(|| format!("Missing session_meta in {}", path.display()))?;
    let title = title_override
        .map(str::to_string)
        .or_else(|| first_user_text.and_then(|text| derive_title_from_text(&text)));
    let id = meta.id;
    Ok(Session {
        id: id.clone(),
        title,
        created_at: meta.timestamp,
        updated_at: updated_at.unwrap_or(meta.timestamp),
        messages,
        tool_uses: Vec::new(),
        usage,
        agent: "build".to_string(),
        metadata: SessionMetadata {
            directory: (!meta.cwd.is_empty()).then(|| PathBuf::from(meta.cwd)),
            model,
            provenance: Some(ExecutionProvenance::for_session(&id, "build")),
            ..Default::default()
        },
        max_steps: None,
        bus: None,
    })
}