aboutsummaryrefslogtreecommitdiff
path: root/src/cli/agent.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli/agent.rs')
-rw-r--r--src/cli/agent.rs146
1 files changed, 124 insertions, 22 deletions
diff --git a/src/cli/agent.rs b/src/cli/agent.rs
index 362c9ab1..306c3bd9 100644
--- a/src/cli/agent.rs
+++ b/src/cli/agent.rs
@@ -3,9 +3,12 @@ use std::time::{Duration, Instant};
use crate::api::schema::{
AgentPromptParams, AgentPromptWaitOptions, AgentReadParams, AgentRenameParams,
AgentSendKeysParams, AgentStartParams, AgentTarget, AgentWaitParams, EmptyParams, Method,
- ReadFormat, ReadSource, Request,
+ PaneProcessInfoParams, PaneTarget, ReadFormat, ReadSource, Request,
};
+const AGENT_START_POLL_INTERVAL: Duration = Duration::from_millis(100);
+const PANE_SHELL_READINESS_RETRY_TIMEOUT: Duration = Duration::from_secs(2);
+
pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> {
let Some(subcommand) = args.first().map(|arg| arg.as_str()) else {
print_agent_help();
@@ -330,24 +333,62 @@ fn agent_start(args: &[String]) -> std::io::Result<i32> {
return Ok(2);
};
let expected_kind = crate::detect::agent_label(expected_kind).to_string();
- let mut response = super::send_request(&Request {
- id: "cli:agent:start".into(),
- method: Method::AgentStart(AgentStartParams {
- name: name.clone(),
- kind,
- pane_id: pane_id.clone(),
- args: if separator < args.len() {
- args[separator + 1..].to_vec()
- } else {
- Vec::new()
- },
- timeout_ms,
- }),
- })?;
- if response.get("error").is_some() {
- return super::print_response(&response);
- }
+ let agent_args = if separator < args.len() {
+ args[separator + 1..].to_vec()
+ } else {
+ Vec::new()
+ };
let timeout = Duration::from_millis(timeout_ms.unwrap_or(30_000));
+ let retryable_timeout = timeout > crate::app::AGENT_START_SETTLE_DELAY
+ && timeout <= crate::app::MAX_AGENT_START_TIMEOUT;
+ let pinned_terminal_id = pane_terminal_id(&pane_id)?;
+ let mut retry_deadline = None;
+ let mut previous_busy_response = None;
+ let mut response = loop {
+ if let Some(previous_busy_response) = previous_busy_response.as_ref() {
+ let retry_expired = retry_deadline.is_some_and(|deadline| Instant::now() >= deadline);
+ if retry_expired
+ || pane_terminal_id(&pane_id)? != pinned_terminal_id
+ || !pane_shell_is_initializing(&pane_id)?
+ {
+ return super::print_response(previous_busy_response);
+ }
+ }
+
+ let response = super::send_request(&Request {
+ id: "cli:agent:start".into(),
+ method: Method::AgentStart(AgentStartParams {
+ name: name.clone(),
+ kind: kind.clone(),
+ pane_id: pane_id.clone(),
+ args: agent_args.clone(),
+ timeout_ms,
+ }),
+ })?;
+ if response.get("error").is_none() {
+ break response;
+ }
+ if response["error"]["code"].as_str() != Some("agent_pane_busy")
+ || !retryable_timeout
+ || pinned_terminal_id.is_none()
+ || pane_terminal_id(&pane_id)? != pinned_terminal_id
+ || !pane_shell_is_initializing(&pane_id)?
+ {
+ return super::print_response(&response);
+ }
+
+ let deadline = *retry_deadline
+ .get_or_insert_with(|| Instant::now() + PANE_SHELL_READINESS_RETRY_TIMEOUT);
+ previous_busy_response = Some(response);
+ let remaining = deadline.saturating_duration_since(Instant::now());
+ if remaining.is_zero() {
+ if let Some(previous_busy_response) = previous_busy_response.as_ref() {
+ return super::print_response(previous_busy_response);
+ }
+ }
+ std::thread::sleep(AGENT_START_POLL_INTERVAL.min(remaining));
+ };
+
let Some(expected_terminal_id) = response["result"]["agent"]["terminal_id"].as_str() else {
return super::print_response(&cli_agent_error(
"cli:agent:start",
@@ -355,6 +396,12 @@ fn agent_start(args: &[String]) -> std::io::Result<i32> {
"agent start response did not include terminal_id",
));
};
+ if pinned_terminal_id
+ .as_deref()
+ .is_some_and(|pinned| pinned != expected_terminal_id)
+ {
+ return super::print_response(&agent_name_lost_error("cli:agent:start", name));
+ }
let waited = wait_for_named_agent(
name,
&pane_id,
@@ -505,8 +552,7 @@ fn wait_for_named_agent(
expected_kind: &str,
expected_terminal_id: &str,
) -> std::io::Result<Result<serde_json::Value, serde_json::Value>> {
- let started_at = Instant::now();
- let deadline = started_at.checked_add(timeout);
+ let deadline = Instant::now().checked_add(timeout);
let mut first_poll = true;
loop {
if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
@@ -525,7 +571,7 @@ fn wait_for_named_agent(
if response.get("error").is_some() {
response = resolve_agent_target_unchecked(fallback_pane_id, poll_id)?;
if response.get("error").is_some() {
- std::thread::sleep(Duration::from_millis(100));
+ std::thread::sleep(AGENT_START_POLL_INTERVAL);
continue;
}
}
@@ -567,10 +613,66 @@ fn wait_for_named_agent(
if let Some(outcome) = outcome {
return Ok(outcome);
}
- std::thread::sleep(Duration::from_millis(100));
+ std::thread::sleep(AGENT_START_POLL_INTERVAL);
}
}
+fn pane_terminal_id(pane_id: &str) -> std::io::Result<Option<String>> {
+ let response = super::send_request(&Request {
+ id: "cli:agent:start:pane".into(),
+ method: Method::PaneGet(PaneTarget {
+ pane_id: pane_id.to_owned(),
+ }),
+ })?;
+ Ok(response["result"]["pane"]["terminal_id"]
+ .as_str()
+ .map(str::to_owned))
+}
+
+fn pane_shell_is_initializing(pane_id: &str) -> std::io::Result<bool> {
+ let response = super::send_request(&Request {
+ id: "cli:agent:start:process_info".into(),
+ method: Method::PaneProcessInfo(PaneProcessInfoParams {
+ pane_id: Some(pane_id.to_owned()),
+ }),
+ })?;
+ Ok(process_info_shows_shell_initialization(
+ &response["result"]["process_info"],
+ ))
+}
+
+#[cfg(unix)]
+fn process_info_shows_shell_initialization(process_info: &serde_json::Value) -> bool {
+ let Some(shell_pid) = process_info["shell_pid"].as_u64() else {
+ return false;
+ };
+ if process_info["foreground_process_group_id"].as_u64() != Some(shell_pid) {
+ return false;
+ }
+ process_info["foreground_processes"]
+ .as_array()
+ .is_some_and(|processes| {
+ processes.iter().any(|process| {
+ process["pid"].as_u64() == Some(shell_pid)
+ && (process["name"]
+ .as_str()
+ .is_some_and(crate::platform::is_pane_shell_process_name)
+ || process["argv"]
+ .as_array()
+ .and_then(|argv| argv.first())
+ .and_then(serde_json::Value::as_str)
+ .is_some_and(crate::platform::is_pane_shell_process_name))
+ })
+ })
+}
+
+// Windows exposes no foreground process group, so shell initialization is not
+// observable and a busy `agent.start` is not retried there.
+#[cfg(not(unix))]
+fn process_info_shows_shell_initialization(_process_info: &serde_json::Value) -> bool {
+ false
+}
+
fn agent_name_lost_error(request_id: &str, expected_name: &str) -> serde_json::Value {
cli_agent_error(
request_id,