use std::sync::Arc;
use tracing::{info, warn};
use super::deployment_check::ensure_deployment_complete;
use super::lifecycle::{
SpawnParams, front_gate_or_escalate, prepare_inproject_session, resolve_gh_env, write_task_md,
};
use crate::daemon::state::DaemonState;
use crate::runtime::RuntimeKind;
use crate::session_manager::{ManagedSessionId, ManagedSessionState, SessionRecord};
pub(super) fn has_concurrent_main_checkout_session<'a>(
existing: &'a [SessionRecord],
local_path: &std::path::Path,
) -> Option<&'a SessionRecord> {
existing
.iter()
.find(|r| r.state == ManagedSessionState::Active && r.cwd == local_path)
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn spawn_managed_on_main(
state: &Arc<DaemonState>,
session_id: &ManagedSessionId,
params: &SpawnParams,
runtime: RuntimeKind,
local_path: &std::path::Path,
owner: &str,
repo: &str,
) -> Result<SessionRecord, String> {
use crate::core::provisioning_stage::{ProvisioningStage, emit};
let mgr = state.session_manager().await;
{
let existing = mgr.list().await;
if let Some(other) = has_concurrent_main_checkout_session(&existing, local_path) {
info!(
path = %local_path.display(),
colliding_session_id = %other.id,
colliding_session_name = %other.tmux_name,
"spawn_managed (launch-on-main): a second Active session is launching in the \
SAME main checkout (#3455). Writers are isolated by the ADR-0048 worktree \
grant and the main-checkout write boundary, so this is expected; recorded so \
the sharing session can be identified"
);
}
}
let reserved_name = mgr
.resolve_session_name(params.name_hint.as_deref(), Some(repo), local_path, |_| {
false
})
.await
.map_err(|e| format!("name resolution failed for session {session_id}: {e}"))?;
write_task_md(local_path, ¶ms.task, session_id);
let synthetic_repo_url = format!("https://github.com/{owner}/{repo}");
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(local_path);
prepare_inproject_session(&fw, session_id, local_path, &synthetic_repo_url)?;
emit(ProvisioningStage::CreatingTmuxSession);
let record = mgr
.create_with_reserved_name(
*session_id,
reserved_name,
params.task.clone(),
Some(local_path.to_path_buf()),
Some(local_path.to_path_buf()),
Some(synthetic_repo_url),
None,
runtime,
params.ephemeral.unwrap_or(false),
false, )
.await
.map_err(|e| {
warn!(id = %session_id, "spawn_managed (launch-on-main): 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 (launch-on-main): set_source_id failed: {e}");
}
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,
local_path.to_path_buf(),
ManagedSessionState::Active,
)
.await
{
warn!(id = %session_id, "spawn_managed (launch-on-main): set_workspace failed: {e}");
}
if let Err(reason) =
ensure_deployment_complete(&fw, local_path, record.repo_url.as_deref(), session_id)
{
warn!(
id = %session_id,
"spawn_managed (launch-on-main): deployment incomplete after auto-repair \
(non-blocking, launch proceeds): {reason}"
);
}
crate::core::session_launch::spawn_workstream_label_ensure(
record.repo_url.clone(),
local_path.to_path_buf(),
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, local_path).await;
if let Err(e) = adapter.spawn(
&record.tmux_name,
local_path,
¶ms.task,
&record.id.to_string(),
&gh_env,
) {
warn!(
id = %record.id,
name = %record.tmux_name,
"spawn_managed (launch-on-main): 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 = %local_path.display(),
"managed session spawned successfully (launch-on-main, no worktree)"
);
}
emit(ProvisioningStage::Complete);
Ok(mgr.get(&record.id).await.unwrap_or(record))
}