Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/apps/desktop/src/api/session_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,10 @@ pub async fn load_persisted_session_metadata(
let manager = PersistenceManager::new(path_manager.inner().clone())
.map_err(|e| format!("Failed to create persistence manager: {}", e))?;

manager
let metadata = manager
.load_session_metadata(&workspace_path, &request.session_id)
.await
.map_err(|e| format!("Failed to load persisted session metadata: {}", e))
.map_err(|e| format!("Failed to load persisted session metadata: {}", e))?;

Ok(metadata.filter(|metadata| !metadata.should_hide_from_user_lists()))
}
12 changes: 7 additions & 5 deletions src/crates/core/src/agentic/coordination/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use super::{scheduler::DialogSubmissionPolicy, turn_outcome::TurnOutcome};
use crate::agentic::agents::get_agent_registry;
use crate::agentic::core::{
has_prompt_markup, Message, MessageContent, ProcessingPhase, PromptEnvelope, Session,
SessionConfig, SessionState, SessionSummary, TurnStats,
SessionConfig, SessionKind, SessionState, SessionSummary, TurnStats,
};
use crate::agentic::events::{
AgenticEvent, EventPriority, EventQueue, EventRouter, EventSubscriber,
Expand Down Expand Up @@ -625,6 +625,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
session_name: "Recovered Session".to_string(),
agent_type: "agentic".to_string(),
created_by: None,
session_kind: SessionKind::Standard,
model_name: "default".to_string(),
created_at: now_ms,
last_active_at: now_ms,
Expand Down Expand Up @@ -688,15 +689,16 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
session_name: String,
agent_type: String,
config: SessionConfig,
creator_session_id: Option<&str>,
parent_info: &SubagentParentInfo,
) -> BitFunResult<Session> {
self.session_manager
.create_session_with_id_and_creator(
.create_session_with_id_and_details(
None,
session_name,
agent_type,
config,
creator_session_id.map(|session_id| format!("session-{}", session_id)),
Some(format!("session-{}", parent_info.session_id)),
SessionKind::Subagent,
)
.await
}
Expand Down Expand Up @@ -1884,7 +1886,7 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet
format!("Subagent: {}", task_description),
agent_type.clone(),
subagent_config,
Some(&subagent_parent_info.session_id),
&subagent_parent_info,
)
.await?;

Expand Down
2 changes: 1 addition & 1 deletion src/crates/core/src/agentic/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ pub use prompt_markup::{
has_prompt_markup, is_system_reminder_only, render_system_reminder, render_user_query,
strip_prompt_markup, PromptBlock, PromptBlockKind, PromptEnvelope,
};
pub use session::{CompressionState, Session, SessionConfig, SessionSummary};
pub use session::{CompressionState, Session, SessionConfig, SessionKind, SessionSummary};
pub use state::{ProcessingPhase, SessionState, ToolExecutionState};
14 changes: 14 additions & 0 deletions src/crates/core/src/agentic/core/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ use serde::{Deserialize, Serialize};
use std::time::SystemTime;
use uuid::Uuid;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SessionKind {
#[default]
Standard,
Subagent,
}

// ============ Session ============

/// Session: contains multiple dialog turns
Expand All @@ -18,6 +26,8 @@ pub struct Session {
alias = "createdBy"
)]
pub created_by: Option<String>,
#[serde(default, alias = "session_kind", alias = "sessionKind")]
pub kind: SessionKind,

