use crate::contracts::Runner;
use crate::runner;
use super::backend::{RunnerAttemptContext, RunnerBackend};
fn continue_session_error_text(err: &runner::RunnerError) -> String {
match err {
runner::RunnerError::NonZeroExit { stdout, stderr, .. }
| runner::RunnerError::TerminatedBySignal { stdout, stderr, .. } => {
format!("{} {}", stdout, stderr).to_lowercase()
}
_ => format!("{:#}", err).to_lowercase(),
}
}
pub(crate) fn should_fallback_to_fresh_continue(
runner_kind: &Runner,
err: &runner::RunnerError,
) -> bool {
let text = continue_session_error_text(err);
match runner_kind {
Runner::Pi => {
text.contains("pi session file not found")
|| text.contains("no session found matching")
|| text.contains("read pi session dir")
}
Runner::Gemini => {
text.contains("error resuming session")
&& (text.contains("invalid session identifier") || text.contains("--list-sessions"))
}
Runner::Claude => {
text.contains("--resume requires a valid session id")
|| text.contains("not a valid uuid")
}
Runner::Opencode => {
(text.contains("zoderror")
&& text.contains("sessionid")
&& text.contains("must start with \"ses\""))
|| (text.contains("semantic failure with zero exit status")
&& text.contains("opencode"))
}
Runner::Cursor => {
text.contains("invalid session")
|| text.contains("invalid chat")
|| text.contains("invalid agent")
|| text.contains("session not found")
|| text.contains("agent not found")
|| text.contains("unknown session")
|| text.contains("unknown agent")
|| text.contains("unknownagenterror")
|| text.contains("no session found")
|| text.contains("no agent found")
|| text.contains("no matching session")
|| (text.contains("resume") && text.contains("not found"))
}
_ => false,
}
}
fn validated_session_id_for_runner<'a>(
runner_kind: &Runner,
session_id: Option<&'a str>,
) -> Option<&'a str> {
session_id.filter(|id| crate::runner::is_valid_runner_session_id(runner_kind, id))
}
fn choose_continue_session_id<'a>(
runner_kind: &Runner,
error_session_id: Option<&'a str>,
invocation_session_id: Option<&'a str>,
) -> Option<&'a str> {
validated_session_id_for_runner(runner_kind, error_session_id)
.or_else(|| validated_session_id_for_runner(runner_kind, invocation_session_id))
}
pub(super) fn continue_or_rerun(
backend: &mut impl RunnerBackend,
attempt: &RunnerAttemptContext<'_>,
continue_message: &str,
fresh_prompt: &str,
invocation_session_id: Option<&str>,
error_session_id: Option<&str>,
) -> Result<runner::RunnerOutput, runner::RunnerError> {
let continue_session_id =
choose_continue_session_id(attempt.runner_kind, error_session_id, invocation_session_id);
if let Some(session_id) = continue_session_id {
match backend.resume_session(attempt.resume_session_request(
session_id,
continue_message,
true,
)) {
Ok(output) => {
log::debug!(
"resume: continuing the existing runner session for phase {:?}",
attempt.phase_type
);
return Ok(output);
}
Err(err) if should_fallback_to_fresh_continue(attempt.runner_kind, &err) => {
log::debug!(
"resume: existing runner session could not be reused; starting a fresh invocation: {err}"
);
}
Err(err) => return Err(err),
}
} else {
log::debug!("resume: no runner session id was available; starting a fresh invocation");
}
let fresh_session_id =
validated_session_id_for_runner(attempt.runner_kind, invocation_session_id)
.map(str::to_string);
backend.run_prompt(attempt.run_prompt_request(fresh_prompt, fresh_session_id))
}
#[cfg(test)]
mod tests {
use crate::contracts::Runner;
use crate::runner::RunnerError;
use super::{
choose_continue_session_id, should_fallback_to_fresh_continue,
validated_session_id_for_runner,
};
#[test]
fn cursor_resume_unknown_session_falls_back_to_fresh() {
let err = RunnerError::NonZeroExit {
code: 1,
stdout: "".into(),
stderr: "session not found for resume".into(),
session_id: None,
};
assert!(should_fallback_to_fresh_continue(&Runner::Cursor, &err));
}
#[test]
fn cursor_sdk_unknown_agent_falls_back_to_fresh() {
let err = RunnerError::NonZeroExit {
code: 1,
stdout: r#"{"type":"error","name":"UnknownAgentError","message":"agent not found"}"#
.into(),
stderr: "".into(),
session_id: None,
};
assert!(should_fallback_to_fresh_continue(&Runner::Cursor, &err));
}
#[test]
fn choose_continue_session_id_prefers_valid_error_id() {
assert_eq!(
choose_continue_session_id(
&Runner::Claude,
Some("claude-error-session"),
Some("claude-invocation-session"),
),
Some("claude-error-session")
);
}
#[test]
fn choose_continue_session_id_preserves_invocation_when_error_id_invalid() {
assert_eq!(
choose_continue_session_id(
&Runner::Claude,
Some("ambiguous error session"),
Some("claude-invocation-session"),
),
Some("claude-invocation-session")
);
}
#[test]
fn choose_continue_session_id_rejects_invalid_opencode_error_id() {
assert_eq!(
choose_continue_session_id(&Runner::Opencode, Some("open-789"), Some("ses_previous"),),
Some("ses_previous")
);
}
#[test]
fn choose_continue_session_id_returns_none_when_candidates_are_ambiguous() {
assert_eq!(
choose_continue_session_id(&Runner::Pi, Some(" "), Some("bad id")),
None
);
}
#[test]
fn fresh_fallback_session_id_reuses_only_valid_invocation_id() {
assert_eq!(
validated_session_id_for_runner(&Runner::Pi, Some("bad id")),
None
);
assert_eq!(
validated_session_id_for_runner(&Runner::Pi, Some("known-good")),
Some("known-good")
);
}
#[test]
fn cursor_resume_unrecognized_error_does_not_fallback() {
let err = RunnerError::NonZeroExit {
code: 1,
stdout: "".into(),
stderr: "unexpected failure".into(),
session_id: None,
};
assert!(!should_fallback_to_fresh_continue(&Runner::Cursor, &err));
}
}