core: box multi-agent wrapper futures (#19059)

## Why

While debugging the Windows stack overflows we saw in
[#13429](https://github.com/openai/codex/pull/13429) and then again in
[#18893](https://github.com/openai/codex/pull/18893), I hit another
overflow in
`tools::handlers::multi_agents::tests::tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed`.

That test drives the legacy multi-agent spawn / close / resume path. The
behavior was fine, but several thin async wrappers were still inlining
much larger `AgentControl` futures into their callers, which was enough
to overflow the default Windows stack.

## What

- Box the thin `AgentControl` wrappers around `spawn_agent_internal`,
`resume_single_agent_from_rollout`, and `shutdown_agent_tree`.
- Box the corresponding legacy `multi_agents` handler calls in `spawn`,
`resume_agent`, and `close_agent`.
- Keep behavior unchanged while reducing future size on this call path
so the Windows test no longer overflows its stack.

## Testing

- `cargo test -p codex-core --lib
tools::handlers::multi_agents::tests::tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed
-- --exact --nocapture`
- `cargo test -p codex-core` (this still hit unrelated local
integration-test failures because `codex.exe` / `test_stdio_server.exe`
were not present in this shell; the relevant unit tests passed)
This commit is contained in:
Michael Bolin
2026-04-22 17:48:13 -07:00
committed by GitHub
Unverified
parent 0e78ce80ee
commit 3cc3763e6c
4 changed files with 56 additions and 56 deletions
+16 -14
View File
@@ -163,15 +163,14 @@ impl AgentControl {
initial_operation: Op,
session_source: Option<SessionSource>,
) -> CodexResult<ThreadId> {
Ok(self
.spawn_agent_internal(
config,
initial_operation,
session_source,
SpawnAgentOptions::default(),
)
.await?
.thread_id)
let spawned_agent = Box::pin(self.spawn_agent_internal(
config,
initial_operation,
session_source,
SpawnAgentOptions::default(),
))
.await?;
Ok(spawned_agent.thread_id)
}
/// Spawn an agent thread with some metadata.
@@ -182,7 +181,7 @@ impl AgentControl {
session_source: Option<SessionSource>,
options: SpawnAgentOptions, // TODO(jif) drop with new fork.
) -> CodexResult<LiveAgent> {
self.spawn_agent_internal(config, initial_operation, session_source, options)
Box::pin(self.spawn_agent_internal(config, initial_operation, session_source, options))
.await
}
@@ -418,9 +417,12 @@ impl AgentControl {
session_source: SessionSource,
) -> CodexResult<ThreadId> {
let root_depth = thread_spawn_depth(&session_source).unwrap_or(0);
let resumed_thread_id = self
.resume_single_agent_from_rollout(config.clone(), thread_id, session_source)
.await?;
let resumed_thread_id = Box::pin(self.resume_single_agent_from_rollout(
config.clone(),
thread_id,
session_source,
))
.await?;
let state = self.upgrade()?;
let Ok(resumed_thread) = state.get_thread(resumed_thread_id).await else {
return Ok(resumed_thread_id);
@@ -698,7 +700,7 @@ impl AgentControl {
{
warn!("failed to persist thread-spawn edge status for {agent_id}: {err}");
}
self.shutdown_agent_tree(agent_id).await
Box::pin(self.shutdown_agent_tree(agent_id)).await
}
/// Shut down `agent_id` and any live descendants reachable from the in-memory spawn tree.
@@ -66,10 +66,7 @@ impl ToolHandler for Handler {
return Err(collab_agent_error(agent_id, err));
}
};
let result = session
.services
.agent_control
.close_agent(agent_id)
let result = Box::pin(session.services.agent_control.close_agent(agent_id))
.await
.map_err(|err| collab_agent_error(agent_id, err))
.map(|_| ());
@@ -61,7 +61,14 @@ impl ToolHandler for Handler {
.get_status(receiver_thread_id)
.await;
let (receiver_agent, error) = if matches!(status, AgentStatus::NotFound) {
match try_resume_closed_agent(&session, &turn, receiver_thread_id, child_depth).await {
match Box::pin(try_resume_closed_agent(
&session,
&turn,
receiver_thread_id,
child_depth,
))
.await
{
Ok(()) => {
status = session
.services
@@ -149,21 +156,18 @@ async fn try_resume_closed_agent(
child_depth: i32,
) -> Result<(), FunctionCallError> {
let config = build_agent_resume_config(turn.as_ref(), child_depth)?;
session
.services
.agent_control
.resume_agent_from_rollout(
config,
receiver_thread_id,
thread_spawn_source(
session.conversation_id,
&turn.session_source,
child_depth,
/*agent_role*/ None,
/*task_name*/ None,
)?,
)
.await
.map(|_| ())
.map_err(|err| collab_agent_error(receiver_thread_id, err))
Box::pin(session.services.agent_control.resume_agent_from_rollout(
config,
receiver_thread_id,
thread_spawn_source(
session.conversation_id,
&turn.session_source,
child_depth,
/*agent_role*/ None,
/*task_name*/ None,
)?,
))
.await
.map(|_| ())
.map_err(|err| collab_agent_error(receiver_thread_id, err))
}
@@ -82,26 +82,23 @@ impl ToolHandler for Handler {
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);
let result = session
.services
.agent_control
.spawn_agent_with_metadata(
config,
input_items,
Some(thread_spawn_source(
session.conversation_id,
&turn.session_source,
child_depth,
role_name,
/*task_name*/ None,
)?),
SpawnAgentOptions {
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
},
)
.await
.map_err(collab_spawn_error);
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
config,
input_items,
Some(thread_spawn_source(
session.conversation_id,
&turn.session_source,
child_depth,
role_name,
/*task_name*/ None,
)?),
SpawnAgentOptions {
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
},
))
.await
.map_err(collab_spawn_error);
let (new_thread_id, new_agent_metadata, status) = match &result {
Ok(spawned_agent) => (
Some(spawned_agent.thread_id),