diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-15 02:13:30 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-15 02:13:30 +0300 |
| commit | dcfc45b45d198434db5057c991e72c089b42c705 (patch) | |
| tree | 4cbdbc253bb5dc12c734a7ed1cbf82fd444d3cd2 /src/terminal | |
| parent | c91348e68b9e438159ffecc3e26bc6f971e0fa2a (diff) | |
fix: preserve claude session rotations
refs #620
Diffstat (limited to 'src/terminal')
| -rw-r--r-- | src/terminal/state.rs | 115 |
1 files changed, 108 insertions, 7 deletions
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<crate::agent_resume::AgentSessionRef> { 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, @@ -643,6 +659,17 @@ impl TerminalState { session_ref: Option<crate::agent_resume::AgentSessionRef>, seq: Option<u64>, ) -> Option<TerminalStateMutation> { + 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<crate::agent_resume::AgentSessionRef>, + seq: Option<u64>, + session_start_source: Option<String>, + ) -> Option<TerminalStateMutation> { let session_ref = session_ref?; if !self.accept_hook_report(&source, seq) { return None; @@ -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; @@ -2361,6 +2393,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(); terminal |
