aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 18:08:48 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 18:08:48 +0300
commit07222706c78943da814e39abece8b5ba5a10dfc3 (patch)
tree63e58aee2a67e3074618252400c76ad29eac833b /src
parent23713874118390d33b8b26cfdc0cdcd7b05720a2 (diff)
fix: ignore nested agent session id clobbers
refs #511
Diffstat (limited to 'src')
-rw-r--r--src/terminal/state.rs162
1 files changed, 162 insertions, 0 deletions
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index bb2642d9..eeff4c49 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -314,6 +314,10 @@ impl TerminalState {
if self.known_agent_label_conflicts_with_detected_agent(&agent_label) {
return None;
}
+ let session_ref = session_ref.map(|session_ref| {
+ self.conflicting_current_session_ref(&source, &agent_label, &session_ref)
+ .unwrap_or(session_ref)
+ });
self.persisted_agent_session = None;
self.hook_authority = Some(HookAuthority {
source,
@@ -421,6 +425,27 @@ impl TerminalState {
})
}
+ fn conflicting_current_session_ref(
+ &self,
+ source: &str,
+ agent_label: &str,
+ session_ref: &crate::agent_resume::AgentSessionRef,
+ ) -> Option<crate::agent_resume::AgentSessionRef> {
+ self.current_session_identity_for_persistence().and_then(
+ |(current_source, current_agent, current_kind, current_value)| {
+ (current_source == source
+ && 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,
+ })
+ },
+ )
+ }
+
pub fn set_persisted_agent_session(
&mut self,
session: crate::agent_resume::PersistedAgentSession,
@@ -442,6 +467,12 @@ impl TerminalState {
if self.known_agent_label_conflicts_with_detected_agent(&agent_label) {
return None;
}
+ if self
+ .conflicting_current_session_ref(&source, &agent_label, &session_ref)
+ .is_some()
+ {
+ return None;
+ }
let previous_session = self.current_session_identity_for_persistence();
self.persisted_agent_session = Some(crate::agent_resume::PersistedAgentSession {
@@ -1937,6 +1968,137 @@ mod tests {
}
#[test]
+ fn different_same_agent_session_ref_is_ignored_until_current_session_clears() {
+ 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(
+ "herdr:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("nested-session"),
+ Some(21),
+ );
+
+ assert!(mutation.is_none());
+ assert_eq!(
+ terminal.hook_report_sequences.get("herdr:claude"),
+ Some(&21)
+ );
+ assert_eq!(
+ terminal
+ .persisted_agent_session
+ .as_ref()
+ .map(|session| session.session_ref.value.as_str()),
+ Some("claude-session")
+ );
+ }
+
+ #[test]
+ fn repeated_same_agent_session_ref_is_accepted_without_session_change() {
+ 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(
+ "herdr:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("claude-session"),
+ Some(21),
+ )
+ .expect("same session should be accepted");
+
+ assert!(!mutation.session_ref_changed);
+ }
+
+ #[test]
+ fn hook_authority_preserves_current_session_ref_when_incoming_ref_differs() {
+ let mut terminal = test_terminal();
+ terminal
+ .set_hook_authority_with_session_ref(
+ "herdr:copilot".into(),
+ "copilot".into(),
+ AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::id("copilot-session"),
+ Some(20),
+ )
+ .expect("initial session should be accepted");
+
+ let mutation = terminal
+ .set_hook_authority_with_session_ref(
+ "herdr:copilot".into(),
+ "copilot".into(),
+ AgentState::Blocked,
+ Some("needs approval".into()),
+ None,
+ crate::agent_resume::AgentSessionRef::id("nested-session"),
+ Some(21),
+ )
+ .expect("state update should still be accepted");
+
+ assert!(!mutation.session_ref_changed);
+ assert_eq!(terminal.state, AgentState::Blocked);
+ assert_eq!(
+ terminal
+ .hook_authority
+ .as_ref()
+ .and_then(|authority| authority.session_ref.as_ref())
+ .map(|session_ref| session_ref.value.as_str()),
+ Some("copilot-session")
+ );
+ }
+
+ #[test]
+ fn different_same_agent_session_ref_is_accepted_after_detection_clears_current_session() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Claude), AgentState::Working);
+ 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 clear = terminal.set_detected_state_with_mutation(None, AgentState::Unknown);
+ assert!(clear.session_ref_changed);
+
+ let mutation = terminal
+ .set_agent_session_ref(
+ "herdr:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("new-session"),
+ Some(21),
+ )
+ .expect("new session should be accepted after clear");
+
+ assert!(mutation.session_ref_changed);
+ assert_eq!(
+ terminal
+ .persisted_agent_session
+ .as_ref()
+ .map(|session| session.session_ref.value.as_str()),
+ Some("new-session")
+ );
+ }
+
+ #[test]
fn clearing_hook_authority_clears_session_ref() {
let mut terminal = test_terminal();
let session_path = test_session_path("pi.jsonl");