magi-code 0.63.4

Repository-aware CLI coding agent for terminal work
Documentation
use super::resolve_session_scope_path;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SessionEventKind {
    UserInput,
    AssistantChunk,
    AssistantOutput,
    ProviderResponseItem,
    ReasoningSummary,
    ToolCall,
    ToolResult,
    ProviderContextItem,
    HookDiagnostic,
    HookLifecycle,
    HookContextInjection,
    TurnStatus,
    AbortRecovery,
    ProviderStreamTrace,
    ContextCache,
    SessionTitle,
    Compaction,
    Diagnostic,
    Rewind,
    SubdirInstructionLoad,
    TtsrInjection,
}

impl SessionEventKind {
    pub(crate) const fn as_str(self) -> &'static str {
        match self {
            Self::UserInput => "user_input",
            Self::AssistantChunk => "assistant_chunk",
            Self::AssistantOutput => "assistant_output",
            Self::ProviderResponseItem => "provider_response_item",
            Self::ReasoningSummary => "reasoning_summary",
            Self::ToolCall => "tool_call",
            Self::ToolResult => "tool_result",
            Self::ProviderContextItem => "provider_context_item",
            Self::HookDiagnostic => "hook_diagnostic",
            Self::HookLifecycle => "hook_lifecycle",
            Self::HookContextInjection => "hook_context_injection",
            Self::TurnStatus => "turn_status",
            Self::AbortRecovery => "abort_recovery",
            Self::ProviderStreamTrace => "provider_stream_trace",
            Self::ContextCache => "context_cache",
            Self::SessionTitle => "session_title",
            Self::Compaction => "compaction",
            Self::Diagnostic => "diagnostic",
            Self::Rewind => "rewind",
            Self::SubdirInstructionLoad => "subdir_instruction_load",
            Self::TtsrInjection => "ttsr_injection",
        }
    }

    fn parse(value: &str) -> Option<Self> {
        match value {
            "user_input" => Some(Self::UserInput),
            "assistant_chunk" => Some(Self::AssistantChunk),
            "assistant_output" => Some(Self::AssistantOutput),
            "provider_response_item" => Some(Self::ProviderResponseItem),
            "reasoning_summary" => Some(Self::ReasoningSummary),
            "tool_call" => Some(Self::ToolCall),
            "tool_result" => Some(Self::ToolResult),
            "provider_context_item" => Some(Self::ProviderContextItem),
            "hook_diagnostic" => Some(Self::HookDiagnostic),
            "hook_lifecycle" => Some(Self::HookLifecycle),
            "hook_context_injection" => Some(Self::HookContextInjection),
            "turn_status" => Some(Self::TurnStatus),
            "abort_recovery" => Some(Self::AbortRecovery),
            "provider_stream_trace" => Some(Self::ProviderStreamTrace),
            "context_cache" => Some(Self::ContextCache),
            "session_title" => Some(Self::SessionTitle),
            "compaction" => Some(Self::Compaction),
            "diagnostic" => Some(Self::Diagnostic),
            "rewind" => Some(Self::Rewind),
            "subdir_instruction_load" => Some(Self::SubdirInstructionLoad),
            "ttsr_injection" => Some(Self::TtsrInjection),
            _ => None,
        }
    }

    pub(crate) const fn is_local_only(self) -> bool {
        matches!(
            self,
            Self::HookDiagnostic
                | Self::HookLifecycle
                | Self::HookContextInjection
                | Self::TurnStatus
                | Self::AbortRecovery
                | Self::ProviderStreamTrace
                | Self::ContextCache
                | Self::SessionTitle
                | Self::Diagnostic
                | Self::Rewind
                | Self::SubdirInstructionLoad
                | Self::TtsrInjection
        )
    }
}

impl AsRef<str> for SessionEventKind {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionEvent {
    pub event_type: String,
    pub timestamp: DateTime<Utc>,
    pub session_id: String,
    pub cwd: PathBuf,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub session_path: Option<PathBuf>,
    pub payload: Value,
}

impl SessionEvent {
    pub fn new(
        event_type: impl Into<String>,
        session_id: String,
        cwd: PathBuf,
        payload: Value,
    ) -> Self {
        Self {
            event_type: event_type.into(),
            timestamp: Utc::now(),
            session_id,
            cwd: cwd.clone(),
            session_path: Some(resolve_session_scope_path(&cwd)),
            payload,
        }
    }

    pub(crate) fn new_kind(
        kind: SessionEventKind,
        session_id: String,
        cwd: PathBuf,
        payload: Value,
    ) -> Self {
        Self::new(kind.as_str(), session_id, cwd, payload)
    }

