use anyhow::Context as _;
use crate::formatters::banner::tmux_has_session;
pub(crate) fn needs_restart(state: &str) -> bool {
matches!(state, "stopped" | "errored")
}
pub(crate) fn is_zombie(state: &str, tmux_live: bool) -> bool {
!tmux_live && !needs_restart(state)
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ResumeAction {
InPlace,
Attach,
Restart,
ReconcileThenRestart,
}
pub(crate) fn plan_resume(state: &str, tmux_live: bool) -> ResumeAction {
if is_zombie(state, tmux_live) {
ResumeAction::ReconcileThenRestart
} else if needs_restart(state) {
ResumeAction::Restart
} else {
ResumeAction::Attach
}
}
pub(crate) async fn resume_guided_session(
client: &reqwest::Client,
url: &str,
session: &trusty_mpm::client::ManagedSessionSummary,
) -> anyhow::Result<()> {
let tmux_live = tmux_has_session(&session.name);
let action = plan_resume(&session.state, tmux_live);
if action == ResumeAction::ReconcileThenRestart {
eprintln!(
"tm: '{}' is marked {} but its tmux pane is gone — reconciling and restarting…",
session.name, session.state
);
reconcile_zombie_stop(client, url, session).await?;
}
if matches!(
action,
ResumeAction::Restart | ResumeAction::ReconcileThenRestart
) {
if action == ResumeAction::Restart {
if tmux_live {
eprintln!(
"tm: session '{}' is {} but its tmux pane is still alive — \
the daemon will KILL that pane and start a fresh runtime \
(in-progress pane state will be lost).",
session.name, session.state
);
} else {
eprintln!(
"tm: session '{}' is {} (tmux absent); restarting via daemon…",
session.name, session.state
);
}
}
restart_via_daemon(client, url, session).await?;
}
tmux_attach(&session.name)
}
async fn reconcile_zombie_stop(
client: &reqwest::Client,
url: &str,
session: &trusty_mpm::client::ManagedSessionSummary,
) -> anyhow::Result<()> {
let resp = match client
.post(format!(
"{url}/api/v1/sessions/managed/{}/runtime-stop",
session.id
))
.timeout(std::time::Duration::from_secs(30))
.send()
.await
{
Ok(r) => r,
Err(e) => {
eprintln!(
"tm: daemon unreachable — cannot reconcile session '{}': {e}",
session.name
);
eprintln!("tm: start the daemon with `tm start`, then run `tm` again.");
anyhow::bail!("daemon unreachable; cannot reconcile zombie session: {e}");
}
};
match resp.status() {
reqwest::StatusCode::NOT_FOUND => {
eprintln!(
"tm: session '{}' not found on daemon; it may have been decommissioned.",
session.name
);
eprintln!("tm: run `tm session ls` to see current sessions.");
anyhow::bail!(
"session '{}' not found on daemon during reconcile",
session.name
);
}
s if !s.is_success() => {
eprintln!(
"tm: daemon returned {s} stopping '{}' during reconcile — try `tm session ls`.",
session.name
);
anyhow::bail!(
"daemon error {s} reconciling zombie session '{}'",
session.name
);
}
_ => Ok(()),
}
}
async fn restart_via_daemon(
client: &reqwest::Client,
url: &str,
session: &trusty_mpm::client::ManagedSessionSummary,
) -> anyhow::Result<()> {
let resp = match client
.post(format!(
"{url}/api/v1/sessions/managed/{}/resume",
session.id
))
.timeout(std::time::Duration::from_secs(30))
.send()
.await
{
Ok(r) => r,
Err(e) => {
eprintln!(
"tm: daemon unreachable — cannot restart session '{}': {e}",
session.name
);
eprintln!("tm: start the daemon with `tm start`, then run `tm` again.");
anyhow::bail!("daemon unreachable; cannot restart stopped session: {e}");
}
};
match resp.status() {
reqwest::StatusCode::NOT_FOUND => {
eprintln!(
"tm: session '{}' not found on daemon; it may have been decommissioned.",
session.name
);
eprintln!(
"tm: run `tm session ls` to see current sessions, \
or press [Enter] to launch a new one."
);
anyhow::bail!("session '{}' not found on daemon", session.name);
}
reqwest::StatusCode::CONFLICT => {
let msg = resp.text().await.unwrap_or_default();
eprintln!("tm: cannot restart session '{}': {}", session.name, msg);
eprintln!("tm: run `tm session ls` to see the current state.");
anyhow::bail!("cannot restart session '{}': {msg}", session.name);
}
s if s.is_server_error() => {
eprintln!(
"tm: daemon returned an internal error ({s}) restarting '{}' — \
try `tm session ls`.",
session.name
);
anyhow::bail!(
"daemon internal error {s} restarting session '{}'",
session.name
);
}
s if !s.is_success() => {
eprintln!(
"tm: daemon returned {s} restarting '{}' — try `tm session ls`.",
session.name
);
anyhow::bail!("daemon error {s} restarting session '{}'", session.name);
}
_ => {
let body: trusty_mpm::client::ManagedSessionSummary = resp
.json()
.await
.context("failed to parse daemon resume response")?;
if body.state == "errored" {
eprintln!(
"tm: session '{}' restarted but the runtime failed to start.",
session.name
);
eprintln!("tm: check `tm session info {}` for details.", session.id);
anyhow::bail!(
"session '{}' restarted but runtime failed to start (state=errored)",
session.name
);
}
eprintln!("tm: session restarted — attaching…");
Ok(())
}
}
}
fn tmux_attach(name: &str) -> anyhow::Result<()> {
crate::commands::tmux_attach::tmux_attach(name)
}