use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use trusty_common::residency::{ACTIVE_PROJECT_SET_SCHEMA, ActiveProject, ActiveProjectSet};
use crate::daemon::rpc::managed::outcome::RouteOutcome;
use crate::daemon::state::DaemonState;
use crate::session_manager::{ManagedSessionId, ManagedSessionState, SessionRecord};
struct ProjectAccum {
root: PathBuf,
repo_url: Option<String>,
session_ids: Vec<ManagedSessionId>,
session_id_strings: Vec<String>,
last_activity_unix: Option<u64>,
}
pub(crate) async fn active_projects_core(state: &Arc<DaemonState>) -> RouteOutcome {
let mgr = state.session_manager().await;
let sessions: Vec<SessionRecord> = mgr.list().await;
let live_tmux_names: Option<HashSet<String>> = match mgr.tmux_driver().list_sessions() {
Ok(names) => Some(names.into_iter().collect()),
Err(e) => {
tracing::warn!(
error = %e,
"mpm.residency.active: tmux list_sessions failed; treating every \
persisted Active/Provisioning session as active (fail closed)"
);
None
}
};
let mut groups: HashMap<PathBuf, ProjectAccum> = HashMap::new();
for record in &sessions {
let persisted_active = matches!(
record.state,
ManagedSessionState::Active | ManagedSessionState::Provisioning
);
if !persisted_active {
continue;
}
let tmux_confirmed = live_tmux_names
.as_ref()
.is_none_or(|names| names.contains(&record.tmux_name));
if !tmux_confirmed {
continue;
}
let root = record
.workspace_path
.clone()
.unwrap_or_else(|| record.cwd.clone());
let last_activity_unix = record
.last_activity_at
.map(|dt| dt.timestamp().max(0) as u64);
let accum = groups.entry(root.clone()).or_insert_with(|| ProjectAccum {
root: root.clone(),
repo_url: None,
session_ids: Vec::new(),
session_id_strings: Vec::new(),
last_activity_unix: None,
});
if accum.repo_url.is_none() {
accum.repo_url = record.repo_url.clone();
}
accum.session_ids.push(record.id);
accum.session_id_strings.push(record.id.to_string());
accum.last_activity_unix = match (accum.last_activity_unix, last_activity_unix) {
(Some(a), Some(b)) => Some(a.max(b)),
(Some(a), None) => Some(a),
(None, b) => b,
};
}
let mut projects = Vec::with_capacity(groups.len());
for accum in groups.into_values() {
let (palace_id, index_ids) = derive_for_group(&mgr, &accum).await;
let mut project = ActiveProject::default();
project.root = accum.root;
project.palace_id = palace_id;
project.index_ids = index_ids;
project.session_ids = accum.session_id_strings;
project.last_activity_unix = accum.last_activity_unix;
projects.push(project);
}
let published_at_unix = chrono::Utc::now().timestamp().max(0) as u64;
let mut set = ActiveProjectSet::default();
set.schema = ACTIVE_PROJECT_SET_SCHEMA;
set.generation = mgr.residency_generation();
set.published_at_unix = published_at_unix;
set.projects = projects;
RouteOutcome::ok(&set)
}
async fn derive_for_group(
mgr: &crate::session_manager::SessionManager,
accum: &ProjectAccum,
) -> (Option<String>, Vec<String>) {
let representative = accum.session_ids[0];
if let Some(cached) = mgr
.residency_cache_lookup(&representative, &accum.root)
.await
{
return cached;
}
let palace_id =
crate::core::session_launch::resolve_palace_slug(&accum.root, accum.repo_url.as_deref());
let mut index_ids = vec![trusty_common::project_index_id::derive_project_index_id(
&accum.root,
)];
if crate::core::worktree_index::is_git_worktree(&accum.root)
&& let Some(base) = crate::core::base_facet_index::base_checkout_of(&accum.root)
{
index_ids.push(trusty_common::project_index_id::derive_project_index_id(
&base,
));
}
for &id in &accum.session_ids {
mgr.residency_cache_store(id, accum.root.clone(), palace_id.clone(), index_ids.clone())
.await;
}
(palace_id, index_ids)
}
#[cfg(test)]
#[path = "residency_tests.rs"]
mod residency_tests;