mirror of
https://github.com/openai/codex.git
synced 2026-11-11 14:32:25 +00:00
Restore V2 agent identities on root thread resume (#32837)
## Why After a cold resume, a Multi-Agent V2 root thread must still be able to address its persisted descendants even though their runtimes are not reopened eagerly. ## What changed - Restore metadata for open descendant agents when resuming a V2 root thread. - Preserve each descendant's path, nickname, and role while leaving its runtime unloaded until a message targets it. - Reuse the spawn metadata preparation path so restored identities retain the same reservation and naming checks as newly spawned agents. ## Testing Added an end-to-end cold-resume test that follows up with a persisted agent by name and verifies that the original agent thread is loaded on demand. GitOrigin-RevId: a1663c1ae60eefadb948cd39f933cd3bbb49a492
This commit is contained in:
1 parent
19a000259d
commit
088239294a
5 files changed
+363
-19
No files matched your search
@@ -540,6 +540,32 @@ impl AgentControl {
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_agent_metadata(
|
||||
&self,
|
||||
reservation: &mut crate::agent::registry::SpawnReservation,
|
||||
config: &Config,
|
||||
agent_path: Option<AgentPath>,
|
||||
agent_role: Option<String>,
|
||||
preferred_agent_nickname: Option<String>,
|
||||
) -> CodexResult<AgentMetadata> {
|
||||
if let Some(agent_path) = agent_path.as_ref() {
|
||||
reservation.reserve_agent_path(agent_path)?;
|
||||
}
|
||||
let candidate_names = spawn::agent_nickname_candidates(config, agent_role.as_deref());
|
||||
let candidate_name_refs: Vec<&str> = candidate_names.iter().map(String::as_str).collect();
|
||||
let agent_nickname = Some(reservation.reserve_agent_nickname_with_preference(
|
||||
&candidate_name_refs,
|
||||
preferred_agent_nickname.as_deref(),
|
||||
)?);
|
||||
Ok(AgentMetadata {
|
||||
agent_id: None,
|
||||
agent_path,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
last_task_message: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepare_thread_spawn(
|
||||
&self,
|
||||
@@ -554,29 +580,20 @@ impl AgentControl {
|
||||
if depth == 1 {
|
||||
self.state.register_root_thread(parent_thread_id);
|
||||
}
|
||||
if let Some(agent_path) = agent_path.as_ref() {
|
||||
reservation.reserve_agent_path(agent_path)?;
|
||||
}
|
||||
let candidate_names = spawn::agent_nickname_candidates(config, agent_role.as_deref());
|
||||
let candidate_name_refs: Vec<&str> = candidate_names.iter().map(String::as_str).collect();
|
||||
let agent_nickname = Some(reservation.reserve_agent_nickname_with_preference(
|
||||
&candidate_name_refs,
|
||||
preferred_agent_nickname.as_deref(),
|
||||
)?);
|
||||
let agent_metadata = self.prepare_agent_metadata(
|
||||
reservation,
|
||||
config,
|
||||
agent_path,
|
||||
agent_role,
|
||||
preferred_agent_nickname,
|
||||
)?;
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path: agent_path.clone(),
|
||||
agent_nickname: agent_nickname.clone(),
|
||||
agent_role: agent_role.clone(),
|
||||
agent_path: agent_metadata.agent_path.clone(),
|
||||
agent_nickname: agent_metadata.agent_nickname.clone(),
|
||||
agent_role: agent_metadata.agent_role.clone(),
|
||||
});
|
||||
let agent_metadata = AgentMetadata {
|
||||
agent_id: None,
|
||||
agent_path,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
last_task_message: None,
|
||||
};
|
||||
Ok((session_source, agent_metadata))
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,77 @@ fn is_multi_agent_v2_usage_hint_message(item: &ResponseItem, usage_hint_texts: &
|
||||
}
|
||||
|
||||
impl AgentControl {
|
||||
/// Restore persisted V2 agent identities without reopening their runtimes.
|
||||
pub(crate) async fn restore_v2_agent_metadata(
|
||||
&self,
|
||||
config: &Config,
|
||||
root_thread_id: ThreadId,
|
||||
) {
|
||||
self.state.register_root_thread(root_thread_id);
|
||||
|
||||
let Ok(state) = self.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(agent_graph_store) = state.agent_graph_store() else {
|
||||
return;
|
||||
};
|
||||
let descendant_ids = match agent_graph_store
|
||||
.list_thread_spawn_descendants(
|
||||
root_thread_id,
|
||||
Some(codex_agent_graph_store::ThreadSpawnEdgeStatus::Open),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(descendant_ids) => descendant_ids,
|
||||
Err(err) => {
|
||||
warn!("failed to restore persisted V2 agent metadata for {root_thread_id}: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for thread_id in descendant_ids {
|
||||
if self.state.agent_metadata_for_thread(thread_id).is_some() {
|
||||
continue;
|
||||
}
|
||||
let restore_result = async {
|
||||
let stored_thread = state
|
||||
.read_stored_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: true,
|
||||
include_history: false,
|
||||
})
|
||||
.await?;
|
||||
let stored_agent_path = stored_thread
|
||||
.agent_path
|
||||
.as_deref()
|
||||
.map(AgentPath::try_from)
|
||||
.transpose()
|
||||
.map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!("invalid stored agent path: {err}"))
|
||||
})?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(/*max_threads*/ None)?;
|
||||
let mut metadata = self.prepare_agent_metadata(
|
||||
&mut reservation,
|
||||
config,
|
||||
stored_agent_path.or_else(|| stored_thread.source.get_agent_path()),
|
||||
stored_thread
|
||||
.agent_role
|
||||
.or_else(|| stored_thread.source.get_agent_role()),
|
||||
stored_thread
|
||||
.agent_nickname
|
||||
.or_else(|| stored_thread.source.get_nickname()),
|
||||
)?;
|
||||
metadata.agent_id = Some(thread_id);
|
||||
reservation.commit(metadata);
|
||||
Ok::<(), CodexErr>(())
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = restore_result {
|
||||
warn!("failed to restore V2 agent metadata for {thread_id}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a new agent thread and submit the initial prompt.
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn spawn_agent(
|
||||
|
||||
@@ -793,6 +793,14 @@ impl ThreadManager {
|
||||
let (session_source, thread_source) = initial_history
|
||||
.get_resumed_session_sources()
|
||||
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
||||
if let InitialHistory::Resumed(resumed) = &initial_history
|
||||
&& initial_history.get_multi_agent_version() == Some(MultiAgentVersion::V2)
|
||||
&& !session_source.is_non_root_agent()
|
||||
{
|
||||
agent_control
|
||||
.restore_v2_agent_metadata(&config, resumed.conversation_id)
|
||||
.await;
|
||||
}
|
||||
Box::pin(self.state.spawn_thread_with_source(
|
||||
config,
|
||||
initial_history,
|
||||
|
||||
Reference in new issue
Block a user