trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! The daemon's implementation of the automatic-resume runtime relaunch
//! (#8233, owner ruling 2026-09-18).
//!
//! Why: `session_manager` sits below the daemon and cannot resolve a pinned
//! `gh` identity (that needs the project registry) or build a runtime adapter
//! against daemon-resolved inputs. The manager therefore owns only the SEAM
//! (`session_manager::relaunch::RuntimeRelauncher`); this module is the body,
//! and it deliberately runs the same two steps `resume_managed` does —
//! `RuntimeAdapter::spawn_resume`, then `launch_verify` — so an auto-resume and
//! an operator resume cannot diverge in what they put in the pane.
//!
//! What: [`DaemonRelauncher`] holds a `Weak<DaemonState>` (the state owns the
//! session manager, which owns this, so a strong handle would be a cycle that
//! never drops). A dropped state answers `Err`, which the manager turns into an
//! errored record rather than a silently-Active one.
//!
//! Test: `crates/trusty-mpm/src/daemon/managed_routes/auto_relaunch_tests.rs`.

use std::sync::{Arc, Weak};

use crate::daemon::state::DaemonState;
use crate::session_manager::SessionRecord;
use crate::session_manager::relaunch::RuntimeRelauncher;

/// Relaunch a session's runtime through the daemon's own adapter path.
///
/// Why/What: see this module's header.
/// Test: `auto_relaunch_tests.rs`.
pub(crate) struct DaemonRelauncher {
    state: Weak<DaemonState>,
}

impl DaemonRelauncher {
    /// Wrap a daemon state as a relauncher.
    ///
    /// Test: `install_auto_relauncher` is exercised by the boot path.
    pub(crate) fn new(state: &Arc<DaemonState>) -> Self {
        Self {
            state: Arc::downgrade(state),
        }
    }
}

#[async_trait::async_trait]
impl RuntimeRelauncher for DaemonRelauncher {
    /// Put a verified runtime back into `record`'s pane.
    ///
    /// What: resolves the workspace the same way the record does
    /// (`workspace_path` → `cwd`), resolves the pinned `gh` identity, builds the
    /// adapter for the record's OWN runtime kind, calls `spawn_resume` into the
    /// record's OWN pane once [`spawn_resume_into_owned_pane`] proves it
    /// (#9101), and then runs the post-send verification. Every
    /// failure is an `Err(message)` — never a warning beside a record left
    /// `Active`, which is the defect this exists to close.
    /// Test: `a_dropped_daemon_state_fails_the_relaunch` in
    /// `auto_relaunch_tests.rs`. The no-runtime arm is proven one level up, by
    /// `an_auto_resume_whose_relaunch_finds_no_runtime_is_not_left_active`
    /// (`session_manager/relaunch_tests.rs`), over a scripted relauncher.
    async fn relaunch(&self, record: &SessionRecord) -> Result<(), String> {
        let Some(state) = self.state.upgrade() else {
            return Err("the daemon state is gone, so no runtime could be started".to_owned());
        };
        let mgr = state.session_manager().await;
        let workspace = record
            .workspace_path
            .clone()
            .unwrap_or_else(|| record.cwd.clone());
        let tmux = mgr.tmux_driver();
        let gh_env = super::lifecycle::resolve_gh_env(&state, &workspace).await;
        // #8233: the daemon's own framework root, never the process home.
        let adapter = crate::runtime::build_adapter(
            record.runtime,
            tmux.clone(),
            None,
            state.framework_root(),
        );
        // #8983: the resumed claude may rebind its session id; a failed
        // launch, or a refused pane (#9101), revokes the grant.
        let granted = state.grant_resume_of(record);
        spawn_resume_into_owned_pane(&mgr, adapter.as_ref(), record, &workspace, &gh_env).map_err(
            |e| {
                state.revoke_resume_grant(granted);
                format!("runtime adapter spawn_resume failed: {e}")
            },
        )?;
        // The SAME verification the interactive resume runs: a record becomes
        // `Active` only behind a runtime this actually saw.
        match super::launch_verify::record_resume_outcome(&mgr, tmux.as_ref(), record, &workspace)
            .await
        {
            Some(msg) => Err(msg),
            None => Ok(()),
        }
    }
}

/// `spawn_resume` into `record`'s pane, once the pane is proven the record's
/// on the live tmux server (#9101).
///
/// Why: both resume paths, the operator's `resume_managed` and the automatic
/// [`DaemonRelauncher`], type the launch command into `record.pane_id`. Each
/// runs `SessionManager::resume_inner` first, which proves or recreates the
/// pane, but work runs between the two (a git fetch, a prompt refresh), and a
/// recreated pane whose capture failed carries no pane id at all.
/// What: `SessionManager::owned_pane`, then `spawn_resume` with the proven
/// pane id. The refusal is the `Err`, and no keys are sent.
/// Test: `a_stale_record_after_a_server_restart_never_gets_a_resume_spawn`,
/// `an_unreadable_pane_identity_refuses_the_resume_spawn`.
pub(crate) fn spawn_resume_into_owned_pane(
    mgr: &crate::session_manager::SessionManager,
    adapter: &dyn crate::runtime::RuntimeAdapter,
    record: &SessionRecord,
    workspace: &std::path::Path,
    gh_env: &[(String, String)],
) -> Result<(), String> {
    let pane_id = mgr
        .owned_pane(&record.id, record)
        .map_err(|e| e.to_string())?;
    adapter
        .spawn_resume(
            &record.tmux_name,
            Some(&pane_id),
            workspace,
            &record.task,
            record.claude_session_id.as_deref(),
            &record.id.to_string(),
            gh_env,
        )
        .map_err(|e| e.to_string())
}

/// Install [`DaemonRelauncher`] on this daemon's session manager.
///
/// Why: the manager is built lazily, so the install happens the first time the
/// daemon has both halves in hand. Idempotent — `install_relauncher` keeps the
/// first.
/// What: no-op when the manager already has one.
/// Test: `installing_a_relauncher_twice_keeps_the_first` in
/// `session_manager::relaunch`'s tests covers the idempotence.
pub async fn install_auto_relauncher(state: &Arc<DaemonState>) {
    let mgr = state.session_manager().await;
    mgr.install_relauncher(Arc::new(DaemonRelauncher::new(state)));
}

#[cfg(test)]
#[path = "auto_relaunch_tests.rs"]
mod tests;