use std::sync::Arc;
use tracing::{info, warn};
use super::deployment_check::{RepairHost, ensure_deployment_complete};
use super::lifecycle::{
SpawnParams, front_gate_or_escalate, prepare_inproject_session, resolve_gh_env,
};
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,
source: &super::local_only_spawn::SessionSource,
) -> 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(&source.name),
local_path,
|_| false,
)
.await
.map_err(|e| format!("name resolution failed for session {session_id}: {e}"))?;
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(local_path);
let memory_reachable =
prepare_inproject_session(&fw, session_id, local_path, source.repo_url.as_deref())?;
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()),
source.repo_url.clone(),
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()
})?;
if let Some(source_id) = &source.source_id
&& 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,
RepairHost::new(memory_reachable, state.user_home()),
) {
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 tmux_driver = tmux_arc.clone();
let adapter = crate::runtime::build_adapter(
record.runtime,
tmux_arc,
memory_reachable,
state.framework_root(),
);
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 {
super::launch_verify::record_spawn_outcome(&mgr, tmux_driver.as_ref(), &record).await;
}
emit(ProvisioningStage::Complete);
Ok(mgr.get(&record.id).await.unwrap_or(record))
}