Skip to content
Open
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
18 changes: 18 additions & 0 deletions codex-rs/core/src/agent/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use crate::session::emit_subagent_session_started;
use crate::session_prefix::format_inter_agent_completion_message;
use crate::session_prefix::format_subagent_context_line;
use crate::session_prefix::format_subagent_notification_message;
use crate::thread_activity::ThreadActivityGate;
use crate::thread_activity::ThreadActivityHandle;
use crate::thread_activity::ThreadActivityRegistrationError;
use crate::thread_manager::ResumeThreadWithHistoryOptions;
use crate::thread_manager::ThreadManagerState;
use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;
Expand Down Expand Up @@ -103,6 +106,7 @@ pub(crate) struct AgentControl {
state: Arc<AgentRegistry>,
v2_residency: Arc<V2Residency>,
agent_execution_limiter: Arc<AgentExecutionLimiter>,
thread_activity_gate: Arc<ThreadActivityGate>,
/// Session-scoped state shared by the root thread and every cloned sub-agent control handle.
rollout_budget: Arc<RolloutBudget>,
}
Expand All @@ -113,8 +117,13 @@ impl AgentControl {
manager: Weak<ThreadManagerState>,
rollout_budget: Option<RolloutBudgetConfig>,
) -> Self {
let thread_activity_gate = manager
.upgrade()
.map(|manager| manager.thread_activity_gate())
.unwrap_or_default();
let control = Self {
manager,
thread_activity_gate,
..Default::default()
};
if let Some(rollout_budget) = rollout_budget {
Expand All @@ -133,6 +142,15 @@ impl AgentControl {
self.session_id
}

pub(crate) fn register_thread_activity(
&self,
thread_id: ThreadId,
parent_thread_id: Option<ThreadId>,
) -> Result<ThreadActivityHandle, ThreadActivityRegistrationError> {
self.thread_activity_gate
.register(thread_id, parent_thread_id)
}

pub(crate) fn rollout_budget(&self) -> &RolloutBudget {
self.rollout_budget.as_ref()
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/codex_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub(crate) async fn run_codex_thread_interactive(
}))
.or_cancel(&cancel_token)
.await??;
codex.session.thread_activity.mark_initialized();
let thread_config = codex.thread_config_snapshot().await;
let client_metadata = parent_session.app_server_client_metadata().await;
emit_subagent_session_started(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pub use codex_protocol::config_types::ModelProviderAuthInfo;
mod event_mapping;
pub mod review_format;
pub use codex_prompts as review_prompts;
mod thread_activity;
mod thread_manager;
pub(crate) mod web_search;
pub(crate) mod windows_sandbox_read_grants;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/session/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@ pub(super) async fn submission_loop(
warn!("failed to shutdown thread persistence after submission channel closed: {err}");
}
}
sess.thread_activity.mark_closed();
Comment thread
winston-openai marked this conversation as resolved.
debug!("Agent loop exited");
}

