diff options
Diffstat (limited to 'src/terminal')
| -rw-r--r-- | src/terminal/state.rs | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 2b9ef4ad..2b17404a 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -797,6 +797,26 @@ impl TerminalState { } } + fn forget_stale_full_lifecycle_hook_session( + &mut self, + source: &str, + agent_label: &str, + session_ref: &crate::agent_resume::AgentSessionRef, + ) { + let remove_source = self + .stale_full_lifecycle_hook_sessions + .get_mut(source) + .is_some_and(|stale_sessions| { + stale_sessions.retain(|stale| { + stale.agent_label != agent_label || &stale.session_ref != session_ref + }); + stale_sessions.is_empty() + }); + if remove_source { + self.stale_full_lifecycle_hook_sessions.remove(source); + } + } + fn detected_state_observed_before_release_suppression( &self, detected_agent: Option<Agent>, @@ -912,6 +932,7 @@ impl TerminalState { "codex", Some("startup" | "clear" | "resume" | "compact") ) | ("herdr:opencode", "opencode", Some("new")) + | ("herdr:pi", "pi", Some("new" | "resume" | "fork")) | ( "herdr:omp", "omp", @@ -1001,6 +1022,9 @@ impl TerminalState { let previous_state = self.state; let previous_presentation = self.effective_presentation_for_state_at(previous_state, now); let previous_session = self.current_session_identity_for_persistence(); + if session_replacement_allowed || foreground_takeover_allowed { + self.forget_stale_full_lifecycle_hook_session(&source, &agent_label, &session_ref); + } if let Some(replaced_hook_session) = replaced_hook_session { self.remember_stale_full_lifecycle_hook_session( source.clone(), @@ -1489,6 +1513,208 @@ mod tests { } #[test] + fn pi_session_replacement_reports_reanchor_full_lifecycle_authority() { + for reason in ["new", "resume", "fork"] { + let mut terminal = test_terminal(); + let old_session = test_session_path(&format!("pi-{reason}-old.jsonl")); + let new_session = test_session_path(&format!("pi-{reason}-new.jsonl")); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle); + terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Idle, + None, + None, + crate::agent_resume::AgentSessionRef::path(old_session), + Some(10), + ); + + let session_report = terminal.set_agent_session_ref_for_session_start( + "herdr:pi".into(), + "pi".into(), + crate::agent_resume::AgentSessionRef::path(new_session.clone()), + Some(11), + Some(reason.into()), + ); + + assert!( + session_report.is_some(), + "{reason} should replace the previous Pi session" + ); + assert!(terminal.hook_authority.is_none()); + + let working = terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Working, + None, + None, + crate::agent_resume::AgentSessionRef::path(new_session.clone()), + Some(12), + ); + + assert!( + working.is_some(), + "{reason} should accept working for the replacement session" + ); + assert_eq!(terminal.state, AgentState::Working); + assert_eq!( + terminal.hook_authority.as_ref().unwrap().session_ref, + crate::agent_resume::AgentSessionRef::path(new_session) + ); + } + } + + #[test] + fn pi_resume_reactivates_a_previously_stale_session() { + let mut terminal = test_terminal(); + let session_a = test_session_path("pi-session-a.jsonl"); + let session_b = test_session_path("pi-session-b.jsonl"); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle); + terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Idle, + None, + None, + crate::agent_resume::AgentSessionRef::path(session_a.clone()), + Some(10), + ); + + terminal.set_agent_session_ref_for_session_start( + "herdr:pi".into(), + "pi".into(), + crate::agent_resume::AgentSessionRef::path(session_b.clone()), + Some(11), + Some("new".into()), + ); + terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Idle, + None, + None, + crate::agent_resume::AgentSessionRef::path(session_b.clone()), + Some(12), + ); + + let resumed = terminal.set_agent_session_ref_for_session_start( + "herdr:pi".into(), + "pi".into(), + crate::agent_resume::AgentSessionRef::path(session_a.clone()), + Some(13), + Some("resume".into()), + ); + let working = terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Working, + None, + None, + crate::agent_resume::AgentSessionRef::path(session_a.clone()), + Some(14), + ); + + assert!(resumed.is_some()); + assert!(working.is_some()); + assert_eq!(terminal.state, AgentState::Working); + assert_eq!( + terminal.hook_authority.as_ref().unwrap().session_ref, + crate::agent_resume::AgentSessionRef::path(session_a) + ); + + let late_session_b = terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Idle, + None, + None, + crate::agent_resume::AgentSessionRef::path(session_b), + Some(15), + ); + assert!(late_session_b.is_none()); + assert_eq!(terminal.state, AgentState::Working); + } + + #[test] + fn pi_startup_adopts_persisted_session_without_live_authority() { + let mut terminal = test_terminal(); + let old_session = test_session_path("pi-startup-old.jsonl"); + let new_session = test_session_path("pi-startup-new.jsonl"); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle); + terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { + source: "herdr:pi".into(), + agent: "pi".into(), + session_ref: crate::agent_resume::AgentSessionRef::path(old_session) + .expect("test session path should be valid"), + }); + + let startup = terminal.set_agent_session_ref_for_session_start( + "herdr:pi".into(), + "pi".into(), + crate::agent_resume::AgentSessionRef::path(new_session.clone()), + Some(11), + Some("startup".into()), + ); + + assert!(startup.is_some()); + assert_eq!( + terminal.current_session_identity_for_persistence(), + Some(( + "herdr:pi".into(), + "pi".into(), + crate::agent_resume::AgentSessionRefKind::Path, + new_session, + )) + ); + } + + #[test] + fn pi_non_replacement_reports_preserve_full_lifecycle_authority() { + for reason in [None, Some("reload"), Some("startup")] { + let mut terminal = test_terminal(); + let old_session = test_session_path("pi-current.jsonl"); + let new_session = test_session_path("pi-unexpected.jsonl"); + terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle); + terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Idle, + None, + None, + crate::agent_resume::AgentSessionRef::path(old_session.clone()), + Some(10), + ); + + let session_report = terminal.set_agent_session_ref_for_session_start( + "herdr:pi".into(), + "pi".into(), + crate::agent_resume::AgentSessionRef::path(new_session.clone()), + Some(11), + reason.map(str::to_string), + ); + let working = terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Working, + None, + None, + crate::agent_resume::AgentSessionRef::path(new_session), + Some(12), + ); + + assert!(session_report.is_none()); + assert!(working.is_none()); + assert_eq!(terminal.state, AgentState::Idle); + assert_eq!( + terminal.hook_authority.as_ref().unwrap().session_ref, + crate::agent_resume::AgentSessionRef::path(old_session), + "{reason:?} must not replace the current Pi session" + ); + } + } + + #[test] fn omp_resume_session_report_reanchors_full_lifecycle_authority() { let mut terminal = test_terminal(); let old_session = test_session_path("omp-old.jsonl"); |
