aboutsummaryrefslogtreecommitdiff
path: root/src/terminal
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-15 02:13:30 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-15 02:13:30 +0300
commitdcfc45b45d198434db5057c991e72c089b42c705 (patch)
tree4cbdbc253bb5dc12c734a7ed1cbf82fd444d3cd2 /src/terminal
parentc91348e68b9e438159ffecc3e26bc6f971e0fa2a (diff)
fix: preserve claude session rotations
refs #620
Diffstat (limited to 'src/terminal')
-rw-r--r--src/terminal/state.rs115
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