    pub(crate) fn kind(&self) -> Option<SessionEventKind> {
        SessionEventKind::parse(&self.event_type)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sessions::SessionManager;
    use serde_json::json;
    use tempfile::TempDir;

    #[test]
    fn ttsr_injection_event_round_trips_local_only() {
        assert_eq!(SessionEventKind::TtsrInjection.as_str(), "ttsr_injection");
        assert_eq!(
            SessionEventKind::parse("ttsr_injection"),
            Some(SessionEventKind::TtsrInjection)
        );
        assert!(SessionEventKind::TtsrInjection.is_local_only());
        let temp = TempDir::new().unwrap();
        let event = SessionEvent::new_kind(
            SessionEventKind::TtsrInjection,
            "session".to_string(),
            temp.path().to_path_buf(),
            json!({
                "schema_version": 1,
                "turn_index": 3,
                "request_sequence": 2,
                "rule_source": "builtin",
                "rule_pattern": "danger",
                "matched_text_redacted": "[REDACTED]",
                "reminder": "stop",
                "aborted_turn": 3
            }),
        );
        let parsed: SessionEvent =
            serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
        assert_eq!(parsed.kind(), Some(SessionEventKind::TtsrInjection));
        assert_eq!(parsed.payload["reminder"], "stop");
    }

    #[test]
    fn rewind_event_round_trips_without_bytes() {
        let temp = TempDir::new().unwrap();
        let event = SessionEvent::new_kind(
            SessionEventKind::Rewind,
            "session".to_string(),
            temp.path().to_path_buf(),
            json!({
                "target_turn": 2,
                "latest_turn": 4,
                "paths": [{"path":"src/lib.rs", "status":"restored"}],
                "counts": {"Restored": 1}
            }),
        );

        let serialized = serde_json::to_string(&event).unwrap();
        assert!(!serialized.contains("old bytes"));
        assert!(!serialized.contains("new bytes"));
        let parsed: SessionEvent = serde_json::from_str(&serialized).unwrap();
        assert_eq!(parsed.kind(), Some(SessionEventKind::Rewind));
        assert_eq!(parsed.payload["target_turn"], 2);
    }

    #[test]
    fn session_event_kind_preserves_jsonl_event_type_strings() {
        let temp = TempDir::new().unwrap();
        let manager = SessionManager::new(temp.path().join("sessions"));
        let session = manager.create().unwrap();
        let event = SessionEvent::new_kind(
            SessionEventKind::UserInput,
            session.id().to_string(),
            temp.path().to_path_buf(),
            json!({"text":"hello"}),
        );

        let serialized = serde_json::to_value(&event).unwrap();
        assert_eq!(
            serialized["event_type"],
            SessionEventKind::UserInput.as_str()
        );
        assert!(serialized.get("kind").is_none());
        assert_eq!(event.kind(), Some(SessionEventKind::UserInput));
        assert_eq!(event.session_path.as_deref(), Some(temp.path()));
    }

    #[test]
    fn session_event_serializes_session_path_and_accepts_legacy_missing_field() {
        let temp = TempDir::new().unwrap();
        let event = SessionEvent::new(
            "user_input",
            "safe".to_string(),
            temp.path().to_path_buf(),
            json!({"text":"hello"}),
        );

        let serialized = serde_json::to_value(&event).unwrap();
        assert_eq!(serialized["session_path"].as_str(), temp.path().to_str());

        let legacy = json!({
            "event_type": "user_input",
            "timestamp": event.timestamp,
            "session_id": "legacy-session",
            "cwd": temp.path(),
            "payload": {"text":"old"}
        });
        let parsed: SessionEvent = serde_json::from_value(legacy).unwrap();
        assert_eq!(parsed.session_path, None);
    }

    #[test]
    fn hook_lifecycle_event_kind_is_local_only() {
        assert_eq!(SessionEventKind::HookLifecycle.as_str(), "hook_lifecycle");
        assert_eq!(
            SessionEventKind::parse("hook_lifecycle"),
            Some(SessionEventKind::HookLifecycle)
        );
        assert!(SessionEventKind::HookLifecycle.is_local_only());
        assert_eq!(
            SessionEventKind::HookContextInjection.as_str(),
            "hook_context_injection"
        );
        assert_eq!(
            SessionEventKind::parse("hook_context_injection"),
            Some(SessionEventKind::HookContextInjection)
        );
        assert!(SessionEventKind::HookContextInjection.is_local_only());
        assert_eq!(
            SessionEventKind::ProviderContextItem.as_str(),
            "provider_context_item"
        );
        assert_eq!(SessionEventKind::AbortRecovery.as_str(), "abort_recovery");
        assert_eq!(
            SessionEventKind::parse("abort_recovery"),
            Some(SessionEventKind::AbortRecovery)
        );
        assert!(SessionEventKind::AbortRecovery.is_local_only());
        assert_eq!(
            SessionEventKind::ProviderStreamTrace.as_str(),
            "provider_stream_trace"
        );
        assert_eq!(
            SessionEventKind::parse("provider_stream_trace"),
            Some(SessionEventKind::ProviderStreamTrace)
        );
        assert!(SessionEventKind::ProviderStreamTrace.is_local_only());
        assert!(!SessionEventKind::ProviderContextItem.is_local_only());
        assert!(!SessionEventKind::ReasoningSummary.is_local_only());
        assert!(!SessionEventKind::ToolResult.is_local_only());
        assert_eq!(
            SessionEvent::new(
                "future_event",
                "safe".to_string(),
                PathBuf::new(),
                json!({})
            )
            .kind(),
            None
        );
    }

    #[test]
    fn provider_stream_trace_event_kind_is_local_only() {
        assert_eq!(
            SessionEventKind::ProviderStreamTrace.as_str(),
            "provider_stream_trace"
        );
        assert_eq!(
            SessionEventKind::parse("provider_stream_trace"),
            Some(SessionEventKind::ProviderStreamTrace)
        );
        assert!(SessionEventKind::ProviderStreamTrace.is_local_only());
    }

    #[test]
    fn session_event_kind_tolerates_unknown_legacy_event_type() {
        let temp = TempDir::new().unwrap();
        let event = SessionEvent::new(
            "legacy_future_event",
            "legacy-session".to_string(),
            temp.path().to_path_buf(),
            json!({"value":1}),
        );
        let line = serde_json::to_string(&event).unwrap();
        let parsed: SessionEvent = serde_json::from_str(&line).unwrap();

        assert_eq!(parsed.event_type, "legacy_future_event");
        assert_eq!(parsed.kind(), None);
        assert_eq!(parsed.payload["value"], 1);
    }
}