magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
//! Presentation-independent chat reconciliation; never provider replay or raw event export.
use super::SessionEvent;
#[cfg(unix)]
use super::{SessionEventKind, TurnStatus};
#[cfg(unix)]
use crate::output::sanitize_display_controls;

#[cfg(unix)]
pub(crate) const CHAT_MAX_BYTES: usize = 8 * 1024 * 1024;
#[cfg(unix)]
pub(crate) const CHAT_MAX_ENTRIES: usize = 10_000;

#[cfg(unix)]
#[derive(Debug)]
pub(crate) struct ChatMessage {
    pub(crate) kind: &'static str,
    pub(crate) text: String,
    pub(crate) partial: bool,
}

/// Some older providers recorded a cumulative output after tool-separated chunks.
pub(crate) fn reconciled_assistant_output(output: &str, prior: &str, current: &str) -> String {
    match output.strip_prefix(prior) {
        Some(suffix) if !prior.is_empty() && suffix.starts_with(current) => suffix.to_owned(),
        _ => output.to_owned(),
    }
}

pub(crate) fn tool_call(event: &SessionEvent) -> Option<crate::providers::ToolCall> {
    Some(crate::providers::ToolCall {
        id: event.payload.get("id")?.as_str()?.to_owned(),
        name: event.payload.get("name")?.as_str()?.to_owned(),
        arguments: event
            .payload
            .get("arguments")
            .cloned()
            .unwrap_or_else(|| serde_json::json!({})),
    })
}

pub(crate) fn tool_result(
    event: &SessionEvent,
    calls: &std::collections::HashMap<String, crate::providers::ToolCall>,
) -> Result<(crate::providers::ToolCall, crate::tools::ToolResult), String> {
    let value = event.payload.get("result").ok_or_else(|| {
        "session replay warning: tool_result event missing result payload".to_owned()
    })?;
    let result: crate::tools::ToolResult = serde_json::from_value(value.clone()).map_err(|_| {
        "session replay warning: tool_result event has malformed result payload".to_owned()
    })?;
    let id = event
        .payload
        .get("call_id")
        .and_then(serde_json::Value::as_str)
        .unwrap_or_default();
    let call = calls
        .get(id)
        .cloned()
        .unwrap_or_else(|| crate::providers::ToolCall {
            id: id.to_owned(),
            name: result.tool_name.clone(),
            arguments: serde_json::json!({}),
        });
    Ok((call, result))
}

#[cfg(unix)]
#[derive(Default)]
struct Projection {
    messages: Vec<ChatMessage>,
    bytes: usize,
    current: Option<usize>,
    prior: String,
    turn_start: usize,
    has_output: bool,
    terminal: bool,
    incomplete: bool,
    turn_partial: bool,
    calls: std::collections::HashMap<String, crate::providers::ToolCall>,
    tool_entries: std::collections::HashMap<String, usize>,
    reasoning_entries: std::collections::HashMap<(String, String), usize>,
}

#[cfg(unix)]
impl Projection {
    fn push(
        &mut self,
        kind: &'static str,
        text: &str,
        partial: bool,
    ) -> Result<usize, &'static str> {
        let text = sanitize_display_controls(text);
        self.bytes = self.bytes.saturating_add(text.len());
        if self.bytes > CHAT_MAX_BYTES || self.messages.len() == CHAT_MAX_ENTRIES {
            return Err("history_limit");
        }
        let index = self.messages.len();
        self.messages.push(ChatMessage {
            kind,
            text,
            partial,
        });
        Ok(index)
    }

    fn replace(&mut self, index: usize, text: &str, partial: bool) -> Result<(), &'static str> {
        let message = &mut self.messages[index];
        let text = sanitize_display_controls(text);
        self.bytes = self
            .bytes
            .saturating_sub(message.text.len())
            .saturating_add(text.len());
        if self.bytes > CHAT_MAX_BYTES {
            return Err("history_limit");
        }
        message.text = text;
        message.partial = partial;
        Ok(())
    }

    fn assistant(&mut self, text: &str, replace: bool) -> Result<(), &'static str> {
        if text.is_empty() {
            return Ok(());
        }
        if let Some(index) = self.current {
            let message = &mut self.messages[index];
            let text = sanitize_display_controls(text);
            let added_bytes = text.len();
            if replace {
                self.bytes = self.bytes.saturating_sub(message.text.len());
                message.text = text;
            } else {
                message.text.push_str(&text);
            }
            self.bytes = self.bytes.saturating_add(added_bytes);
            if self.bytes > CHAT_MAX_BYTES {
                return Err("history_limit");
            }
        } else {
            self.current = Some(self.push("assistant", text, true)?);
        }
        Ok(())
    }

    fn boundary(&mut self) {
        if let Some(index) = self.current.take() {
            self.prior.push_str(&self.messages[index].text);
        }
    }

    fn finish_turn(&mut self) -> Result<(), &'static str> {
        if !self.terminal
            && (!self.calls.is_empty()
                || (!self.has_output
                    && self.messages[self.turn_start..]
                        .iter()
                        .any(|m| m.kind == "user" || m.kind == "assistant")))
        {
            self.incomplete = true;
            self.turn_partial = true;
            self.push(
                "diagnostic",
                "Interrupted turn; no durable completion recorded",
                true,
            )?;
        }
        if self.terminal || self.has_output {
            // An explicit failed/cancelled status already keeps its partial flags.
            if !self.turn_partial {
                for message in &mut self.messages[self.turn_start..] {
                    if message.kind == "assistant" {
                        message.partial = false;
                    }
                }
            }
        }
        self.current = None;
        self.prior.clear();
        self.calls.clear();
        self.tool_entries.clear();
        self.reasoning_entries.clear();
        self.turn_start = self.messages.len();
        self.has_output = false;
        self.terminal = false;
        self.turn_partial = false;
        Ok(())
    }
}

