#[must_use]
pub(crate) fn agent_string_is_stale_cursor_sdk_auth(msg: &str) -> bool {
let text = msg.to_ascii_lowercase();
if text.contains("authenticationerror") || text == "authentication" {
return true;
}
if text.starts_with("authentication") {
return true;
}
if text.contains("error_not_logged_in") {
return true;
}
if text.contains("[unauthenticated]") || text.contains("unauthenticated") {
return true;
}
text.contains("logged in") && text.contains("logging out")
}
#[must_use]
pub(crate) fn agent_string_is_cursor_agent_busy(msg: &str) -> bool {
msg.to_ascii_lowercase().contains("already has active run")
}
const CHILD_OR_BRIDGE_DEAD_NEEDLES: &[&str] = &[
"acp child process appears hung",
"acp child process is not running",
"acp child process is zombie",
"acp stdout closed",
"bridge stdout closed",
"bridge write:",
"bridge flush:",
"bridge read:",
"bridge drain timed out",
crate::acp::DRAIN_IDLE_PREFIX_BRIDGE,
"pi rpc drain timed out",
crate::acp::DRAIN_IDLE_PREFIX_PI,
"pi rpc stdout closed",
"pi rpc write:",
"pi rpc flush:",
"pi rpc read:",
crate::acp::DRAIN_IDLE_PREFIX_CODEX,
"codex stdout closed",
"codex write:",
"codex flush:",
"codex read:",
"codex json-rpc parse:",
"currently streaming",
"iterable is closed",
"connection stalled",
];
fn text_has_any(text: &str, needles: &[&str]) -> bool {
needles.iter().any(|n| text.contains(n))
}
#[must_use]
pub(crate) fn agent_error_requires_coder_session_teardown(msg: &str) -> bool {
let text = msg.to_ascii_lowercase();
if text_has_any(&text, CHILD_OR_BRIDGE_DEAD_NEEDLES) {
return true;
}
if agent_string_is_cursor_agent_busy(msg) {
return true;
}
if agent_string_is_stale_cursor_sdk_auth(msg) {
return true;
}
if !crate::acp::agent_string_is_cursor_http2_transport_error(msg) {
return false;
}
if crate::acp::test_no_real_agent_enabled() {
return false;
}
true
}