use crate::tmux::TmuxManager;
use std::path::Path;
use tokio::time::{timeout, Duration};
use super::{agent, response};
pub struct SessionOutput {
pub exit_code: i32,
pub raw_stdout: String,
pub raw_stderr: String,
pub elapsed_secs: Option<u64>,
}
pub async fn run_agent_session(
task_id: &str,
invocation: &agent::AgentInvocation,
attempt_dir: &Path,
orch_home: &Path,
) -> (TmuxManager, String, SessionOutput) {
let task_timeout = Duration::from_secs(invocation.timeout_seconds + 120);
let tmux = TmuxManager::new();
let session_start = std::time::Instant::now();
let session = match agent::spawn_in_tmux(&tmux, invocation).await {
Ok(s) => s,
Err(e) => {
tracing::error!(task_id, error = ?e, "failed to spawn agent in tmux");
return (
tmux,
String::new(),
SessionOutput {
exit_code: -1,
raw_stdout: String::new(),
raw_stderr: e.to_string(),
elapsed_secs: None,
},
);
}
};
let poll_interval = Duration::from_secs(5);
let wait_result = timeout(
task_timeout,
tmux.wait_for_completion(&session, poll_interval),
)
.await;
let elapsed_since_session_start = session_start.elapsed().as_secs();
match wait_result {
Ok(Ok(_output)) => {
tracing::info!(task_id, "agent session completed");
}
Ok(Err(e)) => {
tracing::error!(task_id, ?e, "error waiting for session");
let _ = tmux.kill_session(&session).await;
}
Err(_) => {
tracing::error!(
task_id,
elapsed_secs = elapsed_since_session_start,
"agent timed out after {} seconds",
elapsed_since_session_start
);
let _ = tmux.kill_session(&session).await;
}
}
let output = collect_output(
task_id,
invocation,
attempt_dir,
orch_home,
elapsed_since_session_start,
)
.await;
(tmux, session, output)
}
async fn collect_output(
task_id: &str,
invocation: &agent::AgentInvocation,
attempt_dir: &Path,
orch_home: &Path,
elapsed_secs: u64,
) -> SessionOutput {
let attempt_exit = attempt_dir.join("exit.txt");
let legacy_exit = crate::home::state_file(&format!("exit-{task_id}.txt"))
.unwrap_or_else(|_| orch_home.join("state").join(format!("exit-{task_id}.txt")));
let exit_code: i32 = match tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&attempt_exit)
.or_else(|_| std::fs::read_to_string(&legacy_exit))
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(-1)
})
.await
{
Ok(code) => code,
Err(e) => {
tracing::error!(task_id, err = ?e, "spawn_blocking panicked reading exit code");
-1
}
};
let raw_stdout =
response::read_output_file(task_id, &invocation.output_file, &invocation.repo).await;
let stderr_path_attempt = attempt_dir.join("stderr.txt");
let stderr_path_legacy = crate::home::state_file(&format!("stderr-{task_id}.txt"))
.unwrap_or_else(|_| {
orch_home
.join("state")
.join(format!("stderr-{task_id}.txt"))
});
let raw_stderr: String = match tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&stderr_path_attempt)
.or_else(|_| std::fs::read_to_string(&stderr_path_legacy))
.unwrap_or_default()
})
.await
{
Ok(s) => s,
Err(e) => {
tracing::error!(task_id, err = ?e, "spawn_blocking panicked reading stderr");
String::new()
}
};
SessionOutput {
exit_code,
raw_stdout,
raw_stderr,
elapsed_secs: Some(elapsed_secs),
}
}
pub async fn cleanup_session(task_id: &str, tmux: &TmuxManager, session: &str) {
if !session.is_empty() && tmux.session_exists(session).await {
if let Err(e) = tmux.kill_session(session).await {
tracing::warn!(task_id, error = ?e, "failed to kill tmux session");
}
}
tmux.unset_global_env("GH_TOKEN").await.ok();
tmux.unset_global_env("GITHUB_TOKEN").await.ok();
}
#[cfg(test)]
mod tests {
use tokio::time::Duration;
#[test]
fn session_timeout_default_is_30_minutes() {
let expected_default = 1800u64;
assert_eq!(
expected_default, 1800,
"default session timeout should be 1800 seconds (30 minutes)"
);
let with_grace = expected_default + 120;
assert_eq!(
with_grace, 1920,
"tokio timeout with 120s grace period should be 1920 seconds"
);
}
#[test]
fn session_timeout_grace_period_is_120_seconds() {
const GRACE_PERIOD_SECS: u64 = 120;
const DEFAULT_TIMEOUT_SECS: u64 = 1800;
let tokio_timeout_secs = DEFAULT_TIMEOUT_SECS + GRACE_PERIOD_SECS;
let tokio_timeout = Duration::from_secs(tokio_timeout_secs);
assert_eq!(
tokio_timeout.as_secs(),
1920,
"tokio timeout with grace period should be 1920 seconds"
);
assert_eq!(GRACE_PERIOD_SECS, 120, "grace period should be 120 seconds");
}
#[test]
fn session_timeout_configuration_is_documented() {
let default_seconds = 1800u64;
let default_minutes = default_seconds / 60;
assert_eq!(default_minutes, 30, "default timeout should be 30 minutes");
}
}