/// Bounded chat projection follows hydration's segment/output reconciliation rules.
#[cfg(unix)]
pub(crate) fn project(events: &[SessionEvent]) -> Result<(Vec<ChatMessage>, bool), &'static str> {
    let mut projection = Projection::default();
    for event in events {
        let text = |key| {
            event
                .payload
                .get(key)
                .and_then(serde_json::Value::as_str)
                .unwrap_or_default()
        };
        let required_text = |key| {
            event
                .payload
                .get(key)
                .and_then(serde_json::Value::as_str)
                .ok_or("session_invalid")
        };
        match event.kind() {
            Some(SessionEventKind::UserInput | SessionEventKind::Compaction) => {
                projection.finish_turn()?;
                if event.kind() == Some(SessionEventKind::Compaction) {
                    projection.push("compaction", required_text("summary")?, false)?;
                } else if !matches!(
                    text("origin"),
                    "automatic_compaction" | "automatic_continuation" | "provider_only"
                ) {
                    projection.push("user", required_text("text")?, false)?;
                }
            }
            Some(SessionEventKind::AssistantChunk) => {
                projection.assistant(required_text("text")?, false)?
            }
            Some(SessionEventKind::AssistantOutput) => {
                let current = projection
                    .current
                    .map(|i| projection.messages[i].text.as_str())
                    .unwrap_or_default();
                let output = if event.payload.get("text").is_some() {
                    required_text("text")?
                } else {
                    current
                };
                let output = reconciled_assistant_output(
                    &sanitize_display_controls(output),
                    &projection.prior,
                    current,
                );
                projection.assistant(&output, true)?;
                projection.has_output = true;
                projection.prior.clear();
            }
            Some(SessionEventKind::ReasoningSummary) => {
                let summary = required_text("text")?;
                if event
                    .payload
                    .get("provider_summary")
                    .and_then(serde_json::Value::as_bool)
                    != Some(true)
                {
                    continue;
                }
                projection.boundary();
                if let Some(item_id) = event
                    .payload
                    .get("item_id")
                    .and_then(serde_json::Value::as_str)
                {
                    let identity = (text("turn_id").to_owned(), item_id.to_owned());
                    if let Some(&index) = projection.reasoning_entries.get(&identity) {
                        projection.replace(index, summary, false)?;
                    } else {
                        let index = projection.push("reasoning_summary", summary, false)?;
                        projection.reasoning_entries.insert(identity, index);
                    }
                } else {
                    projection.push("reasoning_summary", summary, false)?;
                }
            }
            Some(SessionEventKind::ToolCall | SessionEventKind::CodeModeToolCall) => {
                projection.boundary();
                let call = tool_call(event).ok_or("session_invalid")?;
                let index = projection.push(
                    "activity",
                    &format!("{}: started", crate::output::tool_display_label(&call)),
                    true,
                )?;
                projection.tool_entries.insert(call.id.clone(), index);
                projection.calls.insert(call.id.clone(), call);
            }
            Some(
                SessionEventKind::ToolResult
                | SessionEventKind::ToolDisplayResult
                | SessionEventKind::CodeModeToolResult,
            ) => {
                projection.boundary();
                let (call, result) =
                    tool_result(event, &projection.calls).map_err(|_| "session_invalid")?;
                projection.calls.remove(&call.id);
                let summary = crate::output::tool_display_summary(&call, &result);
                let text = format!(
                    "{}: {}",
                    summary.label,
                    if result.success {
                        "completed"
                    } else {
                        "failed"
                    }
                );
                if let Some(index) = projection.tool_entries.remove(&call.id) {
                    projection.replace(index, &text, false)?;
                } else {
                    projection.push("activity", &text, false)?;
                }
            }
            Some(SessionEventKind::TurnStatus) => {
                let status = event.turn_status_payload().ok_or("session_invalid")?;
                let partial = status.status != TurnStatus::Complete;
                projection.turn_partial = partial;
                if !projection.messages[projection.turn_start..]
                    .iter()
                    .any(|m| m.kind == "assistant")
                    && let Some(text) = status.assistant_text.as_deref()
                {
                    projection.assistant(text, true)?;
                }
                for message in &mut projection.messages[projection.turn_start..] {
                    if message.kind == "assistant" {
                        message.partial = partial;
                    }
                }
                if partial {
                    projection.push("diagnostic", &format!("Turn {:?}", status.status), true)?;
                    if let Some(error) = status.error_summary {
                        projection.push("diagnostic", &error, true)?;
                    }
                }
                projection.terminal = true;
                projection.calls.clear();
            }
            Some(
                SessionEventKind::Diagnostic
                | SessionEventKind::HookDiagnostic
                | SessionEventKind::CodeModeWarning,
            ) => {
                let key = if event.kind() == Some(SessionEventKind::CodeModeWarning) {
                    "warning"
                } else {
                    "message"
                };
                // Structured private assessments have no presentation message.
                if event.payload.get(key).is_some() {
                    projection.boundary();
                    projection.push("diagnostic", required_text(key)?, false)?;
                }
            }
            _ => {}
        }
    }
    projection.finish_turn()?;
    let complete = !projection.incomplete;
    Ok((projection.messages, complete))
}