use std::path::{Path, PathBuf};
use chrono::Utc;
use tracing::info;
use super::manager::{ManagedError, SessionManager};
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
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 record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: tmux_name.to_string(),
cwd,
task,
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,
};
{
let mut store = self.store.write().await;
if store.all().await?.iter().any(|r| r.tmux_name == tmux_name) {
return Err(ManagedError::AlreadyAdopted(tmux_name.to_string()));
}
store.upsert(record.clone()).await?;
}
info!(
id = %record.id,
name = %tmux_name,
runtime = %runtime.as_str(),
"adopted existing tmux session into the managed store"
);
Ok(record)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::process::Command;
use tempfile::TempDir;
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,
}
}
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,
}
}
#[test]
fn derive_source_id_ssh_remote() {
let tmp = TempDir::new().expect("tempdir");
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 = TempDir::new().expect("tempdir");
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 = TempDir::new().expect("tempdir");
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());
}
}