aboutsummaryrefslogtreecommitdiff
path: root/src/terminal/state.rs
diff options
context:
space:
mode:
authorcaner-akca <94343893+caner-akca@users.noreply.github.com>2026-09-13 20:00:57 +0300
committerGitHub <noreply@github.com>2026-09-13 19:00:57 +0200
commitf0cb0e0683953e7dbc32d911268931f25d3e448f (patch)
tree070f7332285f6028cab9cb2b8a1ef5203ceb15e3 /src/terminal/state.rs
parentff32bd450eb590c4134f8412e98315919cf896a4 (diff)
fix: keep an agent name through a process-exit observation (#3574)
A process-exit observation is not proof the agent is gone: the same observation can be wrong while the agent keeps running, and the name is the only handle its owner has on the pane. Detection uncertainty already preserves the name, so releasing it on the observation alone contradicts that contract and leaves a live agent unreachable by name with a rename as the only recovery. Free the name at the point the agent actually leaves the pane - a recorded exit with no agent detected any more - so a wrong observation costs nothing and a real exit still releases the name for reuse. refs #3225 Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/terminal/state.rs')
-rw-r--r--src/terminal/state.rs95
1 files changed, 94 insertions, 1 deletions
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index a7c84574..7ceaede7 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -574,7 +574,12 @@ impl TerminalState {
self.hook_authority = None;
self.persisted_agent_session = durable_session;
}
- if agent_released {
+ // Observing a process exit is not the same as the agent being gone: the
+ // observation can be wrong while the agent keeps running, and the name
+ // is the only handle its owner has on the pane. Detection uncertainty
+ // already keeps the name, so free it at the point the agent actually
+ // leaves the pane - a recorded exit with no agent detected any more.
+ if agent.is_none() && self.recent_agent_process_exit.is_some() {
self.clear_agent_name();
}
TerminalStateMutation {
@@ -5549,6 +5554,94 @@ mod tests {
}
#[test]
+ fn a_process_exit_observation_alone_does_not_free_the_name() {
+ // The pane keeps reporting the same agent throughout: this models a
+ // process-exit observation that is wrong (the agent is still running),
+ // which is what `agent_alias_survives_detection_uncertainty_...`
+ // already refuses to let destroy the name through the detection path.
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
+ terminal.set_agent_name("reviewer".into());
+
+ // Contract today: losing detection does not cost the pane its name.
+ terminal.set_detected_state(None, AgentState::Unknown);
+ assert_eq!(
+ terminal.agent_name.as_deref(),
+ Some("reviewer"),
+ "detection uncertainty must not release the name"
+ );
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
+
+ // A single process-exit observation for that same agent destroys it.
+ let exit = terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Pi),
+ AgentState::Idle,
+ false,
+ false,
+ false,
+ true,
+ now,
+ );
+ assert!(exit.agent_released);
+ assert_eq!(
+ terminal.agent_name.as_deref(),
+ Some("reviewer"),
+ "a process-exit observation for the still-detected agent must not \
+ destroy a name that detection uncertainty is allowed to keep"
+ );
+
+ // And the agent proving it is alive again must not leave the pane
+ // permanently unreachable by the name its owner assigned.
+ terminal.set_detected_agent_process_at(Agent::Pi, now + Duration::from_secs(1));
+ assert_eq!(
+ terminal.agent_name.as_deref(),
+ Some("reviewer"),
+ "the name must still resolve once the agent is observed alive again"
+ );
+ assert_eq!(terminal.detected_agent, Some(Agent::Pi));
+ }
+
+ #[test]
+ fn a_confirmed_agent_exit_still_frees_the_name_for_reuse() {
+ // The other side of `a_process_exit_observation_alone_does_not_free_the_name`:
+ // once the agent is actually gone from the pane the name must be
+ // released, so `agent start` can reuse it.
+ let now = Instant::now();
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);
+ terminal.set_agent_name("reviewer".into());
+
+ terminal.set_detected_state_with_screen_signals_at(
+ Some(Agent::Pi),
+ AgentState::Idle,
+ false,
+ false,
+ false,
+ true,
+ now,
+ );
+ assert_eq!(terminal.agent_name.as_deref(), Some("reviewer"));
+
+ // The agent really is gone: the next observation finds no agent while
+ // the recorded exit still stands.
+ terminal.set_detected_state_with_screen_signals_at(
+ None,
+ AgentState::Unknown,
+ false,
+ false,
+ false,
+ false,
+ now + Duration::from_secs(1),
+ );
+ assert_eq!(
+ terminal.agent_name, None,
+ "a confirmed exit must release the name"
+ );
+ assert!(!terminal.is_agent_terminal());
+ }
+
+ #[test]
fn agent_alias_survives_detection_uncertainty_and_reported_release_but_not_replacement() {
let mut terminal = test_terminal();
terminal.set_detected_state(Some(Agent::Pi), AgentState::Working);