use std::path::{Path, PathBuf};
use std::time::Duration;
use chrono::Utc;
use tracing::{info, warn};
use super::driver::ManagedTmuxDriver;
use super::manager::{ManagedError, SessionManager};
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
const CWD_PROBE_BACKOFF: [Duration; 2] = [Duration::from_millis(50), Duration::from_millis(150)];
pub(super) async fn resolve_adoptable_cwd(
tmux: &dyn ManagedTmuxDriver,
name: &str,
) -> Option<PathBuf> {
for attempt in 0..=CWD_PROBE_BACKOFF.len() {
if let Some(cwd) = tmux.get_pane_cwd(name).filter(|p| p.is_dir()) {
if attempt > 0 {
warn!(
name = %name,
attempt = attempt + 1,
"reconcile: pane working directory resolved only on retry — the first probe \
was a transient failure, and declining on it would have exposed a live pane \
to the orphan-GC (#6118)"
);
}
return Some(cwd);
}
if let Some(backoff) = CWD_PROBE_BACKOFF.get(attempt) {
tokio::time::sleep(*backoff).await;
}
}
None
}
pub(super) fn derive_source_id_for_record(record: &SessionRecord) -> Option<String> {
let path: &Path = record
.workspace_path
.as_deref()
.filter(|p| p.is_dir())
.or_else(|| {
let c = record.cwd.as_path();
(c != Path::new("/unknown") && c.is_dir()).then_some(c)
})?;
let gh = trusty_common::github_path::derive_github_path(path)?;
Some(format!("{}/{}", gh.owner, gh.repo))
}
pub(super) async fn backfill_source_ids(records: &mut [SessionRecord]) {
let to_derive: Vec<(usize, SessionRecord)> = records
.iter()
.enumerate()
.filter(|(_, r)| {
r.source_id.is_none() && !matches!(r.state, ManagedSessionState::Decommissioned)
})
.map(|(i, r)| (i, r.clone()))
.collect();
if to_derive.is_empty() {
return;
}
let derived: Vec<(usize, Option<String>)> = tokio::task::spawn_blocking(move || {
to_derive
.into_iter()
.map(|(idx, record)| (idx, derive_source_id_for_record(&record)))
.collect()
})
.await
.unwrap_or_default();
for (idx, sid) in derived {
if let Some(s) = sid {
info!(name = %records[idx].tmux_name, source_id = %s, "reconcile: backfilled source_id");
records[idx].source_id = Some(s);
}
}
}
impl SessionManager {
pub async fn adopt_existing(
&self,
tmux_name: &str,
cwd: PathBuf,
task: String,
runtime: crate::runtime::RuntimeKind,
ephemeral: bool,
) -> Result<SessionRecord, ManagedError> {
if !self.tmux_driver().session_exists(tmux_name) {
return Err(ManagedError::TmuxSessionMissing(tmux_name.to_string()));
}
let pane_id = self.tmux_driver().get_pane_id(tmux_name);
let live_names = self
.tmux
.list_sessions()
.map_err(|e| ManagedError::TmuxUnavailable(e.to_string()))?;
let build_record = |final_name: String| SessionRecord {
id: ManagedSessionId::new(),
tmux_name: final_name,
cwd: cwd.clone(),
task: task.clone(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime,
ephemeral,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: pane_id.clone(),
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
};
let mut store = self.store.write().await;
let existing = store.all().await?;
let colliding = existing
.iter()
.find(|r| r.tmux_name == tmux_name && !r.state.is_terminal())
.cloned();
let record = match colliding {
None => {
let record = build_record(tmux_name.to_string());
store.upsert(record.clone()).await?;
drop(store);
record
}
Some(existing_record) => {
let same_pane = match (existing_record.pane_id.as_deref(), pane_id.as_deref()) {
(Some(a), Some(b)) => a == b,
_ => true,
};
if same_pane {
return Err(ManagedError::AlreadyAdopted(tmux_name.to_string()));
}
let taken = Self::taken_name_set(&live_names, &existing, None);
let deduped = Self::dedupe_name_against(tmux_name, &taken);
drop(store);
self.tmux.rename_session(tmux_name, &deduped)?;
let mut store = self.store.write().await;
let records = match store.all().await {
Ok(r) => r,
Err(e) => {
drop(store);
return Err(self.rollback_adopt_rename(
&deduped,
tmux_name,
&format!("store reload failed: {e}"),
));
}
};
if records
.iter()
.any(|r| r.tmux_name == deduped && !r.state.is_terminal())
{
drop(store);
return Err(self.rollback_adopt_rename(
&deduped,
tmux_name,
&format!("'{deduped}' was claimed concurrently — retry the adopt"),
));
}
if let Some(stale) = records.iter().find(|r| {
r.id == existing_record.id
&& matches!(
r.state,
ManagedSessionState::Active | ManagedSessionState::Provisioning
)
}) {
let mut stale = stale.clone();
stale.state = ManagedSessionState::Stopped;
stale.last_activity_at = Some(Utc::now());
if let Err(e) = store.upsert(stale).await {
drop(store);
return Err(self.rollback_adopt_rename(
&deduped,
tmux_name,
&format!("store write failed: {e}"),
));
}
}
let record = build_record(deduped.clone());
if let Err(e) = store.upsert(record.clone()).await {
drop(store);
return Err(self.rollback_adopt_rename(
&deduped,
tmux_name,
&format!("store write failed: {e}"),
));
}
drop(store);
record
}
};
info!(
id = %record.id,
name = %record.tmux_name,
runtime = %runtime.as_str(),
"adopted existing tmux session into the managed store"
);
Ok(record)
}
fn rollback_adopt_rename(&self, deduped: &str, original: &str, cause: &str) -> ManagedError {
match self.tmux.rename_session(deduped, original) {
Ok(()) => ManagedError::InvalidState(
original.to_string(),
format!("adopt aborted and rolled back ({cause})"),
),
Err(rollback) => ManagedError::InvalidState(
original.to_string(),
format!(
"adopt half-applied: the live tmux session was renamed to '{deduped}' but \
nothing was persisted ({cause}), and the rollback failed ({rollback}) — \
manually run `tmux rename-session -t {deduped} {original}`"
),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::process::Command;
fn make_record_with_workspace(workspace: PathBuf) -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-test".into(),
cwd: workspace.clone(),
task: "test".into(),
state: crate::session_manager::ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(workspace),
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
}
}
fn make_record_unknown_cwd() -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-external".into(),
cwd: PathBuf::from("/unknown"),
task: "adopted session".into(),
state: crate::session_manager::ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: None,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
}
}
#[test]
fn derive_source_id_ssh_remote() {
let tmp = crate::test_support::hermetic_temp_dir();
let dir = tmp.path();
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return; }
git(&[
"remote",
"add",
"origin",
"git@github.com:test-owner/my-repo.git",
]);
let record = make_record_with_workspace(dir.to_path_buf());
let sid = derive_source_id_for_record(&record).expect("should derive");
assert_eq!(sid, "test-owner/my-repo");
}
#[test]
fn derive_source_id_https_remote() {
let tmp = crate::test_support::hermetic_temp_dir();
let dir = tmp.path();
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return;
}
git(&[
"remote",
"add",
"origin",
"https://github.com/acme-org/my-project.git",
]);
let record = make_record_with_workspace(dir.to_path_buf());
let sid = derive_source_id_for_record(&record).expect("should derive");
assert_eq!(sid, "acme-org/my-project");
}
#[test]
fn derive_source_id_none_for_no_remote() {
let tmp = crate::test_support::hermetic_temp_dir();
let dir = tmp.path();
Command::new("git")
.arg("-C")
.arg(dir)
.arg("init")
.output()
.ok();
let record = make_record_with_workspace(dir.to_path_buf());
assert!(derive_source_id_for_record(&record).is_none());
}
#[test]
fn derive_source_id_none_for_unknown_path() {
let record = make_record_unknown_cwd();
assert!(derive_source_id_for_record(&record).is_none());
}
}