core: box multi-agent handler futures (#22266)

## Why

This is the base PR in the split stack for the permissions migration. It
isolates stack-safety work that had been mixed into the larger
permissions PR, so reviewers can evaluate the async-future changes
separately from the permissions model changes in #22267.

The main risk this addresses is large or recursive multi-agent futures
overflowing smaller runner stacks. A follow-up review also called out
that `shutdown_live_agent` must remain quiescent: callers should not
remove a live agent from tracking or release its spawn slot until the
worker loop has actually terminated.

## What Changed

- Boxes the large async futures in the multi-agent spawn, resume, and
close tool handlers.
- Boxes the `AgentControl` spawn and recursive close/shutdown paths that
can otherwise build very deep futures.
- Keeps `shutdown_live_agent` waiting for thread termination before
removing/releasing the live agent, preserving the previous shutdown
ordering while still boxing the recursive close path.

## Verification Strategy

The focused local coverage was `cargo test -p codex-core multi_agents`,
which exercises the multi-agent spawn/resume/close handlers, cascade
close/resume behavior, and the shutdown path touched by this PR.












---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22266).
* #22330
* #22329
* #22328
* #22327
* __->__ #22266
This commit is contained in:
Michael Bolin
2026-05-12 17:22:25 -07:00
committed by GitHub
parent 589b820d6e
commit 9e7cdbd0d2
7 changed files with 656 additions and 620 deletions
+15 -16
View File
@@ -233,32 +233,31 @@ impl AgentControl {
// The same `AgentControl` is sent to spawn the thread.
let new_thread = match (session_source, options.fork_mode.as_ref()) {
(Some(session_source), Some(_)) => {
self.spawn_forked_thread(
Box::pin(self.spawn_forked_thread(
&state,
config,
session_source,
&options,
inherited_shell_snapshot,
inherited_exec_policy,
)
))
.await?
}
(Some(session_source), None) => {
state
.spawn_new_thread_with_source(
config.clone(),
self.clone(),
session_source,
/*thread_source*/ Some(ThreadSource::Subagent),
/*persist_extended_history*/ false,
/*metrics_service_name*/ None,
inherited_shell_snapshot,
inherited_exec_policy,
options.environments.clone(),
)
.await?
Box::pin(state.spawn_new_thread_with_source(
config.clone(),
self.clone(),
session_source,
/*thread_source*/ Some(ThreadSource::Subagent),
/*persist_extended_history*/ false,
/*metrics_service_name*/ None,
inherited_shell_snapshot,
inherited_exec_policy,
options.environments.clone(),
))
.await?
}
(None, _) => state.spawn_new_thread(config.clone(), self.clone()).await?,
(None, _) => Box::pin(state.spawn_new_thread(config.clone(), self.clone())).await?,
};
agent_metadata.agent_id = Some(new_thread.thread_id);
reservation.commit(agent_metadata.clone());