use std::path::Path;
use std::time::Duration;
use crate::commands::run::PhaseType;
use crate::contracts::{ClaudePermissionMode, Model, ReasoningEffort, Runner};
use crate::runner;
use super::backend::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"))
}
_ => false,
}
}
fn choose_continue_session_id<'a>(
error_session_id: Option<&'a str>,
invocation_session_id: Option<&'a str>,
) -> Option<&'a str> {
error_session_id
.map(str::trim)
.filter(|id| !id.is_empty())
.or_else(|| {
invocation_session_id
.map(str::trim)
.filter(|id| !id.is_empty())
})
}
#[allow(clippy::too_many_arguments)]
pub(super) fn continue_or_rerun(
backend: &mut impl RunnerBackend,
runner_kind: &Runner,
repo_root: &Path,
bins: runner::RunnerBinaries<'_>,
model: &Model,
reasoning_effort: Option<ReasoningEffort>,
runner_cli: runner::ResolvedRunnerCliOptions,
continue_message: &str,
fresh_prompt: &str,
timeout: Option<Duration>,
permission_mode: Option<ClaudePermissionMode>,
output_handler: Option<runner::OutputHandler>,
output_stream: runner::OutputStream,
phase_type: PhaseType,
invocation_session_id: Option<&str>,
error_session_id: Option<&str>,
) -> Result<runner::RunnerOutput, runner::RunnerError> {
let continue_session_id = choose_continue_session_id(error_session_id, invocation_session_id);
if let Some(session_id) = continue_session_id {
match backend.resume_session(
runner_kind.clone(),
repo_root,
bins,
model.clone(),
reasoning_effort,
runner_cli,
session_id,
continue_message,
permission_mode,
timeout,
output_handler.clone(),
output_stream,
phase_type,
None,
) {
Ok(output) => {
eprintln!(
"Resume: continuing the existing runner session for phase {:?}.",
phase_type
);
return Ok(output);
}
Err(err) if should_fallback_to_fresh_continue(runner_kind, &err) => {
eprintln!(
"Resume: existing runner session could not be reused; starting a fresh invocation."
);
eprintln!(" {}", err);
}
Err(err) => return Err(err),
}
} else {
eprintln!("Resume: no runner session id was available; starting a fresh invocation.");
}
backend.run_prompt(
runner_kind.clone(),
repo_root,
bins,
model.clone(),
reasoning_effort,
runner_cli,
fresh_prompt,
timeout,
permission_mode,
output_handler,
output_stream,
phase_type,
invocation_session_id.map(str::to_string),
None,
)
}