From dcfc45b45d198434db5057c991e72c089b42c705 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Mon, 15 Jun 2026 02:13:30 +0300 Subject: fix: preserve claude session rotations refs #620 --- src/terminal/state.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 7 deletions(-) (limited to 'src/terminal') diff --git a/src/terminal/state.rs b/src/terminal/state.rs index f2de1342..932e0735 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -356,7 +356,7 @@ impl TerminalState { return None; } let session_ref = session_ref.map(|session_ref| { - self.conflicting_current_session_ref(&source, &agent_label, &session_ref) + self.conflicting_current_session_ref(&source, &agent_label, &session_ref, None) .unwrap_or(session_ref) }); if self.live_full_lifecycle_hook_authority_conflicts_with_session( @@ -613,6 +613,7 @@ impl TerminalState { source: &str, agent_label: &str, session_ref: &crate::agent_resume::AgentSessionRef, + session_start_source: Option<&str>, ) -> Option { self.current_session_identity_for_persistence().and_then( |(current_source, current_agent, current_kind, current_value)| { @@ -620,15 +621,30 @@ impl TerminalState { && current_agent == agent_label && current_kind == crate::agent_resume::AgentSessionRefKind::Id && session_ref.kind == crate::agent_resume::AgentSessionRefKind::Id - && (current_kind != session_ref.kind || current_value != session_ref.value)) - .then_some(crate::agent_resume::AgentSessionRef { - kind: current_kind, - value: current_value, - }) + && (current_kind != session_ref.kind || current_value != session_ref.value) + && !Self::session_start_source_allows_session_replacement( + source, + agent_label, + session_start_source, + )) + .then_some(crate::agent_resume::AgentSessionRef { + kind: current_kind, + value: current_value, + }) }, ) } + fn session_start_source_allows_session_replacement( + source: &str, + agent_label: &str, + session_start_source: Option<&str>, + ) -> bool { + source == "herdr:claude" + && agent_label == "claude" + && matches!(session_start_source, Some("clear" | "resume" | "compact")) + } + pub fn set_persisted_agent_session( &mut self, session: crate::agent_resume::PersistedAgentSession, @@ -642,6 +658,17 @@ impl TerminalState { agent_label: String, session_ref: Option, seq: Option, + ) -> Option { + self.set_agent_session_ref_for_session_start(source, agent_label, session_ref, seq, None) + } + + pub fn set_agent_session_ref_for_session_start( + &mut self, + source: String, + agent_label: String, + session_ref: Option, + seq: Option, + session_start_source: Option, ) -> Option { let session_ref = session_ref?; if !self.accept_hook_report(&source, seq) { @@ -651,7 +678,12 @@ impl TerminalState { return None; } if self - .conflicting_current_session_ref(&source, &agent_label, &session_ref) + .conflicting_current_session_ref( + &source, + &agent_label, + &session_ref, + session_start_source.as_deref(), + ) .is_some() { return None; @@ -2360,6 +2392,75 @@ mod tests { ); } + #[test] + fn claude_startup_session_ref_does_not_replace_existing_session_ref() { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:claude".into(), + "claude".into(), + crate::agent_resume::AgentSessionRef::id("claude-session"), + Some(20), + ) + .expect("initial session should be accepted"); + + let mutation = terminal.set_agent_session_ref_for_session_start( + "herdr:claude".into(), + "claude".into(), + crate::agent_resume::AgentSessionRef::id("nested-session"), + Some(21), + Some("startup".into()), + ); + + assert!(mutation.is_none()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some("claude-session") + ); + } + + #[test] + fn claude_lifecycle_session_ref_replaces_existing_session_ref() { + for session_start_source in ["clear", "resume", "compact"] { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:claude".into(), + "claude".into(), + crate::agent_resume::AgentSessionRef::id("claude-session"), + Some(20), + ) + .expect("initial session should be accepted"); + + let next_session = format!("{session_start_source}-session"); + let mutation = terminal + .set_agent_session_ref_for_session_start( + "herdr:claude".into(), + "claude".into(), + crate::agent_resume::AgentSessionRef::id(&next_session), + Some(21), + Some(session_start_source.into()), + ) + .unwrap_or_else(|| panic!("{session_start_source} should replace the session")); + + assert!( + mutation.session_ref_changed, + "{session_start_source} should mark the session changed" + ); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some(next_session.as_str()), + "{session_start_source} should store the replacement session" + ); + } + } + #[test] fn repeated_same_agent_session_ref_is_accepted_without_session_change() { let mut terminal = test_terminal(); -- cgit v1.2.3