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, schemars::JsonSchema)] #[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, 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" || agent == "omp" { 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 normalize_session_start_source(value: Option) -> Option { match value.as_deref().map(str::trim) { Some( source @ ("startup" | "resume" | "clear" | "compact" | "branch" | "new" | "fork" | "select"), ) => Some(source.to_string()), _ => None, } } pub fn is_reserved_native_state_source(source: &str, agent: &str) -> bool { matches!( (source, agent), ("herdr:claude", "claude") | ("herdr:codex", "codex") | ("herdr:copilot", "copilot") | ("herdr:devin", "devin") | ("herdr:droid", "droid") | ("herdr:qodercli", "qodercli") | ("herdr:qwen", "qwen") | ("herdr:cursor", "cursor") | ("herdr:grok", "grok") ) } 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" | "omp", 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:copilot", "copilot", AgentSessionRefKind::Id) => { vec!["copilot".into(), format!("--resume={}", session_ref.value)] } ("herdr:devin", "devin", AgentSessionRefKind::Id) => { vec!["devin".into(), "--resume".into(), session_ref.value.clone()] } ("herdr:droid", "droid", AgentSessionRefKind::Id) => { vec!["droid".into(), "--resume".into(), session_ref.value.clone()] } ("herdr:kimi", "kimi", AgentSessionRefKind::Id) => { vec!["kimi".into(), "--session".into(), session_ref.value.clone()] } ("herdr:mastracode", "mastracode", AgentSessionRefKind::Id) => { vec![ "mastracode".into(), "--thread".into(), session_ref.value.clone(), ] } ("herdr:pi", "pi", AgentSessionRefKind::Path | AgentSessionRefKind::Id) => { vec!["pi".into(), "--session".into(), session_ref.value.clone()] } ("herdr:omp", "omp", AgentSessionRefKind::Path | AgentSessionRefKind::Id) => { // omp resume is `-r, --resume=` (ID prefix or path); it has no // `--session` flag, unlike pi. vec!["omp".into(), format!("--resume={}", session_ref.value)] } ("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(), ] } ("herdr:qodercli", "qodercli", AgentSessionRefKind::Id) => { vec![ "qodercli".into(), "--resume".into(), session_ref.value.clone(), ] } ("herdr:qwen", "qwen", AgentSessionRefKind::Id) => { vec!["qwen".into(), "--resume".into(), session_ref.value.clone()] } ("herdr:kilo", "kilo", AgentSessionRefKind::Id) => { vec!["kilo".into(), "--session".into(), session_ref.value.clone()] } ("herdr:cursor", "cursor", AgentSessionRefKind::Id) => { vec![ if cfg!(windows) { "cursor-agent.cmd" } else { "cursor-agent" } .into(), "--resume".into(), session_ref.value.clone(), ] } ("herdr:antigravity_cli", "agy", AgentSessionRefKind::Id) => { vec![ "agy".into(), "--conversation".into(), session_ref.value.clone(), ] } ("herdr:grok", "grok", AgentSessionRefKind::Id) => { vec!["grok".into(), "--resume".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 ) } pub(crate) fn is_official_agent_source(source: &str, agent: &str) -> bool { matches!( (source, agent), ("herdr:claude", "claude") | ("herdr:codex", "codex") | ("herdr:copilot", "copilot") | ("herdr:devin", "devin") | ("herdr:droid", "droid") | ("herdr:kimi", "kimi") | ("herdr:omp", "omp") | ("herdr:mastracode", "mastracode") | ("herdr:pi", "pi") | ("herdr:hermes", "hermes") | ("herdr:opencode", "opencode") | ("herdr:qodercli", "qodercli") | ("herdr:qwen", "qwen") | ("herdr:kilo", "kilo") | ("herdr:cursor", "cursor") | ("herdr:antigravity_cli", "agy") | ("herdr:grok", "grok") ) } 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::*; fn absolute_test_path(name: &str) -> String { std::env::current_dir() .unwrap() .join(name) .display() .to_string() } #[test] fn native_state_reservation_excludes_full_lifecycle_sources() { assert!(is_reserved_native_state_source("herdr:claude", "claude")); assert!(is_reserved_native_state_source("herdr:codex", "codex")); assert!(is_reserved_native_state_source("herdr:devin", "devin")); assert!(!is_reserved_native_state_source("herdr:kimi", "kimi")); assert!(!is_reserved_native_state_source( "herdr:opencode", "opencode" )); } #[test] fn planner_allows_supported_agents() { let pi_session = absolute_test_path("pi-session.jsonl"); let omp_session = absolute_test_path("omp-session.jsonl"); 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:copilot", "copilot", &AgentSessionRef::id("copilot-session").unwrap() ) .unwrap() .argv, vec!["copilot", "--resume=copilot-session"] ); assert_eq!( plan( "herdr:devin", "devin", &AgentSessionRef::id("devin-session").unwrap() ) .unwrap() .argv, vec!["devin", "--resume", "devin-session"] ); assert_eq!( plan( "herdr:droid", "droid", &AgentSessionRef::id("droid-session").unwrap() ) .unwrap() .argv, vec!["droid", "--resume", "droid-session"] ); assert_eq!( plan( "herdr:kimi", "kimi", &AgentSessionRef::id("kimi-session").unwrap() ) .unwrap() .argv, vec!["kimi", "--session", "kimi-session"] ); assert_eq!( plan( "herdr:mastracode", "mastracode", &AgentSessionRef::id("mastracode-session").unwrap() ) .unwrap() .argv, vec!["mastracode", "--thread", "mastracode-session"] ); assert_eq!( plan( "herdr:pi", "pi", &AgentSessionRef::path(&pi_session).unwrap() ) .unwrap() .argv, vec!["pi", "--session", pi_session.as_str()] ); assert_eq!( plan( "herdr:omp", "omp", &AgentSessionRef::path(&omp_session).unwrap() ) .unwrap() .argv, vec!["omp", format!("--resume={omp_session}").as_str()] ); 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"] ); assert_eq!( plan( "herdr:qodercli", "qodercli", &AgentSessionRef::id("qoder-session").unwrap() ) .unwrap() .argv, vec!["qodercli", "--resume", "qoder-session"] ); assert_eq!( plan( "herdr:qwen", "qwen", &AgentSessionRef::id("qwen-session").unwrap() ) .unwrap() .argv, vec!["qwen", "--resume", "qwen-session"] ); assert_eq!( plan( "herdr:kilo", "kilo", &AgentSessionRef::id("kilo-session").unwrap() ) .unwrap() .argv, vec!["kilo", "--session", "kilo-session"] ); assert_eq!( plan( "herdr:cursor", "cursor", &AgentSessionRef::id("cursor-session").unwrap() ) .unwrap() .argv, vec![ if cfg!(windows) { "cursor-agent.cmd" } else { "cursor-agent" }, "--resume", "cursor-session", ] ); assert_eq!( plan( "herdr:antigravity_cli", "agy", &AgentSessionRef::id("agy-session").unwrap() ) .unwrap() .argv, vec!["agy", "--conversation", "agy-session"] ); assert_eq!( plan( "herdr:grok", "grok", &AgentSessionRef::id("grok-session").unwrap() ) .unwrap() .argv, vec!["grok", "--resume", "grok-session"] ); } #[test] fn planner_rejects_custom_and_unsupported_path_refs() { let claude_session = absolute_test_path("claude-session"); assert!(plan( "custom:claude", "claude", &AgentSessionRef::id("session").unwrap() ) .is_none()); assert!(plan( "herdr:claude", "claude", &AgentSessionRef::path(&claude_session).unwrap() ) .is_none()); } #[test] fn report_ref_prefers_pi_and_omp_paths_and_validates_values() { let pi_session = absolute_test_path("pi-session.jsonl"); let omp_session = absolute_test_path("omp-session.jsonl"); let claude_session = absolute_test_path("claude-session"); let copilot_session = absolute_test_path("copilot-session"); let session_ref = session_ref_from_report( "herdr:pi", "pi", Some("pi-id".into()), Some(pi_session.clone()), ) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Path); assert_eq!(session_ref.value, pi_session); 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()); let session_ref = session_ref_from_report( "herdr:omp", "omp", Some("omp-id".into()), Some(omp_session.clone()), ) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Path); assert_eq!(session_ref.value, omp_session); let session_ref = session_ref_from_report("herdr:omp", "omp", Some("omp-id".into()), None).unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "omp-id"); let session_ref = session_ref_from_report( "herdr:omp", "omp", Some("omp-id".into()), Some("relative.jsonl".into()), ) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "omp-id"); assert!( session_ref_from_report("herdr:omp", "omp", None, Some("relative.jsonl".into())) .is_none() ); assert!( session_ref_from_report("herdr:claude", "claude", None, Some(claude_session)).is_none() ); let session_ref = session_ref_from_report("herdr:copilot", "copilot", Some("copilot-id".into()), None) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "copilot-id"); assert!( session_ref_from_report("herdr:copilot", "copilot", None, Some(copilot_session)) .is_none() ); let session_ref = session_ref_from_report("herdr:devin", "devin", Some("devin-id".into()), None).unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "devin-id"); let session_ref = session_ref_from_report("herdr:droid", "droid", Some("droid-id".into()), None).unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "droid-id"); assert!(session_ref_from_report( "herdr:droid", "droid", None, Some("/tmp/droid-session".into()) ) .is_none()); let session_ref = session_ref_from_report("herdr:kimi", "kimi", Some("kimi-id".into()), None).unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "kimi-id"); let session_ref = session_ref_from_report( "herdr:mastracode", "mastracode", Some("mastracode-id".into()), None, ) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "mastracode-id"); let session_ref = session_ref_from_report("herdr:kilo", "kilo", Some("kilo-id".into()), None).unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "kilo-id"); let session_ref = session_ref_from_report("herdr:qodercli", "qodercli", Some("qoder-id".into()), None) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "qoder-id"); let session_ref = session_ref_from_report("herdr:qwen", "qwen", Some("qwen-id".into()), None).unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "qwen-id"); let session_ref = session_ref_from_report("herdr:antigravity_cli", "agy", Some("agy-id".into()), None) .unwrap(); assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "agy-id"); } #[test] fn normalize_session_start_source_allows_known_values() { assert_eq!( normalize_session_start_source(Some("startup".into())), Some("startup".into()) ); assert_eq!( normalize_session_start_source(Some("resume".into())), Some("resume".into()) ); assert_eq!( normalize_session_start_source(Some("clear".into())), Some("clear".into()) ); assert_eq!( normalize_session_start_source(Some("compact".into())), Some("compact".into()) ); assert_eq!( normalize_session_start_source(Some("branch".into())), Some("branch".into()) ); assert_eq!( normalize_session_start_source(Some("new".into())), Some("new".into()) ); assert_eq!( normalize_session_start_source(Some("fork".into())), Some("fork".into()) ); assert_eq!( normalize_session_start_source(Some("select".into())), Some("select".into()) ); assert_eq!( normalize_session_start_source(Some(" resume ".into())), Some("resume".into()) ); assert_eq!(normalize_session_start_source(Some("other".into())), None); assert_eq!(normalize_session_start_source(None), None); } #[test] fn ids_are_data_not_shell_text() { let id = "abc; rm -rf /"; let codex_plan = plan("herdr:codex", "codex", &AgentSessionRef::id(id).unwrap()).unwrap(); assert_eq!(codex_plan.argv, vec!["codex", "resume", id]); let copilot_plan = plan( "herdr:copilot", "copilot", &AgentSessionRef::id(id).unwrap(), ) .unwrap(); assert_eq!(copilot_plan.argv, vec!["copilot", "--resume=abc; rm -rf /"]); let devin_plan = plan("herdr:devin", "devin", &AgentSessionRef::id(id).unwrap()).unwrap(); assert_eq!(devin_plan.argv, vec!["devin", "--resume", id]); } #[test] fn planner_rejects_path_refs_for_id_only_agents() { let hermes_session = absolute_test_path("hermes-session"); let opencode_session = absolute_test_path("opencode-session"); let kilo_session = absolute_test_path("kilo-session"); let copilot_session = absolute_test_path("copilot-session"); let devin_session = absolute_test_path("devin-session"); assert!(plan( "herdr:hermes", "hermes", &AgentSessionRef::path(&hermes_session).unwrap() ) .is_none()); assert!(plan( "herdr:opencode", "opencode", &AgentSessionRef::path(&opencode_session).unwrap() ) .is_none()); assert!(plan( "herdr:kilo", "kilo", &AgentSessionRef::path(&kilo_session).unwrap() ) .is_none()); assert!(plan( "herdr:copilot", "copilot", &AgentSessionRef::path(&copilot_session).unwrap() ) .is_none()); assert!(plan( "herdr:devin", "devin", &AgentSessionRef::path(&devin_session).unwrap() ) .is_none()); assert!(session_ref_from_snapshot( "herdr:mastracode", "mastracode", AgentSessionRefKind::Id, "mastracode-session" ) .is_some()); 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()); assert!(session_ref_from_snapshot( "herdr:kilo", "kilo", AgentSessionRefKind::Id, "kilo-session" ) .is_some()); assert!(session_ref_from_snapshot( "herdr:copilot", "copilot", AgentSessionRefKind::Id, "copilot-session" ) .is_some()); assert!(session_ref_from_snapshot( "herdr:devin", "devin", AgentSessionRefKind::Id, "devin-session" ) .is_some()); assert!(session_ref_from_snapshot( "herdr:antigravity_cli", "agy", AgentSessionRefKind::Id, "agy-session" ) .is_some()); let agy_session = absolute_test_path("agy-session"); assert!(plan( "herdr:antigravity_cli", "agy", &AgentSessionRef::path(&agy_session).unwrap() ) .is_none()); } }