diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-10 15:24:24 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-10 15:33:45 +0300 |
| commit | 36a1b7f86444871f0c38ff36510a6e3c2eec48c6 (patch) | |
| tree | 4a2d100eef7f26af7feaa93ce661423c3994c54e /src/terminal | |
| parent | 8d13492926829c5cf0ea42c8613344cd65ff59fa (diff) | |
feat: distribute agent detection manifests
Diffstat (limited to 'src/terminal')
| -rw-r--r-- | src/terminal/runtime.rs | 19 | ||||
| -rw-r--r-- | src/terminal/state.rs | 128 |
2 files changed, 134 insertions, 13 deletions
diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs index 5d1ce1d7..727e9929 100644 --- a/src/terminal/runtime.rs +++ b/src/terminal/runtime.rs @@ -197,6 +197,21 @@ impl TerminalRuntime { self.0.begin_graceful_release(agent); } + pub fn reset_agent_detection(&self) { + self.0.reset_agent_detection(); + } + + #[cfg(test)] + pub(crate) fn agent_detection_reset_notify_for_test( + &self, + ) -> std::sync::Arc<tokio::sync::Notify> { + self.0.agent_detection_reset_notify_for_test() + } + + pub fn set_full_lifecycle_authority_active(&self, active: bool) { + self.0.set_full_lifecycle_authority_active(active); + } + pub fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) { self.0.resize(rows, cols, cell_width_px, cell_height_px); } @@ -250,6 +265,10 @@ impl TerminalRuntime { self.0.visible_ansi() } + pub fn detection_text(&self) -> String { + self.0.detection_text() + } + pub fn recent_text(&self, lines: usize) -> String { self.0.recent_text(lines) } diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 74b7973d..25a48e5f 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -31,6 +31,7 @@ pub struct HookAuthority { struct SuppressedFullLifecycleHookReport { agent_label: String, session_ref: Option<crate::agent_resume::AgentSessionRef>, + observed_at: Instant, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -173,7 +174,7 @@ impl TerminalState { agent: Option<Agent>, fallback_state: AgentState, visible_blocker: bool, - _ignored_screen_idle: bool, + _visible_idle: bool, _visible_working: bool, process_exited: bool, now: Instant, @@ -205,6 +206,19 @@ impl TerminalState { != self.current_session_identity_for_persistence(), }; } + if !process_exited && self.detected_state_observed_before_release_suppression(agent, now) { + 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( @@ -448,7 +462,7 @@ impl TerminalState { .hook_authority .as_ref() .filter(|authority| { - crate::agent_detection_policy::full_lifecycle_hook_authority( + crate::detect::full_lifecycle_hook_authority( &authority.source, &authority.agent_label, ) @@ -464,13 +478,14 @@ impl TerminalState { SuppressedFullLifecycleHookReport { agent_label, session_ref, + observed_at: Instant::now(), }, ); } } 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) { + if crate::detect::full_lifecycle_hook_authority(source, agent_label) { self.suppressed_full_lifecycle_hook_reports.insert( source.to_string(), SuppressedFullLifecycleHookReport { @@ -479,6 +494,7 @@ impl TerminalState { .hook_authority .as_ref() .and_then(|authority| authority.session_ref.clone()), + observed_at: Instant::now(), }, ); } @@ -490,7 +506,7 @@ impl TerminalState { agent_label: &str, session_ref: &Option<crate::agent_resume::AgentSessionRef>, ) -> bool { - if !crate::agent_detection_policy::full_lifecycle_hook_authority(source, agent_label) { + if !crate::detect::full_lifecycle_hook_authority(source, agent_label) { return false; } self.suppressed_full_lifecycle_hook_reports @@ -517,10 +533,8 @@ impl TerminalState { 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, - ) { + if !crate::detect::full_lifecycle_hook_authority(&authority.source, &authority.agent_label) + { return false; } if authority.source != source || authority.agent_label != agent_label { @@ -550,6 +564,22 @@ impl TerminalState { }); } + fn detected_state_observed_before_release_suppression( + &self, + detected_agent: Option<Agent>, + observed_at: Instant, + ) -> bool { + let Some(detected_agent) = detected_agent else { + return false; + }; + self.suppressed_full_lifecycle_hook_reports + .values() + .any(|suppressed| { + crate::detect::parse_agent_label(&suppressed.agent_label) == Some(detected_agent) + && observed_at <= suppressed.observed_at + }) + } + fn current_session_identity_for_persistence( &self, ) -> Option<( @@ -791,6 +821,10 @@ impl TerminalState { self.detected_agent } + pub fn full_lifecycle_hook_authority_active(&self) -> bool { + self.live_full_lifecycle_hook_authority() + } + fn visible_blocker_overrides_hook(&self) -> bool { if self.live_full_lifecycle_hook_authority() { return false; @@ -806,10 +840,7 @@ impl TerminalState { 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, - ) + crate::detect::full_lifecycle_hook_authority(&authority.source, &authority.agent_label) }) } @@ -935,6 +966,7 @@ mod tests { let detection = AgentDetection { state: AgentState::Idle, skip_state_update: false, + visible_idle: false, visible_blocker: false, visible_working: false, }; @@ -1085,6 +1117,39 @@ mod tests { } #[test] + fn process_exit_clears_omp_full_lifecycle_hook_authority_without_known_agent() { + let now = Instant::now(); + let mut terminal = test_terminal(); + terminal.set_hook_authority_with_custom_status_at( + "herdr:omp".into(), + "omp".into(), + AgentState::Working, + None, + None, + None, + Some(10), + now, + ); + + let change = terminal.set_detected_state_with_screen_signals_at( + None, + 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 + ); + } + + #[test] fn late_full_lifecycle_hook_after_process_exit_does_not_reacquire_authority() { let now = Instant::now(); let mut terminal = test_terminal(); @@ -1291,7 +1356,6 @@ mod tests { #[test] 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( @@ -1302,6 +1366,7 @@ mod tests { Some(20), ); terminal.release_agent("herdr:pi", "pi", Some(21)); + let now = Instant::now(); terminal.set_detected_state_with_screen_signals_at( Some(Agent::Pi), @@ -1326,6 +1391,43 @@ mod tests { } #[test] + fn release_suppression_ignores_same_agent_idle_publish() { + let now = Instant::now(); + 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), + ); + terminal.release_agent("herdr:pi", "pi", Some(21)); + + let change = terminal.set_detected_state_with_screen_signals_at( + Some(Agent::Pi), + AgentState::Idle, + false, + true, + false, + false, + now, + ); + let late = terminal.set_hook_authority( + "herdr:pi".into(), + "pi".into(), + AgentState::Working, + None, + Some(22), + ); + + assert!(change.effective_state_change.is_none()); + assert!(late.is_none()); + assert_eq!(terminal.detected_agent, None); + assert_eq!(terminal.state, AgentState::Unknown); + } + + #[test] fn fresh_session_ref_allows_full_lifecycle_hook_after_suppression() { let mut terminal = test_terminal(); terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle); |
