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
16 changes: 4 additions & 12 deletions codex-rs/core/src/agent/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,24 +1165,16 @@ impl AgentControl {
let state = self.upgrade()?;
let mut children_by_parent = HashMap::<ThreadId, Vec<(ThreadId, AgentMetadata)>>::new();

for thread_id in state.list_thread_ids().await {
let Ok(thread) = state.get_thread(thread_id).await else {
continue;
};
let snapshot = thread.config_snapshot().await;
let Some(parent_thread_id) = thread_spawn_parent_thread_id(&snapshot.session_source)
else {
continue;
};
for (parent_thread_id, child_thread_id) in state.list_live_thread_spawn_edges().await {
children_by_parent
.entry(parent_thread_id)
.or_default()
.push((
thread_id,
child_thread_id,
self.state
.agent_metadata_for_thread(thread_id)
.agent_metadata_for_thread(child_thread_id)
.unwrap_or(AgentMetadata {
agent_id: Some(thread_id),
agent_id: Some(child_thread_id),
..Default::default()
}),
));
Expand Down
58 changes: 58 additions & 0 deletions codex-rs/core/src/agent/control_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2123,6 +2123,64 @@ async fn list_agent_subtree_thread_ids_includes_anonymous_and_closed_descendants
);
}

#[tokio::test]
async fn list_agent_subtree_thread_ids_includes_live_descendants_without_state_db() {
let (_home, config) = test_config().await;
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*state_db*/ None,
);
let control = manager.agent_control();
let parent_thread_id = manager
.start_thread(config.clone())
.await
.expect("parent should start")
.thread_id;

let child_thread_id = control
.spawn_agent(
config.clone(),
text_input("hello child"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_path: None,
agent_nickname: None,
agent_role: Some("explorer".to_string()),
})),
)
.await
.expect("child spawn should succeed");
let grandchild_thread_id = control
.spawn_agent(
config,
text_input("hello grandchild"),
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: child_thread_id,
depth: 2,
agent_path: None,
agent_nickname: None,
agent_role: Some("worker".to_string()),
})),
)
.await
.expect("grandchild spawn should succeed");

let mut subtree_thread_ids = manager
.list_agent_subtree_thread_ids(parent_thread_id)
.await
.expect("live subtree should load");
subtree_thread_ids.sort_by_key(ToString::to_string);
let mut expected_subtree_thread_ids =
vec![parent_thread_id, child_thread_id, grandchild_thread_id];
expected_subtree_thread_ids.sort_by_key(ToString::to_string);

assert_eq!(subtree_thread_ids, expected_subtree_thread_ids);
}

#[tokio::test]
async fn shutdown_agent_tree_closes_live_descendants() {
let harness = AgentControlHarness::new().await;
Expand Down
21 changes: 21 additions & 0 deletions codex-rs/core/src/thread_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,27 @@ impl ThreadManagerState {
.collect()
}

/// List parent-child edges for currently loaded thread-spawn agents.
pub(crate) async fn list_live_thread_spawn_edges(&self) -> Vec<(ThreadId, ThreadId)> {
self.threads
.read()
.await
.iter()
.filter_map(|(thread_id, thread)| {
if thread.session_source.is_internal() {
return None;
}
match &thread.session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
..
}) => Some((*parent_thread_id, *thread_id)),
_ => None,
}
})
.collect()
}

/// Fetch a thread by ID or return ThreadNotFound.
pub(crate) async fn get_thread(&self, thread_id: ThreadId) -> CodexResult<Arc<CodexThread>> {
let threads = self.threads.read().await;
Expand Down
Loading