From b528ff02b6504e8399a5826900ada9a392e6bb48 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 20 Apr 2026 10:32:20 +0100 Subject: [PATCH] chore: morpheus to path (#18353) Make the morpheus agent (which is the phase 2 memories agent) follow the agent-v2 path system by naming it `/morpheus`. To maintain the path primitive this means moving it to a dedicated `AgentControl` Co-authored-by: Codex --- codex-rs/core/src/agent/control.rs | 9 +++++++++ codex-rs/core/src/memories/phase2.rs | 8 ++++---- codex-rs/core/src/memories/tests.rs | 14 ++++++++++++++ codex-rs/protocol/src/agent_path.rs | 23 ++++++++++++++++++++--- codex-rs/protocol/src/protocol.rs | 3 +++ 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index af7371607..8004f6e6e 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -148,6 +148,15 @@ impl AgentControl { } } + /// Create a control-plane handle over the same thread manager with an independent live-agent + /// registry. + pub(crate) fn detached_registry(&self) -> Self { + Self { + manager: self.manager.clone(), + ..Default::default() + } + } + /// Spawn a new agent thread and submit the initial prompt. pub(crate) async fn spawn_agent( &self, diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index cbdb09cb8..0b6321b08 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -139,9 +139,8 @@ pub(super) async fn run(session: &Arc, config: Arc) { // 5. Spawn the agent let prompt = agent::get_prompt(config, &selection, &removed_extension_resources); let source = SessionSource::SubAgent(SubAgentSource::MemoryConsolidation); - let thread_id = match session - .services - .agent_control + let agent_control = session.services.agent_control.detached_registry(); + let thread_id = match agent_control .spawn_agent(agent_config, prompt.into(), Some(source)) .await { @@ -182,6 +181,7 @@ pub(super) async fn run(session: &Arc, config: Arc) { raw_memories.clone(), pending_extension_resource_removals, thread_id, + agent_control, phase_two_e2e_timer, ); @@ -369,6 +369,7 @@ mod agent { selected_outputs: Vec, pending_extension_resource_removals: Vec, thread_id: ThreadId, + agent_control: crate::agent::AgentControl, phase_two_e2e_timer: Option, ) { let Some(db) = session.services.state_db.clone() else { @@ -378,7 +379,6 @@ mod agent { tokio::spawn(async move { let _phase_two_e2e_timer = phase_two_e2e_timer; - let agent_control = session.services.agent_control.clone(); // TODO(jif) we might have a very small race here. let rx = match agent_control.subscribe_status(thread_id).await { diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index b991cab3d..78ce3bcba 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -429,6 +429,7 @@ mod phase2 { use codex_config::Constrained; use codex_features::Feature; use codex_login::CodexAuth; + use codex_protocol::AgentPath; use codex_protocol::ThreadId; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -703,6 +704,19 @@ mod phase2 { } other => panic!("unexpected sandbox policy: {other:?}"), } + pretty_assertions::assert_eq!( + config_snapshot.session_source.get_agent_path(), + Some(AgentPath::morpheus()) + ); + assert!( + harness + .session + .services + .agent_control + .get_agent_metadata(thread_id) + .is_none(), + "memory consolidation should not be registered in the root collab agent registry" + ); let turn_context = subagent.codex.session.new_default_turn().await; pretty_assertions::assert_eq!( turn_context.file_system_sandbox_policy, diff --git a/codex-rs/protocol/src/agent_path.rs b/codex-rs/protocol/src/agent_path.rs index f0b99438d..c0ad8b734 100644 --- a/codex-rs/protocol/src/agent_path.rs +++ b/codex-rs/protocol/src/agent_path.rs @@ -16,12 +16,17 @@ pub struct AgentPath(String); impl AgentPath { pub const ROOT: &str = "/root"; + pub const MORPHEUS: &str = "/morpheus"; const ROOT_SEGMENT: &str = "root"; pub fn root() -> Self { Self(Self::ROOT.to_string()) } + pub fn morpheus() -> Self { + Self(Self::MORPHEUS.to_string()) + } + pub fn from_string(path: String) -> Result { validate_absolute_path(path.as_str())?; Ok(Self(path)) @@ -142,15 +147,19 @@ fn validate_agent_name(agent_name: &str) -> Result<(), String> { } fn validate_absolute_path(path: &str) -> Result<(), String> { + if path == AgentPath::MORPHEUS { + return Ok(()); + } + let Some(stripped) = path.strip_prefix('/') else { - return Err("absolute agent paths must start with `/root`".to_string()); + return Err("absolute agent paths must start with `/root` or be `/morpheus`".to_string()); }; let mut segments = stripped.split('/'); let Some(root) = segments.next() else { return Err("absolute agent path must not be empty".to_string()); }; if root != AgentPath::ROOT_SEGMENT { - return Err("absolute agent paths must start with `/root`".to_string()); + return Err("absolute agent paths must start with `/root` or be `/morpheus`".to_string()); } if stripped.ends_with('/') { return Err("absolute agent path must not end with `/`".to_string()); @@ -184,6 +193,14 @@ mod tests { assert!(root.is_root()); } + #[test] + fn morpheus_has_expected_name() { + let morpheus = AgentPath::morpheus(); + assert_eq!(morpheus.as_str(), AgentPath::MORPHEUS); + assert_eq!(morpheus.name(), "morpheus"); + assert!(!morpheus.is_root()); + } + #[test] fn join_builds_child_paths() { let root = AgentPath::root(); @@ -213,7 +230,7 @@ mod tests { ); assert_eq!( AgentPath::try_from("/not-root"), - Err("absolute agent paths must start with `/root`".to_string()) + Err("absolute agent paths must start with `/root` or be `/morpheus`".to_string()) ); assert_eq!( AgentPath::root().resolve("../sibling"), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 88b7e4c8f..ec1fc847a 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2723,6 +2723,9 @@ impl SessionSource { SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. }) => { agent_path.clone() } + SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => { + Some(AgentPath::morpheus()) + } _ => None, } }