/// Associated resources
#[serde(
Expand Down Expand Up @@ -78,6 +88,7 @@ impl Session {
session_name,
agent_type,
created_by: None,
kind: SessionKind::Standard,
snapshot_session_id: None,
dialog_turn_ids: vec![],
state: SessionState::Idle,
Expand All @@ -101,6 +112,7 @@ impl Session {
session_name,
agent_type,
created_by: None,
kind: SessionKind::Standard,
snapshot_session_id: None,
dialog_turn_ids: vec![],
state: SessionState::Idle,
Expand Down Expand Up @@ -173,6 +185,8 @@ pub struct SessionSummary {
alias = "createdBy"
)]
pub created_by: Option<String>,
#[serde(default, alias = "session_kind", alias = "sessionKind")]
pub kind: SessionKind,
pub turn_count: usize,
pub created_at: SystemTime,
pub last_activity_at: SystemTime,
Expand Down
133 changes: 115 additions & 18 deletions src/crates/core/src/agentic/persistence/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use crate::agentic::core::{
strip_prompt_markup, CompressionState, Message, MessageContent, Session, SessionConfig,
SessionState, SessionSummary,
};
use crate::infrastructure::PathManager;
use crate::service::remote_ssh::workspace_state::{
resolve_workspace_session_identity, LOCAL_WORKSPACE_SSH_HOST,
};
use crate::infrastructure::PathManager;
use crate::service::session::{
DialogTurnData, SessionMetadata, SessionStatus, SessionTranscriptExport,
SessionTranscriptExportOptions, SessionTranscriptIndexEntry, ToolItemData, TranscriptLineRange,
Expand Down Expand Up @@ -581,18 +581,17 @@ impl PersistenceManager {
.or_else(|| existing.map(|value| value.model_name.clone()))
.unwrap_or_else(|| "default".to_string());

let resolved_identity = if let Some(workspace_root) =
session.config.workspace_path.as_deref()
{
resolve_workspace_session_identity(
workspace_root,
session.config.remote_connection_id.as_deref(),
session.config.remote_ssh_host.as_deref(),
)
.await
} else {
None
};
let resolved_identity =
if let Some(workspace_root) = session.config.workspace_path.as_deref() {
resolve_workspace_session_identity(
workspace_root,
session.config.remote_connection_id.as_deref(),
session.config.remote_ssh_host.as_deref(),
)
.await
} else {
None
};

let workspace_root = resolved_identity
.as_ref()
Expand Down Expand Up @@ -620,6 +619,7 @@ impl PersistenceManager {
.created_by
.clone()
.or_else(|| existing.and_then(|value| value.created_by.clone())),
session_kind: session.kind,
model_name,
created_at,
last_active_at,
Expand Down Expand Up @@ -1161,7 +1161,7 @@ impl PersistenceManager {
}
}

async fn rebuild_index_locked(
async fn scan_session_metadata_dirs(
&self,
workspace_path: &Path,
) -> BitFunResult<Vec<SessionMetadata>> {
Expand Down Expand Up @@ -1200,15 +1200,28 @@ impl PersistenceManager {

metadata_list.sort_by(|a, b| b.last_active_at.cmp(&a.last_active_at));

Ok(metadata_list)
}

async fn rebuild_index_locked(
&self,
workspace_path: &Path,
) -> BitFunResult<Vec<SessionMetadata>> {
let metadata_list = self.scan_session_metadata_dirs(workspace_path).await?;
let visible_sessions = metadata_list
.into_iter()
.filter(|metadata| !metadata.should_hide_from_user_lists())
.collect::<Vec<_>>();

let index = StoredSessionIndex {
schema_version: SESSION_SCHEMA_VERSION,
updated_at: Self::system_time_to_unix_ms(SystemTime::now()),
sessions: metadata_list.clone(),
sessions: visible_sessions.clone(),
};
self.write_json_atomic(&self.index_path(workspace_path), &index)
.await?;

Ok(metadata_list)
Ok(visible_sessions)
}

async fn upsert_index_entry_locked(
Expand Down Expand Up @@ -1319,6 +1332,17 @@ impl PersistenceManager {
self.rebuild_index_locked(workspace_path).await
}

pub async fn list_session_metadata_including_internal(
&self,
workspace_path: &Path,
) -> BitFunResult<Vec<SessionMetadata>> {
if !workspace_path.exists() {
return Ok(Vec::new());
}

self.scan_session_metadata_dirs(workspace_path).await
}

pub async fn save_session_metadata(
&self,
workspace_path: &Path,
Expand All @@ -1337,7 +1361,12 @@ impl PersistenceManager {
&file,
)
.await?;
self.upsert_index_entry(workspace_path, metadata).await
if !metadata.should_hide_from_user_lists() {
self.upsert_index_entry(workspace_path, metadata).await
} else {
self.remove_index_entry(workspace_path, &metadata.session_id)
.await
}
}

pub async fn load_session_metadata(
Expand Down Expand Up @@ -1581,6 +1610,7 @@ impl PersistenceManager {
session_name: metadata.session_name.clone(),
agent_type: metadata.agent_type.clone(),
created_by: metadata.created_by.clone(),
kind: metadata.session_kind,
snapshot_session_id: stored_state
.and_then(|value| value.snapshot_session_id)
.or(metadata.snapshot_session_id.clone()),
Expand Down Expand Up @@ -1655,6 +1685,7 @@ impl PersistenceManager {
session_name: metadata.session_name,
agent_type: metadata.agent_type,
created_by: metadata.created_by,
kind: metadata.session_kind,
turn_count: metadata.turn_count,
created_at: Self::unix_ms_to_system_time(metadata.created_at),
last_activity_at: Self::unix_ms_to_system_time(metadata.last_active_at),
Expand Down Expand Up @@ -2087,12 +2118,12 @@ impl PersistenceManager {
}
Ok(())
}

}

#[cfg(test)]
mod tests {
use super::PersistenceManager;
use crate::agentic::core::SessionKind;
use crate::infrastructure::PathManager;
use crate::service::session::{
DialogTurnData, SessionMetadata, SessionTranscriptExportOptions, UserMessageData,
Expand Down Expand Up @@ -2212,4 +2243,70 @@ mod tests {
assert!(transcript.contains("## Turn 0"));
assert!(transcript.contains("hello transcript"));
}

#[tokio::test]
async fn subagent_session_kind_is_hidden_from_visible_session_index() {
let workspace = TestWorkspace::new();
let manager = PersistenceManager::new(Arc::new(PathManager::new().expect("path manager")))
.expect("persistence manager");

let mut metadata = SessionMetadata::new(
Uuid::new_v4().to_string(),
"Subagent: repo sweep".to_string(),
"Explore".to_string(),
"model".to_string(),
);
metadata.session_kind = SessionKind::Subagent;

manager
.save_session_metadata(workspace.path(), &metadata)
.await
.expect("metadata should save");

let visible = manager
.list_session_metadata(workspace.path())
.await
.expect("visible metadata should load");
let raw = manager
.list_session_metadata_including_internal(workspace.path())
.await
.expect("raw metadata should load");

assert!(visible.is_empty());
assert_eq!(raw.len(), 1);
assert!(raw[0].is_subagent());
}

#[tokio::test]
async fn legacy_leaked_subagent_is_hidden_from_visible_session_index() {
let workspace = TestWorkspace::new();
let manager = PersistenceManager::new(Arc::new(PathManager::new().expect("path manager")))
.expect("persistence manager");

let mut metadata = SessionMetadata::new(
Uuid::new_v4().to_string(),
"Subagent: stale task".to_string(),
"Explore".to_string(),
"model".to_string(),
);
metadata.created_by = Some("session-parent".to_string());

manager
.save_session_metadata(workspace.path(), &metadata)
.await
.expect("metadata should save");

let visible = manager
.list_session_metadata(workspace.path())
.await
.expect("visible metadata should load");
let raw = manager
.list_session_metadata_including_internal(workspace.path())
.await
.expect("raw metadata should load");

assert!(visible.is_empty());
assert_eq!(raw.len(), 1);
assert!(raw[0].is_legacy_leaked_subagent_candidate());
}
}
4 changes: 4 additions & 0 deletions src/crates/core/src/agentic/persistence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
//! Responsible for persistent storage and loading of data

pub mod manager;
pub mod session_workspace_maintenance;

pub use manager::PersistenceManager;
pub use session_workspace_maintenance::{
SessionWorkspaceMaintenanceReport, SessionWorkspaceMaintenanceService,
};
Loading
Loading