aboutsummaryrefslogtreecommitdiff
path: root/src/terminal
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 17:21:10 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 17:21:10 +0300
commit74a771d12623726983ed2186380f6a216e797a06 (patch)
tree34bec0a2a20177bcf34d4c54c1adf8e33c64bf2c /src/terminal
parentf4d216b63d010987775dc6eaf92afa72f86f2816 (diff)
fix: replace stale agent session from foreground reports
refs #943
Diffstat (limited to 'src/terminal')
-rw-r--r--src/terminal/state.rs152
1 files changed, 147 insertions, 5 deletions
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index 3e536e6e..96580b7d 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -917,14 +917,23 @@ impl TerminalState {
session_start_source.as_deref(),
);
if self.current_session_owner_conflicts(&source, &agent_label)
- || self
- .conflicting_same_owner_session_ref(
+ && !(session_replacement_allowed
+ && self.foreground_agent_confirms_session_owner(
&source,
&agent_label,
&session_ref,
- session_start_source.as_deref(),
- )
- .is_some()
+ ))
+ {
+ return None;
+ }
+ if self
+ .conflicting_same_owner_session_ref(
+ &source,
+ &agent_label,
+ &session_ref,
+ session_start_source.as_deref(),
+ )
+ .is_some()
{
return None;
}
@@ -977,6 +986,19 @@ impl TerminalState {
.is_some_and(|hook_agent| hook_agent != detected_agent)
}
+ fn foreground_agent_confirms_session_owner(
+ &self,
+ source: &str,
+ agent_label: &str,
+ session_ref: &crate::agent_resume::AgentSessionRef,
+ ) -> bool {
+ let Some(detected_agent) = self.detected_agent else {
+ return false;
+ };
+ crate::detect::parse_agent_label(agent_label) == Some(detected_agent)
+ && crate::agent_resume::plan(source, agent_label, session_ref).is_some()
+ }
+
fn accept_hook_report(&mut self, source: &str, seq: Option<u64>) -> bool {
let Some(seq) = seq else {
return !self.hook_report_sequences.contains_key(source);
@@ -3501,6 +3523,126 @@ mod tests {
}
#[test]
+ fn foreground_agent_session_replaces_stale_different_owner_session_ref() {
+ let mut terminal = test_terminal();
+ terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession {
+ source: "herdr:codex".into(),
+ agent: "codex".into(),
+ session_ref: crate::agent_resume::AgentSessionRef::id("codex-session").unwrap(),
+ });
+ terminal.set_detected_state(Some(Agent::Claude), AgentState::Idle);
+
+ let mutation = terminal
+ .set_agent_session_ref_for_session_start(
+ "herdr:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("claude-session"),
+ Some(21),
+ Some("resume".into()),
+ )
+ .expect("foreground claude should replace stale codex session");
+
+ assert!(mutation.session_ref_changed);
+ assert_eq!(
+ terminal.persisted_agent_session.as_ref().map(|session| (
+ session.source.as_str(),
+ session.agent.as_str(),
+ session.session_ref.value.as_str()
+ )),
+ Some(("herdr:claude", "claude", "claude-session"))
+ );
+ }
+
+ #[test]
+ fn foreground_agent_session_requires_lifecycle_source_to_replace_different_owner() {
+ let mut terminal = test_terminal();
+ terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession {
+ source: "herdr:codex".into(),
+ agent: "codex".into(),
+ session_ref: crate::agent_resume::AgentSessionRef::id("codex-session").unwrap(),
+ });
+ terminal.set_detected_state(Some(Agent::Claude), AgentState::Idle);
+
+ let mutation = terminal.set_agent_session_ref_for_session_start(
+ "herdr:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("claude-session"),
+ Some(21),
+ None,
+ );
+
+ assert!(mutation.is_none());
+ assert_eq!(
+ terminal.persisted_agent_session.as_ref().map(|session| (
+ session.source.as_str(),
+ session.agent.as_str(),
+ session.session_ref.value.as_str()
+ )),
+ Some(("herdr:codex", "codex", "codex-session"))
+ );
+ }
+
+ #[test]
+ fn different_owner_session_ref_requires_matching_detected_agent() {
+ for detected_agent in [None, Some(Agent::Codex)] {
+ let mut terminal = test_terminal();
+ terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession {
+ source: "herdr:codex".into(),
+ agent: "codex".into(),
+ session_ref: crate::agent_resume::AgentSessionRef::id("codex-session").unwrap(),
+ });
+ terminal.set_detected_state(detected_agent, AgentState::Idle);
+
+ let mutation = terminal.set_agent_session_ref_for_session_start(
+ "herdr:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("claude-session"),
+ Some(21),
+ Some("resume".into()),
+ );
+
+ assert!(mutation.is_none(), "{detected_agent:?} should not replace");
+ assert_eq!(
+ terminal.persisted_agent_session.as_ref().map(|session| (
+ session.source.as_str(),
+ session.agent.as_str(),
+ session.session_ref.value.as_str()
+ )),
+ Some(("herdr:codex", "codex", "codex-session"))
+ );
+ }
+ }
+
+ #[test]
+ fn custom_session_report_does_not_replace_different_owner_session_ref() {
+ let mut terminal = test_terminal();
+ terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession {
+ source: "herdr:codex".into(),
+ agent: "codex".into(),
+ session_ref: crate::agent_resume::AgentSessionRef::id("codex-session").unwrap(),
+ });
+ terminal.set_detected_state(Some(Agent::Claude), AgentState::Idle);
+
+ let mutation = terminal.set_agent_session_ref_for_session_start(
+ "custom:claude".into(),
+ "claude".into(),
+ crate::agent_resume::AgentSessionRef::id("claude-session"),
+ Some(21),
+ Some("resume".into()),
+ );
+
+ assert!(mutation.is_none());
+ assert_eq!(
+ terminal.persisted_agent_session.as_ref().map(|session| (
+ session.source.as_str(),
+ session.agent.as_str(),
+ session.session_ref.value.as_str()
+ )),
+ Some(("herdr:codex", "codex", "codex-session"))
+ );
+ }
+
+ #[test]
fn different_owner_full_lifecycle_hook_does_not_replace_existing_session_ref() {
let mut terminal = test_terminal();
terminal