diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/agent_resume.rs | 2 | ||||
| -rw-r--r-- | src/detect/mod.rs | 2 | ||||
| -rw-r--r-- | src/handoff_runtime.rs | 2 | ||||
| -rw-r--r-- | src/pane.rs | 2 | ||||
| -rw-r--r-- | src/persist/restore.rs | 110 | ||||
| -rw-r--r-- | src/server/headless/lifecycle.rs | 6 | ||||
| -rw-r--r-- | src/terminal/state.rs | 36 |
7 files changed, 157 insertions, 3 deletions
diff --git a/src/agent_resume.rs b/src/agent_resume.rs index 04e6cf63..56554dd3 100644 --- a/src/agent_resume.rs +++ b/src/agent_resume.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; const MAX_SESSION_ID_LEN: usize = 512; const MAX_SESSION_PATH_LEN: usize = 4096; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AgentSessionRef { pub kind: AgentSessionRefKind, pub value: String, diff --git a/src/detect/mod.rs b/src/detect/mod.rs index 4ed32344..7fe15d11 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -7,7 +7,7 @@ pub mod manifest; pub mod manifest_update; /// The detected state of a terminal pane. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum AgentState { /// Agent finished, prompt visible, nothing happening. Idle, diff --git a/src/handoff_runtime.rs b/src/handoff_runtime.rs index 29ba85f4..12481f6d 100644 --- a/src/handoff_runtime.rs +++ b/src/handoff_runtime.rs @@ -27,6 +27,8 @@ pub(crate) struct HandoffRuntimeState { pub terminal_title: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_history_ansi: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_state: Option<crate::terminal::state::HandoffAgentState>, } #[cfg(unix)] diff --git a/src/pane.rs b/src/pane.rs index 9d75c21c..118dd3a4 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -2088,6 +2088,7 @@ impl PaneRuntime { input_state: self.input_state(), terminal_title: self.terminal_title(), initial_history_ansi: None, + agent_state: None, } } @@ -2298,6 +2299,7 @@ impl PaneRuntime { input_state, terminal_title, initial_history_ansi, + agent_state: _, } = state; let pane_id = PaneId::from_raw(pane_id); use std::os::fd::FromRawFd; diff --git a/src/persist/restore.rs b/src/persist/restore.rs index 31ee0a37..265b4874 100644 --- a/src/persist/restore.rs +++ b/src/persist/restore.rs @@ -561,6 +561,10 @@ fn restore_tab( .unwrap_or_default(); let imported_runtime = old_pane_id.and_then(|old_id| imported_panes.remove(&old_id)); let was_imported = imported_runtime.is_some(); + #[cfg(unix)] + let handoff_agent_state = imported_runtime + .as_ref() + .and_then(|imported| imported.state.agent_state.clone()); let pending_native_agent_restore = if was_imported { None } else { @@ -693,6 +697,10 @@ fn restore_tab( std::time::Instant::now(), ); } + #[cfg(unix)] + if let Some(agent_state) = handoff_agent_state { + terminal.restore_handoff_agent_state(agent_state); + } panes.insert(*id, PaneState::new(terminal_id.clone())); terminal_runtimes.insert(terminal_id, runtime); terminals.push(terminal); @@ -1709,6 +1717,108 @@ mod tests { } #[tokio::test] + #[cfg(unix)] + async fn live_handoff_preserves_hook_status_until_next_report() { + for state_before_handoff in [AgentState::Working, AgentState::Blocked] { + let (snapshot, _) = snapshot_with_saved_pane_history(); + let (events, _events_rx) = mpsc::channel(32); + let (workspaces, mut terminals, runtimes) = restore( + &snapshot, + None, + 24, + 80, + 4096, + test_restore_shell(), + crate::config::ShellModeConfig::NonLogin, + false, + events.clone(), + Arc::new(Notify::new()), + Arc::new(RenderSignal::new()), + ); + let terminal = terminals.values_mut().next().unwrap(); + terminal + .set_detected_agent_process_at(crate::detect::Agent::Pi, std::time::Instant::now()); + terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession { + source: "herdr:pi".into(), + agent: "pi".into(), + session_ref: crate::agent_resume::AgentSessionRef::path( + "/var/tmp/handoff-test.jsonl", + ) + .unwrap(), + }); + terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + state_before_handoff, + None, + Some( + crate::agent_resume::AgentSessionRef::path("/var/tmp/handoff-test.jsonl") + .unwrap(), + ), + Some(1), + ); + assert_eq!(terminal.state, state_before_handoff); + let runtimes = crate::terminal::TerminalRuntimeRegistry::from(runtimes); + let snapshot = crate::persist::capture(&workspaces, &terminals, &runtimes, Some(0), 0); + let pane_id = workspaces[0].tabs[0].panes.keys().next().copied().unwrap(); + let runtime = runtimes.values().next().unwrap(); + runtime + .pause_handoff_reader(std::time::Duration::from_secs(2)) + .unwrap(); + let mut state = runtime.handoff_runtime_state(pane_id.raw()); + state.agent_state = terminals.values().next().unwrap().handoff_agent_state(); + let state = serde_json::from_value(serde_json::to_value(state).unwrap()).unwrap(); + let mut imports = HashMap::from([( + pane_id.raw(), + crate::handoff_runtime::ImportedHandoffRuntime { + master_fd: runtime.duplicate_handoff_fd().unwrap(), + state, + }, + )]); + let (_, mut restored_terminals, restored_runtimes) = restore_handoff( + &snapshot, + 4096, + test_restore_shell(), + crate::config::ShellModeConfig::NonLogin, + &mut imports, + events, + Arc::new(Notify::new()), + Arc::new(RenderSignal::new()), + ) + .unwrap(); + drop(restored_runtimes); + drop(runtimes); + let terminal = restored_terminals.values_mut().next().unwrap(); + assert_eq!(terminal.state, state_before_handoff); + terminal.set_detected_state(Some(crate::detect::Agent::Pi), AgentState::Idle); + assert_eq!( + terminal.state, state_before_handoff, + "screen fallback must not erase the transferred hook status" + ); + terminal.set_hook_authority_with_session_ref( + "herdr:pi".into(), + "pi".into(), + AgentState::Idle, + None, + Some( + crate::agent_resume::AgentSessionRef::path("/var/tmp/handoff-test.jsonl") + .unwrap(), + ), + Some(2), + ); + assert_eq!( + terminal.state, + AgentState::Idle, + "the next hook report must take effect immediately" + ); + assert_eq!( + terminal.finish_agent_process_acquisition(), + state_before_handoff == AgentState::Blocked + ); + } + } + + #[tokio::test] async fn restore_seeds_saved_pane_history_into_runtime() { let (snapshot, history) = snapshot_with_saved_pane_history(); let (events, _events_rx) = mpsc::channel(8); diff --git a/src/server/headless/lifecycle.rs b/src/server/headless/lifecycle.rs index f7ad87dd..c700a9d9 100644 --- a/src/server/headless/lifecycle.rs +++ b/src/server/headless/lifecycle.rs @@ -83,6 +83,12 @@ impl HeadlessServer { continue; }; let mut handoff_runtime = runtime.handoff_runtime_state(pane_id); + handoff_runtime.agent_state = self + .app + .state + .terminals + .get(terminal_id) + .and_then(|terminal| terminal.handoff_agent_state()); let has_agent_session = self .app .state diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 119916c8..fadd6e10 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -14,16 +14,25 @@ use crate::terminal::TerminalId; mod metadata; pub use metadata::{AgentMetadata, AgentMetadataReport, EffectivePresentation}; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct HookAuthority { pub source: String, pub agent_label: String, pub state: AgentState, pub message: Option<String>, + #[serde(skip, default = "Instant::now")] pub reported_at: Instant, pub session_ref: Option<crate::agent_resume::AgentSessionRef>, } +#[cfg(unix)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct HandoffAgentState { + authority: HookAuthority, + sequence: Option<u64>, + acquisition_pending: bool, +} + #[derive(Debug, Clone, PartialEq, Eq)] struct SuppressedFullLifecycleHookReport { agent_label: String, @@ -213,6 +222,31 @@ impl TerminalState { mutation } + #[cfg(unix)] + pub(crate) fn handoff_agent_state(&self) -> Option<HandoffAgentState> { + if !self.live_full_lifecycle_hook_authority() { + return None; + } + let authority = self.hook_authority.as_ref()?; + Some(HandoffAgentState { + authority: authority.clone(), + sequence: self.hook_report_sequences.get(&authority.source).copied(), + acquisition_pending: self.agent_process_acquisition_pending, + }) + } + + #[cfg(unix)] + pub(crate) fn restore_handoff_agent_state(&mut self, snapshot: HandoffAgentState) { + if let Some(sequence) = snapshot.sequence { + self.hook_report_sequences + .insert(snapshot.authority.source.clone(), sequence); + } + self.detected_agent = crate::detect::parse_agent_label(&snapshot.authority.agent_label); + self.state = snapshot.authority.state; + self.hook_authority = Some(snapshot.authority); + self.agent_process_acquisition_pending = snapshot.acquisition_pending; + } + pub(crate) fn finish_agent_process_acquisition(&mut self) -> bool { let reached_idle = self.agent_process_acquisition_pending && self.state == AgentState::Idle; let suppress_completion = reached_idle && self.recent_agent_process_exit.is_none(); |