Expand Down
7 changes: 7 additions & 0 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::shell_snapshot::ShellSnapshot;
use crate::skills::SkillError;
use crate::state::ActiveTurn;
use crate::thread_activity::ThreadActivityHandle;
use codex_extension_api::ExtensionDataInit;
use codex_login::auth::AgentIdentityAuthPolicy;
use codex_protocol::SessionId;
Expand Down Expand Up @@ -145,6 +146,7 @@ pub(crate) struct Session {
pub(crate) conversation: Arc<RealtimeConversationManager>,
pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
pub(crate) submission_lifecycle: SubmissionLifecycle,
pub(crate) thread_activity: ThreadActivityHandle,
pub(crate) submission_send_lock: Mutex<()>,
pub(crate) input_queue: InputQueue,
pub(crate) guardian_review_session: GuardianReviewSessionManager,
Expand Down Expand Up @@ -630,6 +632,7 @@ impl Session {
session_configuration.forked_from_thread_id = forked_from_id;
let parent_thread_id = session_configuration
.parent_thread_id
.or_else(|| session_configuration.session_source.parent_thread_id())
.or_else(|| initial_history.get_resumed_parent_thread_id());
session_configuration.parent_thread_id = parent_thread_id;
let multi_agent_version = multi_agent_version.map(OnceLock::from).unwrap_or_default();
Expand Down Expand Up @@ -1238,6 +1241,9 @@ impl Session {
tool_search_handler_cache: Default::default(),
turn_environments: Arc::clone(&turn_environments),
};
let thread_activity = services
.agent_control
.register_thread_activity(thread_id, parent_thread_id)?;
let sess = Arc::new(Session {
thread_id,
installation_id,
Expand All @@ -1251,6 +1257,7 @@ impl Session {
conversation: Arc::new(RealtimeConversationManager::new()),
active_turn: Mutex::new(None),
submission_lifecycle: SubmissionLifecycle::default(),
thread_activity,
submission_send_lock: Mutex::new(()),
input_queue: InputQueue::new(),
guardian_review_session: GuardianReviewSessionManager::default(),
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5507,6 +5507,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
"turn_id".to_string(),
skills_snapshot,
);
let thread_activity = Arc::new(crate::thread_activity::ThreadActivityGate::default())
.register(thread_id, /*parent_thread_id*/ None)
.expect("test thread activity registration");
thread_activity.mark_initialized();
let session = Session {
thread_id,
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
Expand All @@ -5520,6 +5524,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
conversation: Arc::new(RealtimeConversationManager::new()),
active_turn: Mutex::new(None),
submission_lifecycle: SubmissionLifecycle::default(),
thread_activity,
submission_send_lock: Mutex::new(()),
input_queue: super::input_queue::InputQueue::new(),
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
Expand Down Expand Up @@ -7783,6 +7788,10 @@ where
"turn_id".to_string(),
skills_snapshot,
));
let thread_activity = Arc::new(crate::thread_activity::ThreadActivityGate::default())
.register(thread_id, /*parent_thread_id*/ None)
.expect("test thread activity registration");
thread_activity.mark_initialized();
let session = Arc::new(Session {
thread_id,
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
Expand All @@ -7796,6 +7805,7 @@ where
conversation: Arc::new(RealtimeConversationManager::new()),
active_turn: Mutex::new(None),
submission_lifecycle: SubmissionLifecycle::default(),
thread_activity,
submission_send_lock: Mutex::new(()),
input_queue: super::input_queue::InputQueue::new(),
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/core/src/session_rollout_init_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::io::ErrorKind;
use std::path::Path;

use crate::rollout::SESSIONS_SUBDIR;
use crate::thread_activity::ThreadActivityRegistrationError;
use codex_protocol::error::CodexErr;
use codex_thread_store::ThreadStoreError;

Expand All @@ -13,6 +14,13 @@ pub(crate) fn map_session_init_error(err: &anyhow::Error, codex_home: &Path) ->
return CodexErr::UnsupportedOperation(format!("{operation} is not supported yet"));
}

if let Some(registration_error) = err
.chain()
.find_map(|cause| cause.downcast_ref::<ThreadActivityRegistrationError>())
{
return CodexErr::InvalidRequest(registration_error.to_string());
}

if let Some(mapped) = err
.chain()
.filter_map(|cause| cause.downcast_ref::<std::io::Error>())
Expand Down
221 changes: 221 additions & 0 deletions codex-rs/core/src/thread_activity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use codex_protocol::ThreadId;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::Mutex;

#[derive(Default)]
pub(crate) struct ThreadActivityGate {
state: Mutex<ThreadActivityState>,
}

#[derive(Default)]
struct ThreadActivityState {
next_generation: u64,
nodes: HashMap<ThreadId, ThreadActivityNode>,
}

struct ThreadActivityNode {
generation: u64,
// A lineage edge follows the stable logical thread ID across runtime generations.
parent_thread_id: Option<ThreadId>,
active: usize,
closing: bool,
handle_live: bool,
initializing: bool,
}

#[derive(Debug, thiserror::Error)]
#[error("thread or ancestor is shutting down")]
pub(crate) struct ThreadActivityRegistrationError;

pub(crate) struct ThreadActivityHandle {
gate: Arc<ThreadActivityGate>,
thread_id: ThreadId,
generation: u64,
}

impl ThreadActivityGate {
pub(crate) fn register(
self: &Arc<Self>,
thread_id: ThreadId,
parent_thread_id: Option<ThreadId>,
) -> Result<ThreadActivityHandle, ThreadActivityRegistrationError> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

if state
.nodes
.get(&thread_id)
.is_some_and(|node| !node.closing || node.active > 0)
|| parent_thread_id.is_some_and(|parent_thread_id| {
Self::ancestor_blocks_registration(&state, parent_thread_id, thread_id)
})
|| Self::has_active_descendant(&state, thread_id)
{
return Err(ThreadActivityRegistrationError);
}

state.next_generation = state.next_generation.wrapping_add(1);
let generation = state.next_generation;
state.nodes.insert(
thread_id,
ThreadActivityNode {
generation,
parent_thread_id,
active: 1,
closing: false,
handle_live: true,
initializing: true,
},
);
Ok(ThreadActivityHandle {
gate: Arc::clone(self),
thread_id,
generation,
})
}

fn ancestor_blocks_registration(
state: &ThreadActivityState,
mut ancestor_id: ThreadId,
descendant_id: ThreadId,
) -> bool {
let mut visited = HashSet::new();
while visited.insert(ancestor_id) {
if ancestor_id == descendant_id {
return true;
}
let Some(node) = state.nodes.get(&ancestor_id) else {
return false;
};
if node.closing {
return true;
}
let Some(parent_thread_id) = node.parent_thread_id else {
return false;
};
ancestor_id = parent_thread_id;
}
true
}

fn has_active_descendant(state: &ThreadActivityState, ancestor_id: ThreadId) -> bool {
state.nodes.iter().any(|(thread_id, node)| {
if *thread_id == ancestor_id || node.active == 0 {
return false;
}
let mut parent_thread_id = node.parent_thread_id;
let mut visited = HashSet::new();
while let Some(parent_id) = parent_thread_id
&& visited.insert(parent_id)
{
if parent_id == ancestor_id {
return true;
}
parent_thread_id = state
.nodes
.get(&parent_id)
.and_then(|node| node.parent_thread_id);
}
false
})
}

fn mark_closed(&self, thread_id: ThreadId, generation: u64) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(node) = state.nodes.get_mut(&thread_id) else {
return;
};
if node.generation == generation {
node.closing = true;
}
}

fn mark_initialized(&self, thread_id: ThreadId, generation: u64) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(node) = state.nodes.get_mut(&thread_id) else {
return;
};
if node.generation == generation && node.initializing {
node.initializing = false;
node.active = node.active.saturating_sub(1);
}
}

fn prune_inactive_orphaned(
state: &mut ThreadActivityState,
mut thread_id: ThreadId,
mut generation: u64,
) {
loop {
let has_children = state
.nodes
.values()
.any(|node| node.parent_thread_id == Some(thread_id));
let Some(node) = state.nodes.get(&thread_id) else {
return;
};
if node.generation != generation || node.handle_live || node.active != 0 || has_children
{
return;
}
let parent_thread_id = node.parent_thread_id;
state.nodes.remove(&thread_id);
let Some(parent_id) = parent_thread_id else {
return;
};
let Some(parent) = state.nodes.get(&parent_id) else {
return;
};
thread_id = parent_id;
generation = parent.generation;
}
}
}

impl ThreadActivityHandle {
pub(crate) fn mark_initialized(&self) {
self.gate.mark_initialized(self.thread_id, self.generation);
}

pub(crate) fn mark_closed(&self) {
self.gate.mark_closed(self.thread_id, self.generation);
}
}

impl Drop for ThreadActivityHandle {
fn drop(&mut self) {
let mut state = self
.gate
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(node) = state.nodes.get_mut(&self.thread_id) else {
return;
};
if node.generation == self.generation {
node.handle_live = false;
node.active = 0;
node.closing = true;
node.initializing = false;
ThreadActivityGate::prune_inactive_orphaned(
&mut state,
self.thread_id,
self.generation,
);
}
}
}

#[cfg(test)]
#[path = "thread_activity_tests.rs"]
mod tests;
Loading
Loading