aboutsummaryrefslogtreecommitdiff
path: root/src/terminal
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-24 02:23:42 +0200
committerGitHub <noreply@github.com>2026-09-24 02:23:42 +0200
commit9c96f7ddb3be2cc575a159d4d1f1d49fb10d7006 (patch)
tree182e89183981160397af301c075ab11316b15850 /src/terminal
parent21fd121a63db53ff44b947bbe38c47bbf35e632c (diff)
fix: avoid inferring codex idle from terminal output (#4563)
refs #4507
Diffstat (limited to 'src/terminal')
-rw-r--r--src/terminal/state.rs82
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();