use std::path::Path; use serde::{Deserialize, Serialize}; const MAX_SESSION_ID_LEN: usize = 512; const MAX_SESSION_PATH_LEN: usize = 4096; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentSessionRef { pub kind: AgentSessionRefKind, pub value: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AgentSessionRefKind { Id, Path, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentResumePlan { pub agent: String, pub argv: Vec, pub dedupe_key: String, } #[derive(Debug, Clone, Copy)] pub struct AgentResumeLaunch<'a> { pub plan: &'a AgentResumePlan, pub initial_history_ansi: Option<&'a str>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PersistedAgentSession { pub source: String, pub agent: String, pub session_ref: AgentSessionRef, } impl AgentSessionRef { pub fn id(value: impl Into) -> Option { let value = value.into(); valid_session_id(&value).then_some(Self { kind: AgentSessionRefKind::Id, value, }) } pub fn path(value: impl Into) -> Option { let value = value.into(); valid_session_path(&value).then_some(Self { kind: AgentSessionRefKind::Path, value, }) } } pub fn session_ref_from_report( source: &str, agent: &str, agent_session_id: Option, _agent_session_path: Option, ) -> Option { if !is_official_agent_source(source, agent) { return None; } if agent == "pi" { return _agent_session_path .and_then(AgentSessionRef::path) .or_else(|| agent_session_id.and_then(AgentSessionRef::id)); } agent_session_id.and_then(AgentSessionRef::id) } pub fn session_ref_from_snapshot( source: &str, agent: &str, kind: AgentSessionRefKind, value: &str, ) -> Option { if !is_official_agent_source(source, agent) { return None; } let session_ref = match (agent, kind) { ("pi", AgentSessionRefKind::Path) => AgentSessionRef::path(value)?, (_, AgentSessionRefKind::Id) => AgentSessionRef::id(value)?, _ => return None, }; Some(PersistedAgentSession { source: source.to_string(), agent: agent.to_string(), session_ref, }) } pub fn plan(source: &str, agent: &str, session_ref: &AgentSessionRef) -> Option { if !is_official_agent_source(source, agent) { return None; } let argv = match (source, agent, session_ref.kind) { ("herdr:claude", "claude", AgentSessionRefKind::Id) => { vec![ "claude".into(), "--resume".into(), session_ref.value.clone(), ] } ("herdr:codex", "codex", AgentSessionRefKind::Id) => { vec!["codex".into(), "resume".into(), session_ref.value.clone()] } ("herdr:pi", "pi", AgentSessionRefKind::Path | AgentSessionRefKind::Id) => { vec!["pi".into(), "--session".into(), session_ref.value.clone()] } ("herdr:hermes", "hermes", AgentSessionRefKind::Id) => { vec![ "hermes".into(), "--resume".into(), session_ref.value.clone(), ] } ("herdr:opencode", "opencode", AgentSessionRefKind::Id) => { vec![ "opencode".into(), "--session".into(), session_ref.value.clone(), ] } _ => return None, }; Some(AgentResumePlan { agent: agent.to_string(), argv, dedupe_key: dedupe_key(source, agent, session_ref), }) } pub fn dedupe_key(source: &str, agent: &str, session_ref: &AgentSessionRef) -> String { format!( "{source}\u{0}{agent}\u{0}{:?}\u{0}{}", session_ref.kind, session_ref.value ) } fn is_official_agent_source(source: &str, agent: &str) -> bool { matches!( (source, agent), ("herdr:claude", "claude") | ("herdr:codex", "codex") | ("herdr:pi", "pi") | ("herdr:hermes", "hermes") | ("herdr:opencode", "opencode") ) } fn valid_session_id(value: &str) -> bool { !value.is_empty() && value.len() <= MAX_SESSION_ID_LEN && !value.chars().any(char::is_control) } fn valid_session_path(value: &str) -> bool { !value.is_empty() && value.len() <= MAX_SESSION_PATH_LEN && !value.chars().any(char::is_control) && Path::new(value).is_absolute() } #[cfg(test)] mod tests { use super::*; #[test] fn planner_allows_supported_agents() { assert_eq!( plan( "herdr:claude", "claude", &AgentSessionRef::id("claude-session").unwrap() ) .unwrap() .argv, vec!["claude", "--resume", "claude-session"] ); assert_eq!( plan( "herdr:codex", "codex", &AgentSessionRef::id("codex-session").unwrap() ) .unwrap() .argv, vec!["codex", "resume", "codex-session"] ); assert_eq!( plan( "herdr:pi", "pi", &AgentSessionRef::path("/tmp/pi-session.jsonl").unwrap() ) .unwrap() .argv, vec!["pi", "--session", "/tmp/pi-session.jsonl"] ); assert_eq!( plan( "herdr:hermes", "hermes", &AgentSessionRef::id("hermes-session").unwrap() ) .unwrap() .argv, vec!["hermes", "--resume", "hermes-session"] ); assert_eq!( plan( "herdr:opencode", "opencode", &AgentSessionRef::id("opencode-session").unwrap() ) .unwrap() .argv, vec!["opencode", "--session", "opencode-session"] ); } #[test] fn planner_rejects_custom_and_unsupported_path_refs() { assert!(plan( "custom:claude", "claude", &AgentSessionRef::id("session").unwrap() ) .is_none()); assert!(plan( "herdr:claude", "claude", &AgentSessionRef::path("/tmp/claude-session").unwrap() ) .is_none()); } #[test] fn report_ref_prefers_pi_path_and_validates_values() { let session_ref = session_ref_from_report( "herdr:pi", "pi", Some("pi-id".into()), Some("/tmp/pi-session.jsonl".into()), ) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Path); assert_eq!(session_ref.value, "/tmp/pi-session.jsonl"); assert!(session_ref_from_report("herdr:pi", "pi", Some("bad\nid".into()), None).is_none()); assert!( session_ref_from_report("herdr:pi", "pi", None, Some("relative.jsonl".into())) .is_none() ); assert!(session_ref_from_report("custom:pi", "pi", Some("pi-id".into()), None).is_none()); assert!(session_ref_from_report( "herdr:claude", "claude", None, Some("/tmp/claude-session".into()) ) .is_none()); } #[test] fn ids_are_data_not_shell_text() { let id = "abc; rm -rf /"; let plan = plan("herdr:codex", "codex", &AgentSessionRef::id(id).unwrap()).unwrap(); assert_eq!(plan.argv, vec!["codex", "resume", id]); } #[test] fn planner_rejects_path_refs_for_id_only_agents() { assert!(plan( "herdr:hermes", "hermes", &AgentSessionRef::path("/tmp/hermes-session").unwrap() ) .is_none()); assert!(plan( "herdr:opencode", "opencode", &AgentSessionRef::path("/tmp/opencode-session").unwrap() ) .is_none()); assert!(session_ref_from_snapshot( "herdr:hermes", "hermes", AgentSessionRefKind::Id, "hermes-session" ) .is_some()); assert!(session_ref_from_snapshot( "herdr:opencode", "opencode", AgentSessionRefKind::Id, "opencode-session" ) .is_some()); } }