use std::sync::Arc;
use tracing::{info, warn};
use super::deployment_check::ensure_deployment_complete;
#[cfg(test)]
use super::deployment_check::{carrier_reachable, warn_if_no_persona_carrier};
use super::inproject::try_inproject_spawn;
use super::resume_error::ResumeManagedError;
use crate::core::git_identity::resolve_for_config_enforced;
use crate::daemon::state::DaemonState;
use crate::provisioner::WorkspaceProvisioner;
use crate::runtime::{RuntimeKind, build_adapter};
use crate::session_manager::{ManagedSessionId, ManagedSessionState, SessionRecord};
#[cfg(test)]
use crate::session_manager::ManagedError;
pub(super) async fn resolve_gh_env(
state: &Arc<DaemonState>,
cwd: &std::path::Path,
) -> Vec<(String, String)> {
let registry = state.project_registry().await;
crate::core::gh_account::resolve_gh_account_env_for_registry(®istry, cwd).await
}
#[derive(Debug, Clone)]
pub struct SpawnParams {
pub repo_url: String,
pub git_ref: String,
pub task: String,
pub name_hint: Option<String>,
pub runtime: Option<String>,
pub ephemeral: Option<bool>,
pub mcp_initiated: bool,
pub inject_task: Option<bool>,
pub deliverable_id: Option<String>,
pub force_new: bool,
}
pub async fn spawn_managed(
state: &Arc<DaemonState>,
session_id: ManagedSessionId,
params: SpawnParams,
) -> Result<SessionRecord, String> {
let config = crate::core::trusty_tools_config::TrustyToolsConfig::load();
let runtime = match params.runtime.as_deref() {
None => RuntimeKind::default(),
Some(raw) => raw.parse::<RuntimeKind>().map_err(|e| e.to_string())?,
};
if params.mcp_initiated {
let registry = state.project_registry().await;
super::mcp_spawn_gate::ensure_mcp_spawn_allowed(®istry, &config, ¶ms.repo_url)
.await?;
}
let emitter = crate::core::provisioning_stage::StageEmitter::new(
session_id.to_string(),
params.repo_url.clone(),
state.event_tx.clone(),
);
let inject_flag = params.inject_task != Some(false);
let deliverable_id = params.deliverable_id.clone();
let mut record = crate::core::provisioning_stage::scoped(
emitter,
spawn_managed_routed(state, session_id, params, runtime, config),
)
.await?;
if let Some(raw_id) = deliverable_id {
match raw_id.parse::<crate::deliverable::DeliverableId>() {
Ok(did) => match state
.session_manager()
.await
.set_deliverable_id(&record.id, did)
.await
{
Ok(()) => record.deliverable_id = Some(did),
Err(e) => warn!(
id = %record.id,
"spawn_managed: set_deliverable_id failed: {e}; deliverable link not recorded"
),
},
Err(e) => warn!(
id = %record.id,
"spawn_managed: deliverable_id {raw_id:?} failed to re-parse (already validated by the HTTP layer): {e}"
),
}
}
spawn_task_injection(state.clone(), record.clone(), inject_flag);
Ok(record)
}
fn spawn_task_injection(state: Arc<DaemonState>, record: SessionRecord, inject_flag: bool) {
if !crate::session_manager::should_inject_task(
inject_flag,
&record.task,
record.runtime,
&record.state,
) {
return;
}
tokio::spawn(async move {
let mgr = state.session_manager().await;
match mgr.inject_task_when_ready(&record.id, &record.task).await {
Ok(true) => {}
Ok(false) => warn!(
id = %record.id,
name = %record.tmux_name,
"turnkey task injection skipped (runtime never became ready); task retained as metadata"
),
Err(e) => warn!(
id = %record.id,
name = %record.tmux_name,
"turnkey task injection failed: {e}; task retained as metadata"
),
}
});
}
async fn spawn_managed_routed(
state: &Arc<DaemonState>,
session_id: ManagedSessionId,
params: SpawnParams,
runtime: RuntimeKind,
config: crate::core::trusty_tools_config::TrustyToolsConfig,
) -> Result<SessionRecord, String> {
if is_local_workdir(¶ms.repo_url) {
let local_path = std::path::Path::new(¶ms.repo_url);
if let Some(origin_url) = super::inproject::get_origin_url(local_path)
&& let Some(gh) = trusty_common::github_path::parse_github_path(&origin_url)
{
let registry = state.project_registry().await;
if !super::launch_on_main::worktree_enabled_for_origin(®istry, &origin_url).await {
info!(
id = %session_id,
path = %local_path.display(),
"spawn_managed: project has worktree isolation disabled (#3455); \
launching directly in the main checkout"
);
{
let mgr = state.session_manager().await;
let existing = mgr.list().await;
let source_id = format!("{}/{}", gh.owner, gh.repo);
if let Some(live) = reconnect_candidate(
params.force_new,
&existing,
&source_id,
&*mgr.tmux_driver(),
) {
info!(
id = %live.id,
source_id = %source_id,
"spawn_managed (launch-on-main): reconnecting to existing live session"
);
return Ok(live);
}
}
return super::launch_on_main::spawn_managed_on_main(
state,
&session_id,
¶ms,
runtime,
local_path,
&gh.owner,
&gh.repo,
)
.await;
}
}
match try_inproject_spawn(local_path) {
Ok(Some((base, owner, repo))) => {
{
let mgr = state.session_manager().await;
let existing = mgr.list().await;
let source_id = format!("{owner}/{repo}");
if let Some(live) = reconnect_candidate(
params.force_new,
&existing,
&source_id,
&*mgr.tmux_driver(),
) {
info!(
id = %live.id,
source_id = %source_id,
"spawn_managed (inproject): reconnecting to existing live session"
);
return Ok(live);
}
}
match reserve_inproject_worktree(
state,
&session_id,
¶ms,
local_path,
&base,
&repo,
&config,
)
.await
{
Ok((worktree, reserved_name)) => {
return spawn_managed_inproject(
state,
&session_id,
¶ms,
runtime,
worktree,
owner,
repo,
reserved_name,
)
.await;
}
Err(e) => {
tracing::warn!(
id = %session_id,
"in-project spawn: {e}; falling back to local-path spawn"
);
}
}
}
Ok(None) => {
}
Err(e) => {
tracing::warn!(
id = %session_id,
"in-project spawn failed: {e}; falling back to local-path spawn"
);
}
}
return spawn_managed_local(state, &session_id, ¶ms, runtime).await;
}
spawn_managed_cloned(state, session_id, params, runtime, config).await
}
async fn reserve_inproject_worktree(
state: &Arc<DaemonState>,
session_id: &ManagedSessionId,
params: &SpawnParams,
local_path: &std::path::Path,
base: &std::path::Path,
repo: &str,
config: &crate::core::trusty_tools_config::TrustyToolsConfig,
) -> Result<(std::path::PathBuf, String), String> {
let mgr = state.session_manager().await;
let reserved_name = mgr
.resolve_session_name(
params.name_hint.as_deref(),
Some(repo),
local_path,
|candidate| super::inproject::worktree_name_collides(base, candidate),
)
.await
.map_err(|e| format!("name resolution failed for session {session_id}: {e}"))?;
let worktree = super::inproject::create_session_worktree(base, &reserved_name, session_id)
.map_err(|e| format!("worktree creation failed for session {session_id}: {e}"))?;
let resolved_sync =
crate::core::trusty_tools_config::resolve_untracked_sync(config, Some(repo));
if resolved_sync.enabled {
super::inproject::untracked_sync::sync_untracked_files(
local_path,
&worktree,
&resolved_sync.patterns,
);
}
Ok((worktree, reserved_name))
}
async fn spawn_managed_cloned(
state: &Arc<DaemonState>,
session_id: ManagedSessionId,
params: SpawnParams,
runtime: RuntimeKind,
config: crate::core::trusty_tools_config::TrustyToolsConfig,
) -> Result<SessionRecord, String> {
use crate::core::provisioning_stage::{ProvisioningStage, emit};
let git_identity = resolve_for_config_enforced(&config, ¶ms.repo_url)
.await
.map_err(|e| {
warn!(id = %session_id, "spawn_managed: git identity resolution failed: {e}");
format!("git identity resolution failed: {e}")
})?;
let prepared = match trusty_common::github_path::parse_github_path(¶ms.repo_url) {
Some(gh) => {
let project_dir = crate::core::trusty_tools_config::workspace_subpath(&config, &gh);
let provisioner = WorkspaceProvisioner::new(
crate::provisioner::RealGitBackend::new(git_identity),
std::path::PathBuf::new(),
);
provisioner.provision_in(
&project_dir,
&session_id,
¶ms.repo_url,
¶ms.git_ref,
¶ms.task,
)
}
None => {
let workspace_root = crate::core::trusty_tools_config::workspace_root(&config);
let provisioner = WorkspaceProvisioner::new(
crate::provisioner::RealGitBackend::new(git_identity),
workspace_root,
);
provisioner.provision(&session_id, ¶ms.repo_url, ¶ms.git_ref, ¶ms.task)
}
}
.map_err(|e| {
warn!(id = %session_id, "spawn_managed: provision failed: {e}");
format!("workspace provisioning failed: {e}")
})?;
emit(ProvisioningStage::CreatingTmuxSession);
let mgr = state.session_manager().await;
let record = mgr
.create_with_id(
session_id,
params.task.clone(),
Some(prepared.path.clone()),
params.name_hint,
Some(prepared.path.clone()),
Some(params.repo_url.clone()),
Some(params.git_ref.clone()),
runtime,
params.ephemeral.unwrap_or(false),
false, )
.await
.map_err(|e| {
warn!(id = %session_id, "spawn_managed: session create failed: {e}");
e.to_string()
})?;
mgr.set_worktree_owner_best_effort(&session_id, session_id)
.await;
state
.project_registry()
.await
.register_from_session(&record)
.await;
if let Some(record) =
front_gate_or_escalate(&mgr, &record, ¶ms.repo_url, ¶ms.task).await?
{
return Ok(record);
}
if let Err(e) = mgr
.set_workspace(
&record.id,
prepared.path.clone(),
ManagedSessionState::Active,
)
.await
{
warn!(id = %record.id, "spawn_managed: set_workspace failed: {e}");
}
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(&prepared.path);
if let Err(reason) =
ensure_deployment_complete(&fw, &prepared.path, record.repo_url.as_deref(), &record.id)
{
warn!(id = %record.id, "spawn_managed: deployment incomplete after auto-repair (non-blocking, launch proceeds): {reason}");
}
crate::core::session_launch::spawn_workstream_label_ensure(
record.repo_url.clone(),
prepared.path.clone(),
record.tmux_name.clone(),
);
emit(ProvisioningStage::LaunchingRuntime);
let tmux_arc = mgr.tmux_driver();
let adapter = build_adapter(record.runtime, tmux_arc);
let gh_env = resolve_gh_env(state, &prepared.path).await;
if let Err(e) = adapter.spawn(
&record.tmux_name,
&prepared.path,
¶ms.task,
&record.id.to_string(),
&gh_env,
) {
warn!(
id = %record.id,
name = %record.tmux_name,
runtime = %record.runtime.as_str(),
"spawn_managed: runtime adapter spawn failed: {e}"
);
let _ = mgr
.mark_errored(&record.id, &format!("spawn failed: {e}"))
.await;
} else {
info!(
id = %record.id,
name = %record.tmux_name,
path = %prepared.path.display(),
"managed session spawned successfully"
);
}
emit(ProvisioningStage::Complete);
Ok(mgr.get(&record.id).await.unwrap_or(record))
}
pub fn is_local_workdir(s: &str) -> bool {
let p = std::path::Path::new(s);
p.is_absolute() && p.is_dir()
}
pub fn write_task_md(workspace: &std::path::Path, task: &str, session_id: &ManagedSessionId) {
if task.is_empty() {
return;
}
let task_file = workspace.join("TASK.md");
if let Err(e) = std::fs::write(&task_file, task) {
tracing::warn!(
session = %session_id,
path = %task_file.display(),
"failed to write TASK.md (local-path spawn): {e}"
);
}
}
fn find_reusable_inproject_session(
records: &[SessionRecord],
source_id: &str,
tmux: &dyn crate::session_manager::ManagedTmuxDriver,
) -> Option<SessionRecord> {
records
.iter()
.find(|r| {
r.source_id.as_deref() == Some(source_id)
&& r.state == ManagedSessionState::Active
&& tmux.session_exists(&r.tmux_name)
})
.cloned()
}
fn reconnect_candidate(
force_new: bool,
records: &[SessionRecord],
source_id: &str,
tmux: &dyn crate::session_manager::ManagedTmuxDriver,
) -> Option<SessionRecord> {
if force_new {
return None;
}
find_reusable_inproject_session(records, source_id, tmux)
}
#[allow(clippy::too_many_arguments)]
async fn spawn_managed_inproject(
state: &std::sync::Arc<crate::daemon::state::DaemonState>,
session_id: &crate::session_manager::ManagedSessionId,
params: &SpawnParams,
runtime: crate::runtime::RuntimeKind,
worktree: std::path::PathBuf,
owner: String,
repo: String,
reserved_name: String,
) -> Result<crate::session_manager::SessionRecord, String> {
use crate::core::provisioning_stage::{ProvisioningStage, emit};
use crate::session_manager::ManagedSessionState;
info!(
id = %session_id,
worktree = %worktree.display(),
owner = %owner,
repo = %repo,
"spawn_managed: in-project worktree spawn"
);
write_task_md(&worktree, ¶ms.task, session_id);
let synthetic_repo_url = format!("https://github.com/{owner}/{repo}");
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(&worktree);
prepare_inproject_session(&fw, session_id, &worktree, &synthetic_repo_url);
emit(ProvisioningStage::CreatingTmuxSession);
let mgr = state.session_manager().await;
let record = mgr
.create_with_reserved_name(
*session_id,
reserved_name,
params.task.clone(),
Some(worktree.clone()),
Some(worktree.clone()),
Some(synthetic_repo_url),
None,
runtime,
params.ephemeral.unwrap_or(false),
false, )
.await
.map_err(|e| {
warn!(id = %session_id, "spawn_managed (inproject): create failed: {e}");
e.to_string()
})?;
let source_id = format!("{owner}/{repo}");
if let Err(e) = mgr.set_source_id(session_id, &source_id).await {
warn!(id = %session_id, "spawn_managed (inproject): set_source_id failed: {e}");
}
mgr.set_worktree_owner_best_effort(session_id, *session_id)
.await;
state
.project_registry()
.await
.register_from_session(&record)
.await;
if let Some(record) =
front_gate_or_escalate(&mgr, &record, ¶ms.repo_url, ¶ms.task).await?
{
return Ok(record);
}
if let Err(e) = mgr
.set_workspace(session_id, worktree.clone(), ManagedSessionState::Active)
.await
{
warn!(id = %session_id, "spawn_managed (inproject): set_workspace failed: {e}");
}
if let Err(reason) =
ensure_deployment_complete(&fw, &worktree, record.repo_url.as_deref(), session_id)
{
warn!(id = %session_id, "spawn_managed (inproject): deployment incomplete after auto-repair (non-blocking, launch proceeds): {reason}");
}
crate::core::session_launch::spawn_workstream_label_ensure(
record.repo_url.clone(),
worktree.clone(),
record.tmux_name.clone(),
);
emit(ProvisioningStage::LaunchingRuntime);
let tmux_arc = mgr.tmux_driver();
let adapter = crate::runtime::build_adapter(record.runtime, tmux_arc);
let gh_env = resolve_gh_env(state, &worktree).await;
if let Err(e) = adapter.spawn(
&record.tmux_name,
&worktree,
¶ms.task,
&record.id.to_string(),
&gh_env,
) {
warn!(
id = %record.id,
name = %record.tmux_name,
"spawn_managed (inproject): runtime adapter spawn failed: {e}"
);
let _ = mgr
.mark_errored(&record.id, &format!("spawn failed: {e}"))
.await;
} else {
info!(
id = %record.id,
name = %record.tmux_name,
worktree = %worktree.display(),
"managed session spawned successfully (in-project worktree)"
);
}
emit(ProvisioningStage::Complete);
Ok(mgr.get(&record.id).await.unwrap_or(record))
}
pub(super) fn prepare_inproject_session(
fw: &crate::core::paths::FrameworkPaths,
session_id: &ManagedSessionId,
worktree: &std::path::Path,
repo_url: &str,
) {
match crate::core::session_launch::prepare_session_with_repo_url(fw, worktree, Some(repo_url)) {
Ok(report) => {
info!(
id = %session_id,
deployed = report.deploy.deployed.len(),
skills_deployed = report.skill_deploy.deployed.len(),
worktree = %worktree.display(),
"spawn_managed (inproject): session prepared"
);
for err in &report.roster_errors {
tracing::error!(
id = %session_id,
worktree = %worktree.display(),
"spawn_managed (inproject): roster provisioning gap (session \
still launches with its trusty-mpm identity): {err}"
);
}
}
Err(e) => {
warn!(
id = %session_id,
worktree = %worktree.display(),
"spawn_managed (inproject): session prep failed (non-fatal): {e}"
);
}
}
}
async fn spawn_managed_local(
state: &Arc<DaemonState>,
session_id: &ManagedSessionId,
params: &SpawnParams,
runtime: RuntimeKind,
) -> Result<SessionRecord, String> {
use crate::core::provisioning_stage::{ProvisioningStage, emit};
let local_dir = std::path::PathBuf::from(¶ms.repo_url);
let origin_url = super::inproject::get_origin_url(&local_dir).ok_or_else(|| {
format!(
"spawn failed: '{}' has no git origin remote; \
managed sessions require a GitHub remote. \
Use `tm connect` / `tm launch --live` to run in the live checkout.",
local_dir.display()
)
})?;
let gh = trusty_common::github_path::parse_github_path(&origin_url).ok_or_else(|| {
format!(
"spawn failed: could not parse a GitHub owner/repo from origin remote \
'{origin_url}' for '{}'. \
Use `tm connect` to run in the live checkout instead.",
local_dir.display()
)
})?;
let source_id_str = format!("{}/{}", gh.owner, gh.repo);
let config = crate::core::trusty_tools_config::TrustyToolsConfig::load();
let project_dir = crate::core::trusty_tools_config::workspace_subpath(&config, &gh);
let git_identity = resolve_for_config_enforced(&config, &origin_url)
.await
.map_err(|e| {
warn!(id = %session_id, "spawn_managed (local→managed): git identity resolution failed: {e}");
format!("git identity resolution failed: {e}")
})?;
let provisioner = crate::provisioner::WorkspaceProvisioner::new(
crate::provisioner::RealGitBackend::new(git_identity),
std::path::PathBuf::new(),
);
let prepared = provisioner
.provision_in(
&project_dir,
session_id,
&origin_url,
¶ms.git_ref,
¶ms.task,
)
.map_err(|e| {
warn!(id = %session_id, "spawn_managed (local→managed): provision failed: {e}");
format!("workspace provisioning failed: {e}")
})?;
let workspace = prepared.path;
info!(
id = %session_id,
live = %local_dir.display(),
managed = %workspace.display(),
source_id = %source_id_str,
"spawn_managed: local-path redirected to managed clone (#1590)"
);
emit(ProvisioningStage::CreatingTmuxSession);
let mgr = state.session_manager().await;
let record = mgr
.create_with_id(
*session_id,
params.task.clone(),
Some(workspace.clone()),
params.name_hint.clone(),
Some(workspace.clone()),
Some(origin_url.clone()),
if params.git_ref.is_empty() {
None
} else {
Some(params.git_ref.clone())
},
runtime,
params.ephemeral.unwrap_or(false),
true, )
.await
.map_err(|e| {
warn!(id = %session_id, "spawn_managed (local→managed): create failed: {e}");
e.to_string()
})?;
if let Err(e) = mgr.set_source_id(session_id, &source_id_str).await {
warn!(id = %session_id, "spawn_managed (local→managed): set_source_id failed: {e}");
}
state
.project_registry()
.await
.register_from_session(&record)
.await;
if let Some(record) = front_gate_or_escalate(&mgr, &record, &origin_url, ¶ms.task).await? {
return Ok(record);
}
if let Err(e) = mgr
.set_workspace(&record.id, workspace.clone(), ManagedSessionState::Active)
.await
{
warn!(id = %record.id, "spawn_managed (local→managed): set_workspace failed: {e}");
}
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(&workspace);
if let Err(reason) =
ensure_deployment_complete(&fw, &workspace, record.repo_url.as_deref(), &record.id)
{
warn!(id = %record.id, "spawn_managed (local→managed): deployment incomplete after auto-repair (non-blocking, launch proceeds): {reason}");
}
crate::core::session_launch::spawn_workstream_label_ensure(
record.repo_url.clone(),
workspace.clone(),
record.tmux_name.clone(),
);
emit(ProvisioningStage::LaunchingRuntime);
let tmux_arc = mgr.tmux_driver();
let adapter = build_adapter(record.runtime, tmux_arc);
let gh_env = resolve_gh_env(state, &workspace).await;
if let Err(e) = adapter.spawn(
&record.tmux_name,
&workspace,
¶ms.task,
&record.id.to_string(),
&gh_env,
) {
warn!(
id = %record.id,
name = %record.tmux_name,
runtime = %record.runtime.as_str(),
"spawn_managed (local→managed): runtime adapter spawn failed: {e}"
);
let _ = mgr
.mark_errored(&record.id, &format!("spawn failed: {e}"))
.await;
} else {
info!(
id = %record.id,
name = %record.tmux_name,
path = %workspace.display(),
"managed session spawned successfully (local→managed clone)"
);
}
emit(ProvisioningStage::Complete);
Ok(mgr.get(&record.id).await.unwrap_or(record))
}
pub async fn spawn_runtime_for(
state: &Arc<DaemonState>,
record: &SessionRecord,
) -> Result<(), String> {
let mgr = state.session_manager().await;
let workspace = record
.workspace_path
.clone()
.unwrap_or_else(|| record.cwd.clone());
if let Err(e) = mgr
.set_workspace(&record.id, workspace.clone(), ManagedSessionState::Active)
.await
{
warn!(id = %record.id, "spawn_runtime_for: set_workspace failed: {e}");
}
let tmux_arc = mgr.tmux_driver();
let adapter = build_adapter(record.runtime, tmux_arc);
let gh_env = resolve_gh_env(state, &workspace).await;
if let Err(e) = adapter.spawn(
&record.tmux_name,
&workspace,
&record.task,
&record.id.to_string(),
&gh_env,
) {
warn!(
id = %record.id,
name = %record.tmux_name,
"spawn_runtime_for: runtime adapter spawn failed: {e}"
);
let _ = mgr
.mark_errored(&record.id, &format!("spawn failed: {e}"))
.await;
return Err(e.to_string());
}
info!(
id = %record.id,
name = %record.tmux_name,
"FRONT-gate-escalated session spawned after human approval"
);
Ok(())
}
pub(super) async fn front_gate_or_escalate(
mgr: &Arc<crate::session_manager::SessionManager>,
record: &SessionRecord,
repo_url: &str,
task: &str,
) -> Result<Option<SessionRecord>, String> {
use super::front_gate::{
HeadlessApproval, IsrConformanceGate, prework_autonomy_disposition, run_front_gate,
};
let (owner, repo) = match trusty_common::github_path::parse_github_path(repo_url) {
Some(gh) => (gh.owner, gh.repo),
None => return Ok(None),
};
let repo_root = record
.workspace_path
.clone()
.unwrap_or_else(|| record.cwd.clone());
let gate = IsrConformanceGate::new(repo_root);
let autonomy = prework_autonomy_disposition(task);
let approval = HeadlessApproval::current();
let outcome = run_front_gate(&gate, &owner, &repo, task, autonomy, approval).await;
if outcome.may_spawn() {
return Ok(None);
}
let reason = outcome
.escalation_reason()
.unwrap_or_else(|| "conformance escalation".to_string());
warn!(
id = %record.id,
approval = ?outcome.approval,
"spawn_managed: FRONT gate escalated; withholding spawn: {reason}"
);
mgr.set_pending_decision(&record.id, &reason, outcome.proposed_default.as_deref())
.await
.map_err(|e| {
warn!(id = %record.id, "spawn_managed: set_pending_decision failed: {e}");
e.to_string()
})?;
Ok(Some(
mgr.get(&record.id).await.unwrap_or_else(|_| record.clone()),
))
}
pub async fn resume_managed(
state: &Arc<DaemonState>,
id: &ManagedSessionId,
) -> Result<SessionRecord, ResumeManagedError> {
let mgr = state.session_manager().await;
let record = mgr.resume(id).await.map_err(ResumeManagedError::from)?;
let workspace = record
.workspace_path
.clone()
.unwrap_or_else(|| record.cwd.clone());
crate::core::session_launch::resume_self_heal(&workspace, &record.id.to_string()).await;
if let Err(e) = crate::core::session_launch::ensure_status_line(&workspace) {
warn!(
id = %record.id,
"resume_managed: statusline self-heal failed (non-fatal): {e}"
);
}
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(&workspace);
if let Err(reason) =
ensure_deployment_complete(&fw, &workspace, record.repo_url.as_deref(), &record.id)
{
warn!(id = %record.id, "resume_managed: deployment incomplete after auto-repair (non-blocking, launch proceeds): {reason}");
}
let tmux_arc = mgr.tmux_driver();
let adapter = build_adapter(record.runtime, tmux_arc);
let gh_env = resolve_gh_env(state, &workspace).await;
if let Err(e) = adapter.spawn_resume(
&record.tmux_name,
record.pane_id.as_deref(),
&workspace,
&record.task,
record.claude_session_id.as_deref(),
&record.id.to_string(),
&gh_env,
) {
warn!(
id = %record.id,
name = %record.tmux_name,
runtime = %record.runtime.as_str(),
"resume_managed: runtime adapter spawn_resume failed: {e}"
);
let _ = mgr
.mark_errored(&record.id, &format!("resume spawn failed: {e}"))
.await;
} else {
info!(
id = %record.id,
name = %record.tmux_name,
workspace = %workspace.display(),
"managed session resumed and runtime respawned"
);
}
Ok(mgr.get(id).await.unwrap_or(record))
}
#[cfg(test)]
#[path = "lifecycle_tests.rs"]
mod tests;