diff --git a/src/agent_resume.rs b/src/agent_resume.rs index cd1ae7bb..075c396f 100644 --- a/src/agent_resume.rs +++ b/src/agent_resume.rs @@ -69,6 +69,24 @@ pub fn session_ref_from_report( agent_session_id.and_then(AgentSessionRef::id) } +pub fn persisted_session_from_launch_args( + agent: crate::detect::Agent, + args: &[String], +) -> Option { + let [command, session_id] = args else { + return None; + }; + if agent != crate::detect::Agent::Codex || command != "resume" || session_id.starts_with('-') { + return None; + } + + Some(PersistedAgentSession { + source: "herdr:codex".into(), + agent: "codex".into(), + session_ref: AgentSessionRef::id(session_id.clone())?, + }) +} + pub fn normalize_session_start_source(value: Option) -> Option { match value.as_deref().map(str::trim) { Some( @@ -282,6 +300,40 @@ mod tests { )); } + #[test] + fn codex_noncanonical_resume_launch_has_no_explicit_session() { + assert_eq!( + persisted_session_from_launch_args( + crate::detect::Agent::Codex, + &["resume".into(), "codex-session".into()] + ) + .unwrap() + .session_ref + .value, + "codex-session" + ); + assert!(persisted_session_from_launch_args( + crate::detect::Agent::Codex, + &["resume".into(), "--last".into()] + ) + .is_none()); + assert!(persisted_session_from_launch_args( + crate::detect::Agent::Codex, + &["resume".into(), "not-a-session".into(), "--last".into()] + ) + .is_none()); + assert!(persisted_session_from_launch_args( + crate::detect::Agent::Codex, + &[ + "--remote".into(), + "ws://example.test".into(), + "resume".into(), + "remote-session".into(), + ] + ) + .is_none()); + } + #[test] fn planner_allows_supported_agents() { let pi_session = absolute_test_path("pi-session.jsonl"); diff --git a/src/app/agents.rs b/src/app/agents.rs index f25383fe..5033505d 100644 --- a/src/app/agents.rs +++ b/src/app/agents.rs @@ -160,6 +160,8 @@ impl App { { return Err(AgentStartError::InvalidArgument); } + let persisted_agent_session = + crate::agent_resume::persisted_session_from_launch_args(kind, ¶ms.args); let conflicts = self.agent_name_conflicts(&name, ""); if !conflicts.is_empty() { return Err(AgentStartError::DuplicateName { @@ -217,6 +219,9 @@ impl App { terminal.clear_agent_name(); return Err(AgentStartError::InputFailed(err.to_string())); } + if let Some(session) = persisted_agent_session { + terminal.set_managed_agent_launch_session(session); + } self.state.mark_session_dirty(); self.schedule_session_save(); diff --git a/src/app/mod.rs b/src/app/mod.rs index 131b9729..02175481 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2782,9 +2782,9 @@ mod tests { id: "req_agent_start_input".into(), method: crate::api::schema::Method::AgentStart(crate::api::schema::AgentStartParams { name: "worker".into(), - kind: "pi".into(), + kind: "codex".into(), pane_id: pane_id.clone(), - args: Vec::new(), + args: vec!["resume".into(), "codex-session".into()], timeout_ms: Some(4_000), }), }; @@ -2792,6 +2792,9 @@ mod tests { let response: serde_json::Value = serde_json::from_str(&response).unwrap(); assert_eq!(response["error"]["code"], "agent_start_input_failed"); assert_eq!(app.state.terminals[&terminal_id].agent_name, None); + assert!(app.state.terminals[&terminal_id] + .persisted_agent_session + .is_none()); assert_eq!( app.state.terminals[&terminal_id].manual_label.as_deref(), Some("shell") @@ -2804,6 +2807,15 @@ mod tests { let retry = app.handle_api_request(request()); let retry: serde_json::Value = serde_json::from_str(&retry).unwrap(); assert_eq!(retry["result"]["type"], "agent_started"); + assert_eq!( + retry["result"]["agent"]["agent_session"], + serde_json::json!({ + "source": "herdr:codex", + "agent": "codex", + "kind": "id", + "value": "codex-session", + }) + ); assert_eq!( app.state.terminals[&terminal_id].agent_name.as_deref(), Some("worker") diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 5c659eb9..ee170198 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -133,6 +133,7 @@ pub struct TerminalState { pub agent_name: Option, agent_name_owner: Option, managed_agent: Option, + managed_agent_launch_session: Option, hook_report_sequences: HashMap, suppressed_full_lifecycle_hook_reports: HashMap, stale_full_lifecycle_hook_sessions: HashMap>, @@ -167,6 +168,7 @@ impl TerminalState { agent_name: None, agent_name_owner: None, managed_agent: None, + managed_agent_launch_session: None, hook_report_sequences: HashMap::new(), suppressed_full_lifecycle_hook_reports: HashMap::new(), stale_full_lifecycle_hook_sessions: HashMap::new(), @@ -1367,6 +1369,14 @@ impl TerminalState { self.persisted_agent_session = Some(session); } + pub fn set_managed_agent_launch_session( + &mut self, + session: crate::agent_resume::PersistedAgentSession, + ) { + self.persisted_agent_session = Some(session.clone()); + self.managed_agent_launch_session = Some(session); + } + pub fn set_agent_session_ref( &mut self, source: String, @@ -1587,11 +1597,15 @@ impl TerminalState { self.hook_authority = None; } self.reconcile_agent_name_owner(&agent_label, Some(&session_ref)); - self.persisted_agent_session = Some(crate::agent_resume::PersistedAgentSession { + let persisted_session = crate::agent_resume::PersistedAgentSession { source, agent: agent_label, session_ref, - }); + }; + if self.managed_agent_launch_session.as_ref() == Some(&persisted_session) { + self.managed_agent_launch_session = None; + } + self.persisted_agent_session = Some(persisted_session); let current_session = self.current_session_identity_for_persistence(); Some(TerminalStateMutation { effective_state_change: self.recompute_effective_state( @@ -1972,6 +1986,7 @@ impl TerminalState { kind: managed.kind, phase: ManagedAgentPhase::Active, }); + self.managed_agent_launch_session = None; return true; } return false; @@ -1999,6 +2014,7 @@ impl TerminalState { kind: managed.kind, phase: ManagedAgentPhase::Active, }); + self.managed_agent_launch_session = None; return true; } if ready_after.is_some() { @@ -2041,6 +2057,14 @@ impl TerminalState { } pub fn clear_agent_name(&mut self) { + if self + .managed_agent_launch_session + .take() + .as_ref() + .is_some_and(|session| self.persisted_agent_session.as_ref() == Some(session)) + { + self.persisted_agent_session = None; + } self.agent_name = None; self.agent_name_owner = None; self.managed_agent = None; @@ -2269,9 +2293,15 @@ mod tests { Duration::from_millis(10), Duration::from_millis(20), ); + timed_out.set_managed_agent_launch_session(crate::agent_resume::PersistedAgentSession { + source: "herdr:codex".into(), + agent: "codex".into(), + session_ref: crate::agent_resume::AgentSessionRef::id("codex-session").unwrap(), + }); assert!(timed_out.reconcile_managed_agent_at(now + Duration::from_millis(20), false)); assert_eq!(timed_out.agent_name, None); assert_eq!(timed_out.managed_agent_kind(), None); + assert!(timed_out.persisted_agent_session.is_none()); } #[test]