diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-09-24 02:23:42 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-24 02:23:42 +0200 |
| commit | 9c96f7ddb3be2cc575a159d4d1f1d49fb10d7006 (patch) | |
| tree | 182e89183981160397af301c075ab11316b15850 /src/terminal | |
| parent | 21fd121a63db53ff44b947bbe38c47bbf35e632c (diff) | |
fix: avoid inferring codex idle from terminal output (#4563)
refs #4507
Diffstat (limited to 'src/terminal')
| -rw-r--r-- | src/terminal/state.rs | 82 |
1 files changed, 80 insertions, 2 deletions
diff --git a/src/terminal/state.rs b/src/terminal/state.rs index fadd6e10..4a5abb12 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -142,6 +142,7 @@ pub struct TerminalState { pub agent_name: Option<String>, agent_name_owner: Option<AgentNameOwner>, managed_agent: Option<ManagedAgent>, + codex_prompt_ready: bool, managed_agent_launch_session: Option<crate::agent_resume::PersistedAgentSession>, hook_report_sequences: HashMap<String, u64>, suppressed_full_lifecycle_hook_reports: HashMap<String, SuppressedFullLifecycleHookReport>, @@ -179,6 +180,7 @@ impl TerminalState { agent_name: None, agent_name_owner: None, managed_agent: None, + codex_prompt_ready: false, managed_agent_launch_session: None, hook_report_sequences: HashMap::new(), suppressed_full_lifecycle_hook_reports: HashMap::new(), @@ -217,6 +219,7 @@ impl TerminalState { now, ); if starts_acquisition { + self.codex_prompt_ready = false; self.agent_process_acquisition_pending = true; } mutation @@ -419,6 +422,9 @@ impl TerminalState { }; } self.detected_agent = agent; + if process_exited || agent != Some(Agent::Codex) || fallback_state == AgentState::Blocked { + self.codex_prompt_ready = false; + } if let Some(agent) = agent { let agent_label = crate::detect::agent_label(agent); self.reconcile_agent_name_owner(agent_label, None); @@ -1972,6 +1978,7 @@ impl TerminalState { settle_delay: Duration, timeout: Duration, ) { + self.codex_prompt_ready = false; self.set_agent_name(name); self.agent_process_acquisition_pending = true; self.agent_name_owner = Some(AgentNameOwner { @@ -2002,6 +2009,20 @@ impl TerminalState { .is_some_and(|managed| matches!(managed.phase, ManagedAgentPhase::Active)) } + pub fn observe_codex_prompt_ready(&mut self, ready: bool) -> Option<TerminalStateMutation> { + if self.detected_agent != Some(Agent::Codex) + || self.recent_agent_process_exit.is_some() + || !self.managed_agent.is_some_and(|managed| { + managed.kind == Agent::Codex && managed.phase != ManagedAgentPhase::Active + }) + || self.codex_prompt_ready == ready + { + return None; + } + self.codex_prompt_ready = ready; + Some(TerminalStateMutation::default()) + } + pub fn managed_agent_kind(&self) -> Option<Agent> { self.managed_agent.map(|managed| managed.kind) } @@ -2039,7 +2060,12 @@ impl TerminalState { return true; } if managed.phase == ManagedAgentPhase::Blocked { - if known_agent == Some(managed.kind) && self.state == AgentState::Idle { + if known_agent == Some(managed.kind) + && (self.state == AgentState::Idle + || managed.kind == Agent::Codex + && self.state == AgentState::Unknown + && self.codex_prompt_ready) + { self.managed_agent = Some(ManagedAgent { kind: managed.kind, phase: ManagedAgentPhase::Active, @@ -2067,7 +2093,12 @@ impl TerminalState { return true; } if ready_after.is_none_or(|ready_after| now >= ready_after) { - if known_agent == Some(managed.kind) && self.state == AgentState::Idle { + if known_agent == Some(managed.kind) + && (self.state == AgentState::Idle + || managed.kind == Agent::Codex + && self.state == AgentState::Unknown + && self.codex_prompt_ready) + { self.managed_agent = Some(ManagedAgent { kind: managed.kind, phase: ManagedAgentPhase::Active, @@ -2115,6 +2146,7 @@ impl TerminalState { } pub fn clear_agent_name(&mut self) { + self.codex_prompt_ready = false; if self .managed_agent_launch_session .take() @@ -2329,6 +2361,52 @@ mod tests { } #[test] + fn codex_managed_readiness_requires_current_prompt_without_idle() { + let now = Instant::now(); + let mut terminal = test_terminal(); + terminal.begin_managed_agent( + "reviewer".into(), + Agent::Codex, + now, + Duration::from_millis(100), + Duration::from_secs(1), + ); + terminal.set_detected_state(Some(Agent::Codex), AgentState::Unknown); + terminal.observe_codex_prompt_ready(true); + terminal.reconcile_managed_agent_at(now, false); + assert!(!terminal.managed_agent_interactive_ready()); + terminal.observe_codex_prompt_ready(false); + assert!(terminal.reconcile_managed_agent_at(now + Duration::from_millis(100), false)); + assert!(!terminal.managed_agent_interactive_ready()); + + terminal.observe_codex_prompt_ready(true); + assert!(terminal.reconcile_managed_agent_at(now + Duration::from_millis(101), false)); + assert!(terminal.managed_agent_interactive_ready()); + assert_eq!(terminal.state, AgentState::Unknown); + terminal.observe_codex_prompt_ready(false); + assert!(terminal.managed_agent_interactive_ready()); + + terminal.begin_managed_agent( + "next".into(), + Agent::Codex, + now, + Duration::ZERO, + Duration::from_secs(1), + ); + terminal.reconcile_managed_agent_at(now, false); + assert!(!terminal.managed_agent_interactive_ready()); + + terminal.set_detected_state(Some(Agent::Codex), AgentState::Blocked); + assert!(terminal.reconcile_managed_agent_at(now, false)); + terminal.observe_codex_prompt_ready(false); + terminal.set_detected_state(Some(Agent::Codex), AgentState::Unknown); + assert!(!terminal.reconcile_managed_agent_at(now, false)); + terminal.observe_codex_prompt_ready(true); + assert!(terminal.reconcile_managed_agent_at(now, false)); + assert!(terminal.managed_agent_interactive_ready()); + } + + #[test] fn managed_agent_mismatch_and_timeout_release_name() { let now = Instant::now(); let mut mismatch = test_terminal(); |
