aboutsummaryrefslogtreecommitdiff
path: root/src/terminal
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 20:55:51 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-09 04:26:19 +0300
commitee51c5e5fbaea694e65bb0a4908f75bcaf9707d1 (patch)
treef566d2a5921d4112602a7abcffb5cf6eb04d5f24 /src/terminal
parenta7cd7806f2fa0251e2dca41ffec61ceafb6f0e18 (diff)
feat: use pty-first agent detection
Diffstat (limited to 'src/terminal')
-rw-r--r--src/terminal/metadata.rs7
-rw-r--r--src/terminal/state.rs1052
2 files changed, 657 insertions, 402 deletions
diff --git a/src/terminal/metadata.rs b/src/terminal/metadata.rs
index 1dcb80c0..2666b32f 100644
--- a/src/terminal/metadata.rs
+++ b/src/terminal/metadata.rs
@@ -352,7 +352,7 @@ impl TerminalState {
fn effective_custom_status_for_state_at_with_ttl(
&self,
- state: AgentState,
+ _state: AgentState,
now: Instant,
enforce_ttl: bool,
) -> Option<String> {
@@ -360,10 +360,7 @@ impl TerminalState {
return Some(custom_status);
}
- if self.visible_blocker_overrides_hook()
- || self.visible_working_overrides_hook()
- || self.visible_idle_masks_hook_custom_status(state, now)
- {
+ if self.visible_blocker_overrides_hook() {
return None;
}
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index eeff4c49..afd94ecf 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -1,11 +1,12 @@
use std::collections::HashMap;
use std::path::PathBuf;
-use std::time::{Duration, Instant};
+#[cfg(test)]
+use std::time::Duration;
+use std::time::Instant;
-// Effective state arbitration is intentionally centralized here. Hooks are the
-// default authority for agent-owned internal state, but a narrow set of strong
-// visible screen signals can veto stale hook reports. Precedence is:
-// strong visible blocker > visible working/idle recovery > hook > fallback.
+// Effective state arbitration is intentionally centralized here. Full lifecycle
+// Herdr hook integrations are hook-authoritative while live; screen recovery
+// remains only for session-only/custom hook paths and fallback detection.
// Process-exit updates clear matching hook authority before recomputing state.
use crate::detect::{Agent, AgentState};
@@ -15,9 +16,6 @@ use crate::terminal::TerminalId;
mod metadata;
pub use metadata::{AgentMetadata, AgentMetadataReport, EffectivePresentation};
-const SCREEN_WORKING_HOLD: Duration = Duration::from_millis(1200);
-const STALE_HOOK_IDLE_GRACE: Duration = Duration::from_secs(2);
-
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HookAuthority {
pub source: String,
@@ -30,6 +28,12 @@ pub struct HookAuthority {
}
#[derive(Debug, Clone, PartialEq, Eq)]
+struct SuppressedFullLifecycleHookReport {
+ agent_label: String,
+ session_ref: Option<crate::agent_resume::AgentSessionRef>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveStateChange {
pub previous_agent_label: Option<String>,
pub previous_known_agent: Option<Agent>,
@@ -58,16 +62,14 @@ pub struct TerminalState {
pub detected_agent: Option<Agent>,
pub fallback_state: AgentState,
fallback_visible_blocker: bool,
- fallback_visible_idle: bool,
- fallback_visible_working: bool,
fallback_observed_at: Option<Instant>,
- stale_hook_idle_since: Option<Instant>,
pub hook_authority: Option<HookAuthority>,
pub agent_metadata: HashMap<String, AgentMetadata>,
pub persisted_agent_session: Option<crate::agent_resume::PersistedAgentSession>,
pub manual_label: Option<String>,
pub agent_name: Option<String>,
hook_report_sequences: HashMap<String, u64>,
+ suppressed_full_lifecycle_hook_reports: HashMap<String, SuppressedFullLifecycleHookReport>,
metadata_report_sequences: HashMap<String, u64>,
pub state: AgentState,
pub revision: u64,
@@ -84,16 +86,14 @@ impl TerminalState {
detected_agent: None,
fallback_state: AgentState::Unknown,
fallback_visible_blocker: false,
- fallback_visible_idle: false,
- fallback_visible_working: false,
fallback_observed_at: None,
- stale_hook_idle_since: None,
hook_authority: None,
agent_metadata: HashMap::new(),
persisted_agent_session: None,
manual_label: None,
agent_name: None,
hook_report_sequences: HashMap::new(),
+ suppressed_full_lifecycle_hook_reports: HashMap::new(),
metadata_report_sequences: HashMap::new(),
state: AgentState::Unknown,
revision: 0,
@@ -153,14 +153,14 @@ impl TerminalState {
agent: Option<Agent>,
fallback_state: AgentState,
visible_blocker: bool,
- visible_idle: bool,
+ _ignored_screen_idle: bool,
process_exited: bool,
) -> Option<EffectiveStateChange> {
self.set_detected_state_with_screen_signals_at(
agent,
fallback_state,
visible_blocker,
- visible_idle,
+ false,
false,
process_exited,
Instant::now(),
@@ -173,8 +173,8 @@ impl TerminalState {
agent: Option<Agent>,
fallback_state: AgentState,
visible_blocker: bool,
- visible_idle: bool,
- visible_working: bool,
+ _ignored_screen_idle: bool,
+ _visible_working: bool,
process_exited: bool,
now: Instant,
) -> TerminalStateMutation {
@@ -184,11 +184,36 @@ impl TerminalState {
let previous_presentation = self.effective_presentation_for_state_at(previous_state, now);
let previous_detected_agent = self.detected_agent;
let previous_session = self.current_session_identity_for_persistence();
+ if self.should_ignore_detected_state_under_full_lifecycle_hook(agent, process_exited) {
+ if self
+ .hook_authority
+ .as_ref()
+ .and_then(|authority| crate::detect::parse_agent_label(&authority.agent_label))
+ == agent
+ {
+ self.detected_agent = agent;
+ }
+ return TerminalStateMutation {
+ effective_state_change: self.recompute_effective_state(
+ previous_agent_label,
+ previous_known_agent,
+ previous_state,
+ previous_presentation,
+ now,
+ ),
+ session_ref_changed: previous_session
+ != self.current_session_identity_for_persistence(),
+ };
+ }
self.detected_agent = agent;
+ if !process_exited {
+ self.clear_full_lifecycle_hook_suppression_for_detected_agent(
+ previous_detected_agent,
+ agent,
+ );
+ }
self.fallback_state = fallback_state;
self.fallback_visible_blocker = visible_blocker && fallback_state == AgentState::Blocked;
- self.fallback_visible_idle = visible_idle && fallback_state == AgentState::Idle;
- self.fallback_visible_working = visible_working && fallback_state == AgentState::Working;
self.fallback_observed_at = Some(now);
if process_exited
&& self.hook_authority_not_newer_than(now)
@@ -196,8 +221,8 @@ impl TerminalState {
crate::detect::parse_agent_label(&authority.agent_label) == agent
})
{
+ self.suppress_current_full_lifecycle_hook_authority();
self.hook_authority = None;
- self.stale_hook_idle_since = None;
}
if self.hook_authority_not_newer_than(now)
&& (self.hook_authority_conflicts_with_detected_agent(agent)
@@ -208,8 +233,8 @@ impl TerminalState {
== previous_detected_agent
})))
{
+ self.suppress_current_full_lifecycle_hook_authority();
self.hook_authority = None;
- self.stale_hook_idle_since = None;
}
let detected_agent_changed_or_disappeared =
previous_detected_agent.is_some() && agent != previous_detected_agent;
@@ -220,7 +245,6 @@ impl TerminalState {
{
self.persisted_agent_session = None;
}
- self.update_stale_hook_idle_window(now);
TerminalStateMutation {
effective_state_change: self.recompute_effective_state(
previous_agent_label,
@@ -302,6 +326,9 @@ impl TerminalState {
seq: Option<u64>,
now: Instant,
) -> Option<TerminalStateMutation> {
+ if self.full_lifecycle_hook_report_is_suppressed(&source, &agent_label, &session_ref) {
+ return None;
+ }
if !self.accept_hook_report(&source, seq) {
return None;
}
@@ -318,6 +345,16 @@ impl TerminalState {
self.conflicting_current_session_ref(&source, &agent_label, &session_ref)
.unwrap_or(session_ref)
});
+ if self.live_full_lifecycle_hook_authority_conflicts_with_session(
+ &source,
+ &agent_label,
+ &session_ref,
+ ) {
+ return None;
+ }
+ if session_ref.is_some() {
+ self.suppressed_full_lifecycle_hook_reports.remove(&source);
+ }
self.persisted_agent_session = None;
self.hook_authority = Some(HookAuthority {
source,
@@ -328,7 +365,6 @@ impl TerminalState {
reported_at: now,
session_ref,
});
- self.stale_hook_idle_since = None;
let current_session = self.current_session_identity_for_persistence();
Some(TerminalStateMutation {
effective_state_change: self.recompute_effective_state(
@@ -365,6 +401,16 @@ impl TerminalState {
})
}
+ fn should_ignore_detected_state_under_full_lifecycle_hook(
+ &self,
+ detected_agent: Option<Agent>,
+ process_exited: bool,
+ ) -> bool {
+ self.live_full_lifecycle_hook_authority()
+ && !process_exited
+ && !self.hook_authority_conflicts_with_detected_agent(detected_agent)
+ }
+
fn persisted_agent_session_conflicts_with_detected_agent(
&self,
detected_agent: Option<Agent>,
@@ -397,6 +443,113 @@ impl TerminalState {
.is_some_and(|session| session.source == source && session.agent == agent)
}
+ fn suppress_current_full_lifecycle_hook_authority(&mut self) {
+ if let Some((source, agent_label)) = self
+ .hook_authority
+ .as_ref()
+ .filter(|authority| {
+ crate::agent_detection_policy::full_lifecycle_hook_authority(
+ &authority.source,
+ &authority.agent_label,
+ )
+ })
+ .map(|authority| (authority.source.clone(), authority.agent_label.clone()))
+ {
+ let session_ref = self
+ .hook_authority
+ .as_ref()
+ .and_then(|authority| authority.session_ref.clone());
+ self.suppressed_full_lifecycle_hook_reports.insert(
+ source,
+ SuppressedFullLifecycleHookReport {
+ agent_label,
+ session_ref,
+ },
+ );
+ }
+ }
+
+ fn suppress_full_lifecycle_hook_report(&mut self, source: &str, agent_label: &str) {
+ if crate::agent_detection_policy::full_lifecycle_hook_authority(source, agent_label) {
+ self.suppressed_full_lifecycle_hook_reports.insert(
+ source.to_string(),
+ SuppressedFullLifecycleHookReport {
+ agent_label: agent_label.to_string(),
+ session_ref: self
+ .hook_authority
+ .as_ref()
+ .and_then(|authority| authority.session_ref.clone()),
+ },
+ );
+ }
+ }
+
+ fn full_lifecycle_hook_report_is_suppressed(
+ &self,
+ source: &str,
+ agent_label: &str,
+ session_ref: &Option<crate::agent_resume::AgentSessionRef>,
+ ) -> bool {
+ if !crate::agent_detection_policy::full_lifecycle_hook_authority(source, agent_label) {
+ return false;
+ }
+ self.suppressed_full_lifecycle_hook_reports
+ .get(source)
+ .is_some_and(|suppressed| {
+ if suppressed.agent_label != agent_label {
+ return false;
+ }
+ match (&suppressed.session_ref, session_ref) {
+ (Some(suppressed_ref), Some(incoming_ref)) => incoming_ref == suppressed_ref,
+ (Some(_), None) => true,
+ (None, Some(_)) => false,
+ (None, None) => true,
+ }
+ })
+ }
+
+ fn live_full_lifecycle_hook_authority_conflicts_with_session(
+ &self,
+ source: &str,
+ agent_label: &str,
+ session_ref: &Option<crate::agent_resume::AgentSessionRef>,
+ ) -> bool {
+ let Some(authority) = self.hook_authority.as_ref() else {
+ return false;
+ };
+ if !crate::agent_detection_policy::full_lifecycle_hook_authority(
+ &authority.source,
+ &authority.agent_label,
+ ) {
+ return false;
+ }
+ if authority.source != source || authority.agent_label != agent_label {
+ return false;
+ }
+ authority
+ .session_ref
+ .as_ref()
+ .zip(session_ref.as_ref())
+ .is_some_and(|(current, incoming)| current != incoming)
+ }
+
+ fn clear_full_lifecycle_hook_suppression_for_detected_agent(
+ &mut self,
+ previous_detected_agent: Option<Agent>,
+ detected_agent: Option<Agent>,
+ ) {
+ let Some(detected_agent) = detected_agent else {
+ return;
+ };
+ if previous_detected_agent == Some(detected_agent) {
+ return;
+ }
+ self.suppressed_full_lifecycle_hook_reports
+ .retain(|_, agent_label| {
+ crate::detect::parse_agent_label(&agent_label.agent_label) != Some(detected_agent)
+ });
+ }
+
fn current_session_identity_for_persistence(
&self,
) -> Option<(
@@ -551,8 +704,8 @@ impl TerminalState {
if !should_clear {
return None;
}
+ self.suppress_current_full_lifecycle_hook_authority();
self.hook_authority = None;
- self.stale_hook_idle_since = None;
self.persisted_agent_session = None;
Some(TerminalStateMutation {
effective_state_change: self.recompute_effective_state(
@@ -605,14 +758,12 @@ impl TerminalState {
let previous_state = self.state;
let previous_presentation = self.effective_presentation_for_state_at(previous_state, now);
let previous_session = self.current_session_identity_for_persistence();
+ self.suppress_full_lifecycle_hook_report(source, agent_label);
self.detected_agent = None;
self.fallback_state = AgentState::Unknown;
self.fallback_visible_blocker = false;
- self.fallback_visible_idle = false;
- self.fallback_visible_working = false;
self.fallback_observed_at = None;
self.hook_authority = None;
- self.stale_hook_idle_since = None;
self.persisted_agent_session = None;
Some(TerminalStateMutation {
effective_state_change: self.recompute_effective_state(
@@ -641,6 +792,9 @@ impl TerminalState {
}
fn visible_blocker_overrides_hook(&self) -> bool {
+ if self.live_full_lifecycle_hook_authority() {
+ return false;
+ }
self.fallback_visible_blocker
&& self.fallback_not_older_than_hook()
&& self.hook_authority.as_ref().is_some_and(|authority| {
@@ -650,61 +804,13 @@ impl TerminalState {
})
}
- fn visible_working_overrides_hook(&self) -> bool {
- self.fallback_visible_working
- && self.visible_working_is_fresh_enough_for_hook()
- && self.hook_authority.as_ref().is_some_and(|authority| {
- (authority.state == AgentState::Idle || authority.state == AgentState::Blocked)
- && crate::detect::parse_agent_label(&authority.agent_label)
- == self.detected_agent
- })
- }
-
- fn visible_working_is_fresh_enough_for_hook(&self) -> bool {
- self.fallback_not_older_than_hook()
- || self
- .fallback_observed_at
- .zip(
- self.hook_authority
- .as_ref()
- .map(|authority| authority.reported_at),
- )
- .is_some_and(|(observed_at, reported_at)| {
- reported_at >= observed_at
- && reported_at.duration_since(observed_at) < SCREEN_WORKING_HOLD
- })
- }
-
- fn visible_idle_stales_hook(&self, now: Instant) -> bool {
- self.stale_hook_idle_since
- .is_some_and(|since| now.duration_since(since) >= STALE_HOOK_IDLE_GRACE)
- }
-
- fn visible_idle_masks_hook_custom_status(&self, state: AgentState, now: Instant) -> bool {
- self.fallback_visible_idle
- && self.fallback_not_older_than_hook()
- && self.hook_authority.as_ref().is_some_and(|authority| {
- (authority.state == AgentState::Working || authority.state == AgentState::Blocked)
- && crate::detect::parse_agent_label(&authority.agent_label)
- == self.detected_agent
- })
- && (state == AgentState::Idle || self.visible_idle_stales_hook(now))
- }
-
- fn update_stale_hook_idle_window(&mut self, now: Instant) {
- let visible_idle_stales_hook = self.fallback_visible_idle
- && self.fallback_not_older_than_hook()
- && self.hook_authority.as_ref().is_some_and(|authority| {
- (authority.state == AgentState::Working || authority.state == AgentState::Blocked)
- && crate::detect::parse_agent_label(&authority.agent_label)
- == self.detected_agent
- });
-
- if visible_idle_stales_hook {
- self.stale_hook_idle_since.get_or_insert(now);
- } else {
- self.stale_hook_idle_since = None;
- }
+ fn live_full_lifecycle_hook_authority(&self) -> bool {
+ self.hook_authority.as_ref().is_some_and(|authority| {
+ crate::agent_detection_policy::full_lifecycle_hook_authority(
+ &authority.source,
+ &authority.agent_label,
+ )
+ })
}
pub fn set_manual_label(&mut self, label: String) {
@@ -729,13 +835,11 @@ impl TerminalState {
self.detected_agent = None;
self.fallback_state = AgentState::Unknown;
self.fallback_visible_blocker = false;
- self.fallback_visible_idle = false;
- self.fallback_visible_working = false;
self.fallback_observed_at = None;
- self.stale_hook_idle_since = None;
self.hook_authority = None;
self.persisted_agent_session = None;
self.agent_metadata.clear();
+ self.suppressed_full_lifecycle_hook_reports.clear();
self.state = AgentState::Unknown;
self.launch_argv = None;
self.respawn_shell_on_exit = false;
@@ -772,10 +876,6 @@ impl TerminalState {
) -> Option<EffectiveStateChange> {
let state = if self.visible_blocker_overrides_hook() {
AgentState::Blocked
- } else if self.visible_working_overrides_hook() {
- AgentState::Working
- } else if self.visible_idle_stales_hook(now) {
- AgentState::Idle
} else {
self.hook_authority
.as_ref()
@@ -809,55 +909,8 @@ impl TerminalState {
}
}
-pub(crate) fn stabilize_agent_state(
- agent: Option<Agent>,
- previous: AgentState,
- raw: AgentState,
- now: std::time::Instant,
- last_screen_working_at: &mut Option<std::time::Instant>,
-) -> AgentState {
- if !matches!(agent, Some(Agent::Claude | Agent::Droid)) {
- return raw;
- }
-
- match raw {
- AgentState::Working => {
- *last_screen_working_at = Some(now);
- AgentState::Working
- }
- AgentState::Blocked => AgentState::Blocked,
- AgentState::Idle if previous == AgentState::Working => {
- if last_screen_working_at
- .is_some_and(|last_working| now.duration_since(last_working) < SCREEN_WORKING_HOLD)
- {
- AgentState::Working
- } else {
- AgentState::Idle
- }
- }
- _ => raw,
- }
-}
-
-pub(crate) fn stabilize_agent_detection(
- agent: Option<Agent>,
- previous: AgentState,
- detection: crate::detect::AgentDetection,
- process_exited: bool,
- now: std::time::Instant,
- last_screen_working_at: &mut Option<std::time::Instant>,
-) -> AgentState {
- if process_exited {
- return detection.state;
- }
-
- stabilize_agent_state(
- agent,
- previous,
- detection.state,
- now,
- last_screen_working_at,
- )
+pub(crate) fn stabilize_agent_detection(detection: crate::detect::AgentDetection) -> AgentState {
+ detection.state
}
#[cfg(test)]
@@ -878,184 +931,367 @@ mod tests {
}
#[test]
- fn claude_working_is_sticky_for_short_gap() {
- let now = std::time::Instant::now();
- let mut last_working = None;
+ fn stabilization_uses_raw_policy_state() {
+ let detection = AgentDetection {
+ state: AgentState::Idle,
+ skip_state_update: false,
+ visible_blocker: false,
+ visible_working: false,
+ };
- let working = stabilize_agent_state(
- Some(Agent::Claude),
- AgentState::Idle,
- AgentState::Working,
- now,
- &mut last_working,
- );
- assert_eq!(working, AgentState::Working);
+ assert_eq!(stabilize_agent_detection(detection), AgentState::Idle);
+ }
- let still_working = stabilize_agent_state(
- Some(Agent::Claude),
+ #[test]
+ fn hook_authority_overrides_fallback_for_same_agent() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
- AgentState::Idle,
- now + std::time::Duration::from_millis(400),
- &mut last_working,
+ None,
+ None,
);
- assert_eq!(still_working, AgentState::Working);
+
+ assert_eq!(terminal.detected_agent, Some(Agent::Pi));
+ assert_eq!(terminal.fallback_state, AgentState::Idle);
+ assert_eq!(terminal.effective_agent_label(), Some("pi"));
+ assert_eq!(terminal.state, AgentState::Working);
}
#[test]
- fn claude_transitions_to_idle_after_hold_expires() {
- let now = std::time::Instant::now();
- let mut last_working = Some(now);
-
- let state = stabilize_agent_state(
- Some(Agent::Claude),
+ fn hook_authority_can_override_with_unknown_agent_label() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority(
+ "herdr:custom".into(),
+ "custom-agent".into(),
AgentState::Working,
- AgentState::Idle,
- now + SCREEN_WORKING_HOLD + std::time::Duration::from_millis(1),
- &mut last_working,
+ None,
+ None,
);
- assert_eq!(state, AgentState::Idle);
+
+ assert_eq!(terminal.detected_agent, Some(Agent::Pi));
+ assert_eq!(terminal.effective_agent_label(), Some("custom-agent"));
+ assert_eq!(terminal.effective_known_agent(), None);
+ assert_eq!(terminal.state, AgentState::Working);
}
#[test]
- fn process_exit_idle_bypasses_claude_working_hold() {
- let now = std::time::Instant::now();
- let mut last_working = Some(now);
-
- let state = stabilize_agent_detection(
- Some(Agent::Claude),
+ fn omp_hook_authority_works_without_detected_agent_variant() {
+ let mut terminal = test_terminal();
+ terminal.set_hook_authority(
+ "herdr:omp".into(),
+ "omp".into(),
AgentState::Working,
- AgentDetection {
- state: AgentState::Idle,
- skip_state_update: false,
- visible_blocker: false,
- visible_idle: false,
- visible_working: false,
- },
+ None,
+ None,
+ );
+
+ assert_eq!(terminal.detected_agent, None);
+ assert_eq!(terminal.effective_agent_label(), Some("omp"));
+ assert_eq!(terminal.effective_known_agent(), None);
+ assert_eq!(terminal.state, AgentState::Working);
+
+ let change = terminal.set_detected_state_with_visible_blocker(
+ None,
+ AgentState::Blocked,
true,
- now + std::time::Duration::from_millis(100),
- &mut last_working,
+ false,
+ false,
);
- assert_eq!(state, AgentState::Idle);
+ assert_eq!(terminal.fallback_state, AgentState::Unknown);
+ assert_eq!(terminal.state, AgentState::Working);
+ assert!(change.is_none());
}
#[test]
- fn visible_idle_does_not_bypass_claude_working_hold() {
- let now = std::time::Instant::now();
- let mut last_working = Some(now);
+ fn session_only_report_does_not_create_hook_authority() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Codex), AgentState::Idle);
- let state = stabilize_agent_detection(
- Some(Agent::Claude),
+ let mutation = terminal.set_agent_session_ref(
+ "herdr:codex".into(),
+ "codex".into(),
+ crate::agent_resume::AgentSessionRef::id("codex-session"),
+ Some(1),
+ );
+
+ assert!(mutation.is_some());
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Idle);
+
+ terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Codex),
AgentState::Working,
- AgentDetection {
- state: AgentState::Idle,
- skip_state_update: false,
- visible_blocker: false,
- visible_idle: true,
- visible_working: false,
- },
false,
- now + std::time::Duration::from_millis(100),
- &mut last_working,
+ false,
+ false,
+ false,
+ Instant::now(),
);
- assert_eq!(state, AgentState::Working);
+ assert_eq!(terminal.state, AgentState::Working);
}
#[test]
- fn droid_working_is_sticky_for_short_gap() {
- let now = std::time::Instant::now();
- let mut last_working = None;
+ fn process_exit_clears_matching_full_lifecycle_hook_authority() {
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
+ terminal.set_hook_authority_with_custom_status_at(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ None,
+ None,
+ Some(10),
+ now,
+ );
- let working = stabilize_agent_state(
- Some(Agent::Droid),
+ let change = terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Pi),
AgentState::Idle,
+ false,
+ true,
+ false,
+ true,
+ now + Duration::from_millis(1),
+ );
+
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Idle);
+ assert_eq!(
+ change.effective_state_change.unwrap().previous_state,
+ AgentState::Working
+ );
+
+ let stale = terminal.set_hook_authority_with_custom_status_at(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
- now,
- &mut last_working,
+ None,
+ None,
+ None,
+ Some(9),
+ now + Duration::from_millis(2),
);
- assert_eq!(working, AgentState::Working);
- let still_working = stabilize_agent_state(
- Some(Agent::Droid),
+ assert!(stale.is_none());
+ assert_eq!(terminal.state, AgentState::Idle);
+ }
+
+ #[test]
+ fn late_full_lifecycle_hook_after_process_exit_does_not_reacquire_authority() {
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
+ terminal.set_hook_authority_with_custom_status_at(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
+ None,
+ None,
+ None,
+ Some(20),
+ now,
+ );
+
+ terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Pi),
AgentState::Idle,
- now + std::time::Duration::from_millis(400),
- &mut last_working,
+ false,
+ true,
+ false,
+ true,
+ now + Duration::from_millis(1),
);
- assert_eq!(still_working, AgentState::Working);
+ let late = terminal.set_hook_authority_with_custom_status_at(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ Some("late".into()),
+ None,
+ Some(21),
+ now + Duration::from_millis(2),
+ );
+
+ assert!(late.is_none());
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Idle);
}
#[test]
- fn droid_transitions_to_idle_after_hold_expires() {
- let now = std::time::Instant::now();
- let mut last_working = Some(now);
-
- let state = stabilize_agent_state(
- Some(Agent::Droid),
+ fn late_full_lifecycle_hook_with_same_session_after_process_exit_does_not_reacquire_authority()
+ {
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ let session_path = test_session_path("pi.jsonl");
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
+ terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(session_path.clone()),
+ Some(20),
+ );
+
+ terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Pi),
AgentState::Idle,
- now + SCREEN_WORKING_HOLD + std::time::Duration::from_millis(1),
- &mut last_working,
+ false,
+ true,
+ false,
+ true,
+ now + Duration::from_millis(1),
);
- assert_eq!(state, AgentState::Idle);
+ let late = terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ Some("late".into()),
+ crate::agent_resume::AgentSessionRef::path(session_path),
+ Some(21),
+ );
+
+ assert!(late.is_none());
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Idle);
}
#[test]
- fn process_exit_idle_bypasses_droid_working_hold() {
- let now = std::time::Instant::now();
- let mut last_working = Some(now);
+ fn late_full_lifecycle_hook_after_release_does_not_reacquire_authority() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ Some(20),
+ );
- let state = stabilize_agent_detection(
- Some(Agent::Droid),
+ terminal.release_agent("herdr:pi", "pi", Some(21));
+ let late = terminal.set_hook_authority(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
- AgentDetection {
- state: AgentState::Idle,
- skip_state_update: false,
- visible_blocker: false,
- visible_idle: false,
- visible_working: false,
- },
- true,
- now + std::time::Duration::from_millis(100),
- &mut last_working,
+ None,
+ Some(22),
);
- assert_eq!(state, AgentState::Idle);
+ assert!(late.is_none());
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Unknown);
}
#[test]
- fn droid_blocked_bypasses_working_hold() {
- let now = std::time::Instant::now();
- let mut last_working = Some(now);
+ fn late_full_lifecycle_hook_with_same_session_after_release_does_not_reacquire_authority() {
+ let mut terminal = test_terminal();
+ let session_path = test_session_path("pi.jsonl");
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(session_path.clone()),
+ Some(20),
+ );
- let state = stabilize_agent_state(
- Some(Agent::Droid),
+ terminal.release_agent("herdr:pi", "pi", Some(21));
+ let late = terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
- AgentState::Blocked,
- now + std::time::Duration::from_millis(100),
- &mut last_working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(session_path),
+ Some(22),
);
- assert_eq!(state, AgentState::Blocked);
+
+ assert!(late.is_none());
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Unknown);
}
#[test]
- fn codex_states_are_unchanged_by_screen_working_hold() {
- let now = std::time::Instant::now();
- let mut last_working = None;
+ fn changed_session_ref_allows_full_lifecycle_hook_after_suppression() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(test_session_path("old.jsonl")),
+ Some(20),
+ );
+ terminal.release_agent("herdr:pi", "pi", Some(21));
- let state = stabilize_agent_state(
- Some(Agent::Codex),
+ let fresh = terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(test_session_path("new.jsonl")),
+ Some(22),
+ );
+
+ assert!(fresh.is_some());
+ assert!(terminal.hook_authority.is_some());
+ assert_eq!(terminal.state, AgentState::Working);
+ }
+
+ #[test]
+ fn live_full_lifecycle_hook_rejects_different_session_ref_for_same_source() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(test_session_path("one.jsonl")),
+ Some(20),
+ );
+
+ let mutation = terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Idle,
- now,
- &mut last_working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::path(test_session_path("two.jsonl")),
+ Some(21),
+ );
+
+ assert!(mutation.is_none());
+ assert_eq!(terminal.state, AgentState::Working);
+ assert_eq!(
+ terminal
+ .hook_authority
+ .as_ref()
+ .and_then(|authority| authority.session_ref.as_ref())
+ .map(|session_ref| session_ref.value.as_str()),
+ Some(test_session_path("one.jsonl").as_str())
);
- assert_eq!(state, AgentState::Idle);
}
#[test]
- fn hook_authority_overrides_fallback_for_same_agent() {
+ fn fresh_detected_process_allows_full_lifecycle_hook_after_suppression() {
+ let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
terminal.set_hook_authority(
@@ -1063,30 +1299,57 @@ mod tests {
"pi".into(),
AgentState::Working,
None,
+ Some(20),
+ );
+ terminal.release_agent("herdr:pi", "pi", Some(21));
+
+ terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Pi),
+ AgentState::Unknown,
+ false,
+ false,
+ false,
+ false,
+ now,
+ );
+ let fresh = terminal.set_hook_authority(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
None,
+ Some(22),
);
- assert_eq!(terminal.detected_agent, Some(Agent::Pi));
- assert_eq!(terminal.fallback_state, AgentState::Idle);
- assert_eq!(terminal.effective_agent_label(), Some("pi"));
+ assert!(fresh.is_some());
+ assert!(terminal.hook_authority.is_some());
assert_eq!(terminal.state, AgentState::Working);
}
#[test]
- fn hook_authority_can_override_with_unknown_agent_label() {
+ fn fresh_session_ref_allows_full_lifecycle_hook_after_suppression() {
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
terminal.set_hook_authority(
- "herdr:custom".into(),
- "custom-agent".into(),
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ Some(20),
+ );
+ terminal.release_agent("herdr:pi", "pi", Some(21));
+
+ let fresh = terminal.set_hook_authority_with_session_ref(
+ "herdr:pi".into(),
+ "pi".into(),
AgentState::Working,
None,
None,
+ crate::agent_resume::AgentSessionRef::id("fresh-session"),
+ Some(22),
);
- assert_eq!(terminal.detected_agent, Some(Agent::Pi));
- assert_eq!(terminal.effective_agent_label(), Some("custom-agent"));
- assert_eq!(terminal.effective_known_agent(), None);
+ assert!(fresh.is_some());
+ assert!(terminal.hook_authority.is_some());
assert_eq!(terminal.state, AgentState::Working);
}
@@ -1116,6 +1379,31 @@ mod tests {
}
#[test]
+ fn visible_blocker_does_not_override_full_lifecycle_hook_authority() {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ None,
+ );
+
+ let change = terminal.set_detected_state_with_visible_blocker(
+ Some(Agent::Pi),
+ AgentState::Blocked,
+ true,
+ false,
+ false,
+ );
+
+ assert_eq!(terminal.fallback_state, AgentState::Idle);
+ assert_eq!(terminal.state, AgentState::Working);
+ assert!(change.is_none());
+ }
+
+ #[test]
fn weak_blocked_fallback_does_not_override_hook_authority() {
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Codex), AgentState::Idle);
@@ -1214,18 +1502,10 @@ mod tests {
}
#[test]
- fn visible_idle_waits_before_overriding_claude_hook_working() {
+ fn fallback_idle_does_not_override_hook_working() {
let now = Instant::now();
let mut terminal = test_terminal();
- terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
- AgentState::Working,
- false,
- false,
- true,
- false,
- now,
- );
+ terminal.set_detected_state(Some(Agent::Claude), AgentState::Working);
terminal.set_hook_authority_with_custom_status_at(
"herdr:claude".into(),
"claude".into(),
@@ -1237,50 +1517,32 @@ mod tests {
now,
);
- let waiting = terminal.set_detected_state_with_screen_signals_at(
+ terminal.set_detected_state_with_screen_signals_at(
Some(Agent::Claude),
AgentState::Idle,
false,
true,
false,
false,
- now + Duration::from_millis(500),
+ now + Duration::from_secs(10),
);
- assert!(waiting.effective_state_change.is_none());
assert_eq!(terminal.fallback_state, AgentState::Idle);
assert_eq!(terminal.state, AgentState::Working);
assert_eq!(
terminal.effective_custom_status().as_deref(),
Some("thinking")
);
-
- let change = terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
- AgentState::Idle,
- false,
- true,
- false,
- false,
- now + Duration::from_millis(500) + STALE_HOOK_IDLE_GRACE + Duration::from_millis(1),
- );
-
- assert_eq!(terminal.state, AgentState::Idle);
- assert_eq!(terminal.effective_custom_status(), None);
- assert_eq!(
- change.effective_state_change.unwrap().previous_state,
- AgentState::Working
- );
}
#[test]
- fn fresh_hook_working_resets_visible_idle_stale_window() {
+ fn fallback_idle_does_not_override_full_lifecycle_hook_working() {
let now = Instant::now();
let mut terminal = test_terminal();
- terminal.set_detected_state(Some(Agent::Claude), AgentState::Working);
+ terminal.set_detected_state(Some(Agent::Kimi), AgentState::Working);
terminal.set_hook_authority_with_custom_status_at(
- "herdr:claude".into(),
- "claude".into(),
+ "herdr:kimi".into(),
+ "kimi".into(),
AgentState::Working,
None,
Some("thinking".into()),
@@ -1289,47 +1551,62 @@ mod tests {
now,
);
terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
+ Some(Agent::Kimi),
AgentState::Idle,
false,
true,
false,
false,
- now + Duration::from_millis(500),
+ now + Duration::from_secs(10),
+ );
+
+ assert_eq!(terminal.fallback_state, AgentState::Working);
+ assert_eq!(terminal.state, AgentState::Working);
+ assert_eq!(
+ terminal.effective_custom_status().as_deref(),
+ Some("thinking")
);
+ }
+ #[test]
+ fn visible_working_does_not_override_hook_idle_for_same_agent() {
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Claude), AgentState::Idle);
terminal.set_hook_authority_with_custom_status_at(
"herdr:claude".into(),
"claude".into(),
- AgentState::Working,
+ AgentState::Idle,
None,
- Some("thinking".into()),
None,
- Some(1),
- now + Duration::from_millis(800),
+ None,
+ None,
+ now,
);
+
let change = terminal.set_detected_state_with_screen_signals_at(
Some(Agent::Claude),
- AgentState::Idle,
+ AgentState::Working,
false,
- true,
false,
+ true,
false,
- now + STALE_HOOK_IDLE_GRACE + Duration::from_millis(1),
+ now + Duration::from_millis(1),
);
+ assert_eq!(terminal.fallback_state, AgentState::Working);
+ assert_eq!(terminal.state, AgentState::Idle);
assert!(change.effective_state_change.is_none());
- assert_eq!(terminal.state, AgentState::Working);
}
#[test]
- fn visible_working_overrides_hook_idle_for_same_agent() {
+ fn visible_working_does_not_override_full_lifecycle_hook_idle() {
let now = Instant::now();
let mut terminal = test_terminal();
- terminal.set_detected_state(Some(Agent::Claude), AgentState::Idle);
+ terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle);
terminal.set_hook_authority_with_custom_status_at(
- "herdr:claude".into(),
- "claude".into(),
+ "herdr:hermes".into(),
+ "hermes".into(),
AgentState::Idle,
None,
None,
@@ -1339,7 +1616,7 @@ mod tests {
);
let change = terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
+ Some(Agent::Hermes),
AgentState::Working,
false,
false,
@@ -1348,44 +1625,44 @@ mod tests {
now + Duration::from_millis(1),
);
- assert_eq!(terminal.state, AgentState::Working);
- assert_eq!(
- change.effective_state_change.unwrap().previous_state,
- AgentState::Idle
- );
+ assert_eq!(terminal.fallback_state, AgentState::Idle);
+ assert_eq!(terminal.state, AgentState::Idle);
+ assert!(change.effective_state_change.is_none());
}
#[test]
- fn recent_visible_working_holds_against_newer_claude_hook_idle() {
+ fn pty_working_fallback_is_ignored_under_full_lifecycle_hook_authority() {
let now = Instant::now();
let mut terminal = test_terminal();
- terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
- AgentState::Working,
- false,
- false,
- true,
- false,
- now,
- );
-
- let change = terminal.set_hook_authority_with_custom_status_at(
- "herdr:claude".into(),
- "claude".into(),
+ terminal.set_detected_state(Some(Agent::Qodercli), AgentState::Idle);
+ terminal.set_hook_authority_with_custom_status_at(
+ "herdr:qodercli".into(),
+ "qodercli".into(),
AgentState::Idle,
None,
None,
None,
None,
- now + Duration::from_millis(100),
+ now,
);
- assert!(change.unwrap().effective_state_change.is_none());
- assert_eq!(terminal.state, AgentState::Working);
+ let change = terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Qodercli),
+ AgentState::Working,
+ false,
+ false,
+ false,
+ false,
+ now + Duration::from_millis(1),
+ );
+
+ assert_eq!(terminal.fallback_state, AgentState::Idle);
+ assert_eq!(terminal.state, AgentState::Idle);
+ assert!(change.effective_state_change.is_none());
}
#[test]
- fn old_visible_working_does_not_hold_against_newer_claude_hook_idle() {
+ fn visible_working_does_not_hold_against_newer_claude_hook_idle() {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state_with_screen_signals_at(
@@ -1406,7 +1683,7 @@ mod tests {
None,
None,
None,
- now + SCREEN_WORKING_HOLD + Duration::from_millis(1),
+ now + Duration::from_millis(100),
);
assert_eq!(terminal.state, AgentState::Idle);
@@ -1421,7 +1698,7 @@ mod tests {
}
#[test]
- fn refreshed_visible_working_overrides_newer_hook_blocked() {
+ fn refreshed_visible_working_does_not_override_newer_hook_blocked() {
let now = Instant::now();
let mut terminal = test_terminal();
terminal.set_detected_state_with_screen_signals_at(
@@ -1441,7 +1718,7 @@ mod tests {
Some("permission".into()),
None,
None,
- now + SCREEN_WORKING_HOLD + Duration::from_millis(1),
+ now + Duration::from_millis(1201),
);
assert_eq!(terminal.state, AgentState::Blocked);
@@ -1453,71 +1730,20 @@ mod tests {
false,
true,
false,
- now + SCREEN_WORKING_HOLD + Duration::from_millis(800),
- );
-
- assert_eq!(terminal.state, AgentState::Working);
- assert_eq!(terminal.effective_custom_status(), None);
- assert_eq!(
- change.effective_state_change.unwrap().previous_state,
- AgentState::Blocked
- );
- }
-
- #[test]
- fn visible_idle_waits_before_overriding_claude_hook_blocked() {
- let now = Instant::now();
- let mut terminal = test_terminal();
- terminal.set_detected_state(Some(Agent::Claude), AgentState::Working);
- terminal.set_hook_authority_with_custom_status_at(
- "herdr:claude".into(),
- "claude".into(),
- AgentState::Blocked,
- None,
- Some("permission".into()),
- None,
- None,
- now,
- );
-
- let waiting = terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
- AgentState::Idle,
- false,
- true,
- false,
- false,
- now + Duration::from_millis(500),
+ now + Duration::from_millis(2000),
);
- assert!(waiting.effective_state_change.is_none());
- assert_eq!(terminal.fallback_state, AgentState::Idle);
+ assert_eq!(terminal.fallback_state, AgentState::Working);
assert_eq!(terminal.state, AgentState::Blocked);
assert_eq!(
terminal.effective_custom_status().as_deref(),
Some("permission")
);
-
- let change = terminal.set_detected_state_with_screen_signals_at(
- Some(Agent::Claude),
- AgentState::Idle,
- false,
- true,
- false,
- false,
- now + Duration::from_millis(500) + STALE_HOOK_IDLE_GRACE + Duration::from_millis(1),
- );
-
- assert_eq!(terminal.state, AgentState::Idle);
- assert_eq!(terminal.effective_custom_status(), None);
- assert_eq!(
- change.effective_state_change.unwrap().previous_state,
- AgentState::Blocked
- );
+ assert!(change.effective_state_change.is_none());
}
#[test]
- fn visible_idle_does_not_override_other_agent_hook_working() {
+ fn fallback_idle_does_not_override_other_agent_hook_working() {
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Codex), AgentState::Working);
terminal.set_hook_authority(
@@ -1620,6 +1846,39 @@ mod tests {
}
#[test]
+ fn full_lifecycle_hook_authority_ignores_detected_agent_clear_without_process_exit() {
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
+ terminal.set_hook_authority_with_custom_status_at(
+ "herdr:pi".into(),
+ "pi".into(),
+ AgentState::Working,
+ None,
+ None,
+ None,
+ None,
+ now,
+ );
+
+ let change = terminal.set_detected_state_with_screen_signals_at(
+ None,
+ AgentState::Unknown,
+ false,
+ false,
+ false,
+ false,
+ now + Duration::from_millis(1),
+ );
+
+ assert!(terminal.hook_authority.is_some());
+ assert_eq!(terminal.detected_agent, Some(Agent::Pi));
+ assert_eq!(terminal.fallback_state, AgentState::Idle);
+ assert_eq!(terminal.state, AgentState::Working);
+ assert!(change.effective_state_change.is_none());
+ }
+
+ #[test]
fn detected_agent_clear_clears_matching_hook_authority() {
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::OpenCode), AgentState::Idle);
@@ -1721,7 +1980,6 @@ mod tests {
);
assert_eq!(terminal.state, AgentState::Working);
- assert!(terminal.stale_hook_idle_since.is_none());
}
#[test]
@@ -2202,7 +2460,7 @@ mod tests {
}
#[test]
- fn detected_agent_disappearance_clears_matching_hook_session_ref() {
+ fn detected_agent_disappearance_does_not_clear_full_lifecycle_hook_session_ref() {
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle);
terminal.set_hook_authority_with_session_ref(
@@ -2217,10 +2475,10 @@ mod tests {
let mutation = terminal.set_detected_state_with_mutation(None, AgentState::Unknown);
- assert!(mutation.session_ref_changed);
- assert!(terminal.hook_authority.is_none());
+ assert!(!mutation.session_ref_changed);
+ assert!(terminal.hook_authority.is_some());
assert!(terminal.persisted_agent_session.is_none());
- assert_eq!(terminal.effective_agent_label(), None);
+ assert_eq!(terminal.effective_agent_label(), Some("hermes"));
}
#[test]