use std::path::{Path, PathBuf};
use chrono::Utc;
use tracing::{info, warn};
use super::manager::{ManagedError, ReconcileReport, SessionManager};
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord, StopCause};
impl SessionManager {
pub async fn reconcile_on_boot(
&self,
auto_resume: bool,
) -> Result<ReconcileReport, ManagedError> {
let live_names: Option<std::collections::HashSet<String>> =
match self.observed_live_managed_names() {
Ok(names) => Some(names),
Err(e) => {
warn!(
"reconcile: tmux liveness could not be observed ({e}); leaving every \
record's state untouched — no session is marked Stopped, queued for \
auto-resume, or adopted without positive evidence about its pane"
);
None
}
};
let mut report = ReconcileReport::default();
let mut guard = self.store.write().await;
let mut all_records = guard.all().await?;
let known_names: std::collections::HashSet<String> =
all_records.iter().map(|r| r.tmux_name.clone()).collect();
let mut to_resume: Vec<ManagedSessionId> = Vec::new();
super::adopt::backfill_source_ids(&mut all_records).await;
for mut record in all_records {
if record.state.is_terminal() {
if live_names
.as_ref()
.is_some_and(|live| live.contains(&record.tmux_name))
&& !record.is_leaked_test_adoption()
{
warn!(
id = %record.id,
name = %record.tmux_name,
state = ?record.state,
"reconcile: terminal record has a LIVE tmux session — the picker hides \
this row. If the session is genuinely in use, revive it with: curl -sS \
-X POST $TRUSTY_MPM_URL/api/v1/sessions/managed/{}/reactivate",
record.id
);
}
if record.pending_decision.is_some() || record.proposed_default.is_some() {
record.pending_decision = None;
record.proposed_default = None;
report.stale_decisions_cleared.push(record.id.to_string());
warn!(
id = %record.id,
name = %record.tmux_name,
"reconcile: cleared stale pending_decision on terminal record (#4400 backfill)"
);
guard.upsert(record).await?;
}
continue;
}
if record.is_leaked_test_adoption() {
record.set_lifecycle_state(ManagedSessionState::Deleted, Utc::now());
record.pending_decision = None;
record.proposed_default = None;
report.reserved_test_swept.push(record.id.to_string());
warn!(
id = %record.id,
name = %record.tmux_name,
"reconcile: tombstoned an adopted reserved test-namespace record (#6116) — \
a leaked test session is not a session to keep, resume or relaunch; its \
pane goes back to the orphan-GC"
);
guard.upsert(record).await?;
continue;
}
let Some(live_names) = live_names.as_ref() else {
continue;
};
if live_names.contains(&record.tmux_name) {
record.state = ManagedSessionState::Active;
record.stop_cause = None;
report.adopted.push(record.tmux_name.clone());
info!(name = %record.tmux_name, "reconcile: re-adopted live session");
} else {
record.state = ManagedSessionState::Stopped;
record.stop_cause.get_or_insert(StopCause::Unexpected);
report.stopped.push(record.id.to_string());
warn!(name = %record.tmux_name, "reconcile: tmux session gone, marked Stopped (workspace intact, resumable)");
if auto_resume && record.is_auto_resumable() {
to_resume.push(record.id);
}
}
guard.upsert(record).await?;
}
let known_workspaces: std::collections::HashSet<PathBuf> = guard
.all()
.await?
.iter()
.filter(|r| !matches!(r.state, ManagedSessionState::Decommissioned))
.filter_map(|r| r.workspace_path.clone())
.filter(|p| p.as_path() != Path::new(super::record::UNRESOLVED_PATH_SENTINEL))
.map(|p| std::fs::canonicalize(&p).unwrap_or(p))
.collect();
let mut newly_resolved: Vec<(ManagedSessionId, PathBuf)> = Vec::new();
for name in live_names.iter().flatten() {
if known_names.contains(name) {
continue;
}
if crate::core::names::is_reserved_test_session_name(name) {
warn!(
name = %name,
"reconcile: refusing to adopt a reserved test-namespace session (#6116) — \
it leaked from a test process that died before its cleanup ran. Left \
untracked for the orphan-GC, which kills an idle shell with no live child \
on two consecutive sweeps"
);
report.reserved_test_refused.push(name.clone());
continue;
}
let Some(resolved_cwd) = super::adopt::resolve_adoptable_cwd(&*self.tmux, name).await
else {
warn!(
name = %name,
"reconcile: declining external-adopt — the pane's working directory did not \
resolve on any attempt, so there is nothing to track, resume or attach to \
(#6118). The pane is left to the orphan-GC, which kills it only if it is an \
idle shell with no live child on two consecutive sweeps; `tmux attach -t \
<name>` still reaches it until then"
);
report.adoption_declined.push(name.clone());
continue;
};
let canon =
std::fs::canonicalize(&resolved_cwd).unwrap_or_else(|_| resolved_cwd.clone());
if known_workspaces.contains(&canon) {
warn!(
name = %name,
workspace = %resolved_cwd.display(),
"reconcile: skipping external-adopt — live tmux session resolves to \
a workspace already tracked by another managed session record; its \
tmux_name may be stale/crossed (#3396) — investigate with `tm sessions \
ls` and `tmux list-panes` rather than auto-adopting a duplicate"
);
continue;
}
let id = ManagedSessionId::for_adopted_tmux_name(name);
let external = SessionRecord {
id,
tmux_name: name.clone(),
cwd: resolved_cwd.clone(),
task: super::record::ADOPTED_TASK.to_string(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(resolved_cwd.clone()),
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: crate::runtime::RuntimeKind::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: self.tmux.get_pane_id(name),
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
};
newly_resolved.push((id, resolved_cwd));
guard.upsert(external).await?;
report.external_adopted.push(name.clone());
info!(name = %name, "reconcile: adopted external managed session");
}
drop(guard);
self.dedup_stale_duplicates(&mut to_resume).await?;
for (id, workspace) in newly_resolved {
let fw = crate::core::paths::FrameworkPaths::for_managed_workspace(&workspace);
let outcome = crate::core::deploy_validate::validate_and_repair(&fw, &workspace, None);
if outcome.before.is_complete() {
continue;
}
if outcome.is_complete() {
info!(id = %id, "reconcile: auto-repaired adopted session's deployment");
} else {
warn!(
id = %id,
gaps = outcome.after.gaps.len(),
"reconcile: adopted session's deployment remains incomplete after auto-repair"
);
}
}
if auto_resume && !to_resume.is_empty() {
info!(
"reconcile: auto_resume=true, resuming {} stopped sessions",
to_resume.len()
);
for sid in to_resume {
if let Err(e) = self.resume(&sid).await {
warn!(id = %sid, "reconcile: auto_resume failed: {e}");
}
}
}
Ok(report)
}
}