Use step environment snapshots for subagent inheritance (#55331)

## What changed

- Remove `subagent_thread_environments` and the separate configured environment bindings. Spawned agents inherit the invoking step's environment snapshot.
- Retain environment selections across child eviction without retaining executor bindings. Reload V2 children against the parent's captured environments, requiring matching environment IDs, working directories, and workspace roots while preserving permission validation.

GitOrigin-RevId: d820c63ea20f5834ed88071d16c60683f2545406
This commit is contained in:
jif
2026-10-19 17:43:31 +00:00
committed by copyberry
parent 03478b4b92
commit fbb2f9dbec
23 changed files with 132 additions and 667 deletions
+8 -1
View File
@@ -651,7 +651,14 @@ impl LocalAgentControl {
};
let parent_thread = state.get_thread(*parent_thread_id).await.ok()?;
Some(parent_thread.session.inherited_environment_snapshot().await)
Some(
parent_thread
.session
.services
.turn_environments
.snapshot()
.await,
)
}
async fn inherited_exec_policy_for_source(
+1 -11
View File
@@ -4,7 +4,6 @@ use super::LocalAgentControl;
use super::LocalAgentRuntime;
use super::runtime::AgentTreeMembership;
use crate::agent::AgentStatus;
use crate::agent::registry::EvictedEnvironments;
use crate::codex_thread::CodexThread;
use crate::config::Config;
use crate::thread_manager::ThreadManagerState;
@@ -77,16 +76,7 @@ impl ThreadManagerState {
.collect();
let runtime = &thread.session.services.local_agent_runtime;
let _ = runtime.mailboxes.enqueue(thread_id, /*id*/ None, mail);
// Preserve configured bindings across eviction without retaining turn-only attachments.
// Raw bindings keep the original pending owner configuration alive.
let environments = EvictedEnvironments {
selections: thread.environment_selections().await,
bindings: thread
.session
.services
.turn_environments
.configured_snapshot_now(),
};
let environments = thread.environment_selections().await;
let mut threads = manager.threads.write().await;
if threads
.get(&thread_id)
+14 -39
View File
@@ -362,11 +362,7 @@ impl LocalAgentControl {
{
return Err(CodexErr::ThreadNotFound(thread_id));
}
let evicted_environments = self.runtime.registry.evicted_environments(thread_id);
let mut environment_selections = evicted_environments
.as_ref()
.map(|environments| environments.selections.clone());
let evicted_environments = evicted_environments.map(|environments| environments.bindings);
let mut environment_selections = self.runtime.registry.evicted_environments(thread_id);
let stored_thread = state
.read_stored_thread(ReadThreadParams {
@@ -422,8 +418,7 @@ impl LocalAgentControl {
"cannot resume multi-agent v2 child {thread_id} with the current parent settings"
))
})?;
let environments = parent.session.inherited_environment_snapshot().await;
Some((parent, environments))
Some((parent, turn.initial_environments.clone()))
} else {
None
};
@@ -495,11 +490,6 @@ impl LocalAgentControl {
)));
}
if let Some(selections) = environment_selections.as_mut() {
let parent_selections = parent_environments.all_selections();
let ready_parent = parent_environments.refresh_readiness();
let ready_evicted = evicted_environments
.as_ref()
.map(TurnEnvironmentSnapshot::refresh_readiness);
for selection in selections {
let environment_id = &selection.environment_id;
let invalid_environment = |reason: &str| {
@@ -507,22 +497,17 @@ impl LocalAgentControl {
"cannot resume multi-agent v2 child {thread_id}: cached environment {environment_id} {reason}"
))
};
// A changed parent selection does not revoke the child's retained binding.
// If the parent still configures this workspace, keep enforcing its current
// roots and permissions rather than restoring superseded authority.
let owner_environments = if parent_selections.iter().any(|parent_selection| {
parent_selection.environment_id == selection.environment_id
&& parent_selection.cwd == selection.cwd
}) {
&ready_parent
} else {
ready_evicted.as_ref().unwrap_or(&ready_parent)
};
let owner_environment = owner_environments
// Matching the attachment also keeps startup on the captured owner executor.
let owner_environment = parent_environments
.turn_environments()
.find(|environment| environment.selection.has_same_workspace(selection))
.find(|environment| {
let parent_selection = &environment.selection;
parent_selection.environment_id == selection.environment_id
&& parent_selection.cwd == selection.cwd
&& parent_selection.workspace_roots == selection.workspace_roots
})
.ok_or_else(|| {
invalid_environment("has no matching ready environment binding")
invalid_environment("no longer matches a ready parent environment")
})?;
let owner_config = owner_environment.config();
let child_config = match &selection.config {
@@ -584,24 +569,14 @@ impl LocalAgentControl {
}
}
(
Some(
evicted_environments
.as_ref()
.unwrap_or(parent_environments)
.clone(),
),
Some(parent_environments.clone()),
Some(Arc::clone(&parent.session.services.exec_policy)),
Some(parent.client_mcp_extensions()),
)
} else {
(
match &evicted_environments {
Some(environments) => Some(environments.clone()),
None => {
self.inherited_environments_for_source(&state, Some(&session_source))
.await
}
},
self.inherited_environments_for_source(&state, Some(&session_source))
.await,
self.inherited_exec_policy_for_source(&state, Some(&session_source), &config)
.await,
None,
+3 -6
View File
@@ -1175,12 +1175,9 @@ async fn cancelled_v2_send_finishes_reload_registration() {
.unwrap(),
child_id,
);
assert!(
control
.runtime
.registry
.evicted_environments(child_id)
.is_none()
assert_eq!(
control.runtime.registry.evicted_environments(child_id),
None
);
// Evict and reload again with the single child slot. This also catches a leaked
// pin or pending residency reservation left behind by the cancelled sender.
+6 -11
View File
@@ -1,5 +1,4 @@
use crate::agent::types::AgentMetadata;
use crate::environment_selection::TurnEnvironmentSnapshot;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::error::AgentErrorContext;
@@ -38,16 +37,9 @@ struct ActiveAgents {
nickname_reset_count: usize,
}
/// Thread defaults and executor bindings retained while a child's runtime is unloaded.
#[derive(Clone)]
pub(crate) struct EvictedEnvironments {
pub(crate) selections: Vec<TurnEnvironmentSelection>,
pub(crate) bindings: TurnEnvironmentSnapshot,
}
struct RegisteredAgent {
path: String,
evicted_environments: Option<EvictedEnvironments>,
evicted_environments: Option<Vec<TurnEnvironmentSelection>>,
}
impl RegisteredAgent {
@@ -182,7 +174,7 @@ impl AgentRegistry {
pub(crate) fn save_evicted_environments(
&self,
thread_id: ThreadId,
environments: EvictedEnvironments,
environments: Vec<TurnEnvironmentSelection>,
) {
let mut active_agents = self
.active_agents
@@ -193,7 +185,10 @@ impl AgentRegistry {
}
}
pub(crate) fn evicted_environments(&self, thread_id: ThreadId) -> Option<EvictedEnvironments> {
pub(crate) fn evicted_environments(
&self,
thread_id: ThreadId,
) -> Option<Vec<TurnEnvironmentSelection>> {
let active_agents = self
.active_agents
.lock()
+1 -1
View File
@@ -32,7 +32,7 @@ pub struct SpawnAgentOptions {
/// Attribute delegated usage to the turn that initiated it.
pub turn_trigger: Option<String>,
pub root_turn_id: Option<String>,
/// Inheritable bindings captured by the step that requested the child.
/// Exact environment bindings from the step that requested the child.
pub environments: Option<TurnEnvironmentSnapshot>,
/// Plugin choices from the turn requesting the child. Omission inherits the parent's
/// current choices; an explicit list, including an empty one, overrides them and history.
+54 -182
View File
@@ -165,22 +165,6 @@ type TurnEnvironmentResolution = Shared<BoxFuture<'static, TurnEnvironmentResult
type PendingConfigurationResult = Result<EnvironmentConfig, String>;
pub(crate) type PendingConfiguration = Shared<BoxFuture<'static, PendingConfigurationResult>>;
fn pending_configuration(
completion: &watch::Sender<Option<PendingConfigurationResult>>,
) -> PendingConfiguration {
let mut receiver = completion.subscribe();
async move {
receiver
.wait_for(Option::is_some)
.await
.ok()
.and_then(|result| result.clone())
.unwrap_or_else(|| Err("environment configuration was canceled".to_string()))
}
.boxed()
.shared()
}
// Shared startup result used to build each turn's environment with its own config
// without restarting the connection or shell resolution.
#[derive(Clone)]
@@ -207,7 +191,7 @@ struct SelectedTurnEnvironment {
resolution: TurnEnvironmentResolution,
// Executor retries keep this session's wait for its first accepted configuration.
pending_completion: Option<watch::Sender<Option<PendingConfigurationResult>>>,
// Inherited owner only; otherwise snapshots expose this attachment's own completion.
// Descendants can await the original attachment's first accepted config or failure.
owner_config_result: Option<PendingConfiguration>,
}
@@ -239,22 +223,6 @@ fn resolve_selection_config(
}
impl SelectedTurnEnvironment {
// Pending configuration belongs to each attachment set; completed startup can be shared.
fn for_turn(&self, local_shell: &Shell, shell_snapshot: &ShellSnapshot) -> Self {
if matches!(self.selection.config, EnvironmentConfigState::Pending) {
return ThreadEnvironments::start_environment(
self.selection.clone(),
self.config_origin,
Arc::clone(&self.environment),
self.owner_config_result.clone(),
/*pending_completion*/ None,
local_shell,
shell_snapshot,
);
}
self.clone()
}
fn refresh_thread_config(&mut self, defaults: &ThreadEnvironmentDefaults) {
if self.config_origin == EnvironmentConfigOrigin::Thread {
self.selection.config =
@@ -298,8 +266,6 @@ pub(crate) struct ThreadEnvironments {
shell_snapshot: ShellSnapshot,
non_blocking_snapshots: bool,
state: Mutex<ThreadEnvironmentsState>,
// Retain configured bindings independently of active-turn membership and cancellation.
configured: Mutex<ThreadEnvironmentsState>,
connection_event_tx: OnceLock<Sender<Event>>,
}
@@ -317,19 +283,10 @@ impl ThreadEnvironments {
current: TurnEnvironmentSnapshot,
non_blocking_snapshots: bool,
) -> Self {
let environments: Vec<_> = current
let environments = current
.environments
.into_iter()
.filter_map(|inherited| {
// Reuse completed startup without resolving a Pending owner's configuration.
let inherited = if let TurnEnvironmentState::Starting(starting) = &inherited
&& let EnvironmentConfigState::Ready(_) = &starting.selection.config
&& let Some(Ok(resolved)) = starting.resolution.clone().now_or_never()
{
TurnEnvironmentState::from_resolution(starting.clone(), Some(Ok(resolved)))
} else {
inherited
};
let environment = match inherited {
TurnEnvironmentState::Ready(environment) => environment,
TurnEnvironmentState::Starting(inherited) => {
@@ -392,20 +349,12 @@ impl ThreadEnvironments {
Some(inherited_environment)
})
.collect();
let active = environments
.iter()
.map(|environment| environment.for_turn(&local_shell, &shell_snapshot))
.collect();
Self {
environment_manager,
local_shell,
shell_snapshot,
non_blocking_snapshots,
state: Mutex::new(ThreadEnvironmentsState {
thread_defaults: thread_defaults.clone(),
environments: active,
}),
configured: Mutex::new(ThreadEnvironmentsState {
thread_defaults,
environments,
}),
@@ -449,40 +398,7 @@ impl ThreadEnvironments {
/// Updates the selected list before waking work that was waiting on the previous selection.
pub(crate) fn update_selections(&self, environments: &[TurnEnvironmentSelection]) {
let bindings = self
.configured
.lock()
.unwrap_or_else(PoisonError::into_inner)
.environments
.clone();
self.update_selections_with_bindings(
&self.state,
environments,
&bindings,
self.connection_event_tx.get(),
);
// Retry configured bindings with their own settings, keeping turn-only changes temporary.
self.update_configured_selections(&self.configured_snapshot_now().all_selections());
}
/// Retains configured bindings without changing the active turn's attachments.
pub(crate) fn update_configured_selections(&self, environments: &[TurnEnvironmentSelection]) {
self.update_selections_with_bindings(
&self.configured,
environments,
&[],
/*tx_event*/ None,
);
}
fn update_selections_with_bindings(
&self,
state: &Mutex<ThreadEnvironmentsState>,
environments: &[TurnEnvironmentSelection],
bindings: &[SelectedTurnEnvironment],
tx_event: Option<&Sender<Event>>,
) {
let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let ThreadEnvironmentsState {
thread_defaults,
environments: current,
@@ -490,52 +406,18 @@ impl ThreadEnvironments {
let mut seen_environment_ids = HashSet::with_capacity(environments.len());
let mut next = Vec::with_capacity(environments.len());
let mut pending_completions = Vec::new();
// Connection state belongs to the environment instance, not its cwd or roots.
let connection_events_task = |environment_id: &str, environment: &Arc<Environment>| {
current
.iter()
.find(|previous| {
previous.selection.environment_id == environment_id
&& Arc::ptr_eq(&previous.environment, environment)
})
.and_then(|previous| previous.connection_events_task.clone())
.or_else(|| {
Self::spawn_connection_event_listener(
environment.as_ref(),
environment_id.to_owned(),
tx_event?.clone(),
)
})
};
for selected_environment in environments {
if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) {
continue;
}
let current_environment = current.iter().find(|environment| {
environment
.selection
.has_same_workspace(selected_environment)
});
let previous = current_environment.or_else(|| {
bindings.iter().find(|environment| {
environment
.selection
.has_same_workspace(selected_environment)
})
let previous = current.iter().find(|environment| {
let previous = &environment.selection;
previous.has_same_workspace(selected_environment)
});
let (selected_environment, config_origin) =
resolve_selection_config(selected_environment.clone(), thread_defaults);
if let Some(environment) = previous {
let failed =
matches!(
environment.selection.config,
EnvironmentConfigState::Failed(_)
) || matches!(environment.resolution.clone().now_or_never(), Some(Err(_)));
let mut environment = if current_environment.is_some() {
environment.clone()
} else {
environment.for_turn(&self.local_shell, &self.shell_snapshot)
};
let mut environment = environment.clone();
if let Some(completion) = environment.pending_completion.take() {
match &selected_environment.config {
EnvironmentConfigState::Ready(config) => {
@@ -549,6 +431,11 @@ impl ThreadEnvironments {
}
}
}
let failed =
matches!(
environment.selection.config,
EnvironmentConfigState::Failed(_)
) || matches!(environment.resolution.clone().now_or_never(), Some(Err(_)));
let restarting_as_pending =
matches!(selected_environment.config, EnvironmentConfigState::Pending)
&& !matches!(
@@ -572,12 +459,6 @@ impl ThreadEnvironments {
{
self.restart_shell_snapshot(&mut environment, resolved);
}
if environment.connection_events_task.is_none() {
environment.connection_events_task = connection_events_task(
&environment.selection.environment_id,
&environment.environment,
);
}
next.push(environment);
continue;
}
@@ -588,7 +469,23 @@ impl ThreadEnvironments {
tracing::warn!("skipping unknown turn environment `{environment_id}`");
continue;
};
let connection_events_task = connection_events_task(environment_id, &environment);
// Connection state belongs to the environment instance, not its cwd or roots.
let connection_events_task = current
.iter()
.find(|previous| {
previous.selection.environment_id.as_str() == environment_id.as_str()
&& Arc::ptr_eq(&previous.environment, &environment)
})
.and_then(|previous| previous.connection_events_task.clone())
.or_else(|| {
self.connection_event_tx.get().and_then(|tx_event| {
Self::spawn_connection_event_listener(
environment.as_ref(),
environment_id.clone(),
tx_event.clone(),
)
})
});
let pending = previous.filter(|previous| {
matches!(selected_environment.config, EnvironmentConfigState::Pending)
&& previous.pending_completion.is_some()
@@ -598,7 +495,7 @@ impl ThreadEnvironments {
config_origin,
environment,
pending.and_then(|previous| previous.owner_config_result.clone()),
current_environment.and_then(|previous| previous.pending_completion.clone()),
pending.and_then(|previous| previous.pending_completion.clone()),
&self.local_shell,
&self.shell_snapshot,
);
@@ -621,13 +518,7 @@ impl ThreadEnvironments {
// Publish after installing the new list, including pending bindings replaced by retries.
for (completion, result) in pending_completions {
completion.send_if_modified(|current| {
if current.is_some() {
return false;
}
*current = Some(result);
true
});
let _ = completion.send_replace(Some(result));
}
for task in removed_connection_tasks {
@@ -650,12 +541,25 @@ impl ThreadEnvironments {
if matches!(selection.config, EnvironmentConfigState::Pending) {
let sender =
pending_completion.unwrap_or_else(|| watch::channel(/*init*/ None).0);
let result = pending_configuration(&sender);
let mut receiver = sender.subscribe();
let result = async move {
receiver
.wait_for(Option::is_some)
.await
.ok()
.and_then(|result| result.clone())
.unwrap_or_else(
|| Err("environment configuration was canceled".to_string()),
)
}
.boxed()
.shared();
(Some(sender), Some(result))
} else {
(None, None)
};
// The resolver waits for this Session; descendants can follow the original owner.
let owner_config_result = owner_config_result.or_else(|| configuration_ready.clone());
let (task, resolution) = Self::resolve_environment(
selection.clone(),
Arc::clone(&environment),
@@ -723,19 +627,7 @@ impl ThreadEnvironments {
/// Changes the defaults inherited by all selected and later-added environments. A step that
/// already captured its environments keeps the previous values.
pub(crate) fn set_active_thread_defaults(&self, defaults: EnvironmentConfig) {
self.set_thread_defaults(&self.state, defaults);
}
pub(crate) fn set_configured_thread_defaults(&self, defaults: EnvironmentConfig) {
self.set_thread_defaults(&self.configured, defaults);
}
fn set_thread_defaults(
&self,
state: &Mutex<ThreadEnvironmentsState>,
defaults: EnvironmentConfig,
) {
let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let ThreadEnvironmentsState {
thread_defaults,
environments,
@@ -766,19 +658,11 @@ impl ThreadEnvironments {
pub(crate) fn set_snapshot_credential_broker(&self, broker: SnapshotCredentialBrokerState) {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let mut configured = self
.configured
.lock()
.unwrap_or_else(PoisonError::into_inner);
if !self.shell_snapshot.set_credential_broker(broker) {
return;
}
for selected in state
.environments
.iter_mut()
.chain(&mut configured.environments)
{
for selected in &mut state.environments {
if !selected.environment.is_remote()
&& let Some(Ok(resolved)) = selected.resolution.clone().now_or_never()
{
@@ -1025,35 +909,23 @@ impl ThreadEnvironments {
/// Copies the current selection without waiting for any executor to connect.
pub(crate) fn snapshot_now(&self) -> TurnEnvironmentSnapshot {
Self::snapshot_state(&self.state).refresh_readiness()
}
/// Captures configured bindings without resolving readiness or losing config ownership.
/// Inheritance and eviction retain these bindings; execution and validation resolve a view.
pub(crate) fn configured_snapshot_now(&self) -> TurnEnvironmentSnapshot {
Self::snapshot_state(&self.configured)
}
fn snapshot_state(state: &Mutex<ThreadEnvironmentsState>) -> TurnEnvironmentSnapshot {
// Keep the startup results, but not the senders that would keep removed Pending work alive.
let environments = state
let environments = self
.state
.lock()
.unwrap_or_else(PoisonError::into_inner)
.environments
.iter()
.map(|environment| {
TurnEnvironmentState::Starting(StartingTurnEnvironment {
let starting = StartingTurnEnvironment {
selection: environment.selection.clone(),
config_origin: environment.config_origin,
resolution: environment.resolution.clone(),
environment: Arc::clone(&environment.environment),
owner_config_result: environment.owner_config_result.clone().or_else(|| {
environment
.pending_completion
.as_ref()
.map(pending_configuration)
}),
})
owner_config_result: environment.owner_config_result.clone(),
};
let resolved = starting.resolution.clone().now_or_never();
TurnEnvironmentState::from_resolution(starting, resolved)
})
.collect();
TurnEnvironmentSnapshot { environments }
@@ -126,10 +126,8 @@ async fn capability_roots_follow_attachments_through_configuration_changes() {
));
}
#[test_case::test_case(false; "standalone startup")]
#[test_case::test_case(true; "configured startup")]
#[tokio::test]
async fn unpolled_snapshot_does_not_delay_canceling_a_removed_environment(shared_startup: bool) {
async fn unpolled_snapshot_does_not_delay_canceling_a_removed_environment() {
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd"));
let selection = TurnEnvironmentSelection {
selected_capability_roots: Default::default(),
@@ -146,9 +144,6 @@ async fn unpolled_snapshot_does_not_delay_canceling_a_removed_environment(shared
TurnEnvironmentSnapshot::default(),
/*non_blocking_snapshots*/ false,
);
if shared_startup {
environments.update_configured_selections(std::slice::from_ref(&selection));
}
environments.update_selections(&[selection]);
let starting = environments
.snapshot()
@@ -213,7 +208,7 @@ async fn updating_another_environment_retries_the_executor_without_canceling_pen
(
failing.resolution.clone(),
failing.pending_completion.as_ref().unwrap().subscribe(),
pending_configuration(failing.pending_completion.as_ref().unwrap()),
failing.owner_config_result.clone().unwrap(),
)
};
@@ -223,9 +218,9 @@ async fn updating_another_environment_retries_the_executor_without_canceling_pen
local.config = EnvironmentConfigState::Ready(tests::test_environment_config());
environments.update_selections(&[local.clone(), pending.clone()]);
let (second_attempt, second_config, _) = current();
let (second_attempt, _, second_owner) = current();
assert!(!first_attempt.ptr_eq(&second_attempt));
assert!(original_config.same_channel(&second_config));
assert!(original_owner.ptr_eq(&second_owner));
assert!(second_attempt.await.is_err());
assert!(original_config.borrow().is_none());
-6
View File
@@ -23,7 +23,6 @@ use crate::session::session::SessionConfiguration;
use crate::session::session::SessionSettingsUpdate;
/// Defaults for environments that inherit their configuration from the running turn.
#[derive(Clone)]
pub(crate) struct ThreadEnvironmentDefaults {
pub(crate) common: EnvironmentConfig,
// Chosen once when the session starts; it does not apply to remote environments.
@@ -350,11 +349,6 @@ impl Session {
}
}
if update_future {
self.services
.turn_environments
.update_configured_selections(&future);
}
state.session_configuration.environments = future;
if update_current {
// Invalidate MCP before installed configuration can wake a waiting turn.
+2 -33
View File
@@ -2040,14 +2040,6 @@ impl Session {
// Save new environment defaults for future turns. The running turn keeps its own.
windows_sandbox_changed =
state.session_configuration.windows_sandbox_level != updated.windows_sandbox_level;
self.services
.turn_environments
.set_configured_thread_defaults(updated.inferred_environment_config());
if updated.environments != state.session_configuration.environments {
self.services
.turn_environments
.update_configured_selections(&updated.environments);
}
state.session_configuration = updated;
if root_service_tier_changed {
self.services.agent_control.propagate_config_update(
@@ -2121,18 +2113,6 @@ impl Session {
.thread_config_snapshot(state.session_configuration.environments.clone())
}
/// Captures configured child bindings, or the active turn's bindings for legacy inheritance.
pub(crate) async fn inherited_environment_snapshot(&self) -> TurnEnvironmentSnapshot {
let snapshot = {
let _state = self.state.lock().await;
if self.features.enabled(Feature::SubagentThreadEnvironments) {
return self.services.turn_environments.configured_snapshot_now();
}
self.services.turn_environments.snapshot()
};
snapshot.await
}
pub(crate) async fn configured_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
let state = self.state.lock().await;
state.session_configuration.environments.clone()
@@ -3885,10 +3865,6 @@ impl Session {
.await
}
#[expect(
clippy::await_holding_invalid_type,
reason = "step settings and both environment snapshots must be captured under active_turn then state"
)]
#[tracing::instrument(name = "step_context.capture", level = "info", skip_all)]
async fn capture_step_context_inner(
self: &Arc<Self>,
@@ -3898,21 +3874,16 @@ impl Session {
required_plugins: &HashSet<String>,
) -> CodexResult<Arc<StepContext>> {
// Read the step's model and record its environments together so an update cannot split them.
// Match apply_turn_settings and activate_turn_environments: active_turn before state.
// Wait for executor startup below, after releasing both locks.
let (mut settings, environments, inherited_environments) = {
// Wait for executor startup below, after releasing the lock.
let (mut settings, environments) = {
let _active = self
.active_turn
.lock()
.or_cancel(cancellation_token)
.await?;
let _state = self.state.lock().or_cancel(cancellation_token).await?;
(
turn_context.next_step_settings.load_full(),
self.services.turn_environments.snapshot(),
self.features
.enabled(Feature::SubagentThreadEnvironments)
.then(|| self.services.turn_environments.configured_snapshot_now()),
)
};
if matches!(
@@ -3940,7 +3911,6 @@ impl Session {
);
let session_telemetry = settings.telemetry(&turn_context.session_telemetry);
let environments = environments.or_cancel(cancellation_token).await?;
let inherited_environments = inherited_environments.unwrap_or_else(|| environments.clone());
// Keep both preparation futures off caller stacks while they are live together.
let load_agents_md = Box::pin(async {
let (loaded_agents_md, warnings) = self
@@ -4073,7 +4043,6 @@ impl Session {
turn: turn_context,
extension_data,
environments,
inherited_environments,
selected_capability_roots,
executor_capability_discovery,
executor_capability_discovery_v2,
-1
View File
@@ -1483,7 +1483,6 @@ impl Session {
inherited_environments.clone(),
config.features.enabled(Feature::DeferredExecutor),
));
turn_environments.update_configured_selections(environment_selections);
turn_environments.update_selections(environment_selections);
session_configuration.environments = turn_environments.selections();
let resolved_environments = turn_environments.snapshot().await;
@@ -189,27 +189,14 @@ async fn next_step_waits_for_environment_publication_before_capturing() {
.turn_environments
.update_selections(&selections);
}
{
// Thread attachments publish under state independently of active-turn settings.
// A capture waiting for that publication must not retain the previous attachments.
let mut state = session.state.lock().await;
let mut context = std::task::Context::from_waker(futures::task::noop_waker_ref());
assert!(std::future::Future::poll(capture.as_mut(), &mut context).is_pending());
session
.services
.turn_environments
.update_configured_selections(&selections);
state.session_configuration.environments = selections.clone();
}
let step = capture.await.expect("capture after publication");
assert_eq!(
(
step.settings.model_info.slug.as_str(),
step.environments.primary().map(TurnEnvironment::cwd),
step.inherited_environments.all_selections(),
),
("next-step-model", Some(&next_cwd), selections),
("next-step-model", Some(&next_cwd)),
);
session.abort_all_tasks(TurnAbortReason::Interrupted).await;
}
@@ -422,7 +409,6 @@ async fn activation_fixture(models: Vec<ModelInfo>) -> ActivationFixture {
let (models, lookup) = GatedModelsManager::new(models);
mutable.services.models_manager = models;
for feature in [
Feature::SubagentThreadEnvironments,
Feature::StepModelSwitching,
Feature::FastMode,
Feature::TokenBudget,
@@ -35,8 +35,6 @@ pub(crate) struct StepContext {
/// Telemetry context tagged with this sampling request's model.
pub(crate) session_telemetry: SessionTelemetry,
pub(crate) environments: TurnEnvironmentSnapshot,
/// Child bindings captured with this step; configured-only when opted in.
pub(crate) inherited_environments: TurnEnvironmentSnapshot,
/// Capability roots bound to ready environments in this exact step.
pub(crate) selected_capability_roots: Vec<ResolvedSelectedCapabilityRoot>,
/// Executor-materialized capability files shared by MCP and skills in this exact step.
-1
View File
@@ -289,7 +289,6 @@ impl StepContext {
mode_instructions: None,
},
turn: Arc::clone(&turn),
inherited_environments: environments.clone(),
environments,
selected_capability_roots: Vec::new(),
executor_capability_discovery: None,
+1 -1
View File
@@ -152,7 +152,7 @@ pub(crate) async fn spawn(
parent_turn_id: Some(turn.sub_id.clone()),
root_turn_id: turn.turn_metadata_state.root_turn_id(),
turn_trigger: turn.turn_metadata_state.current_turn_trigger(),
environments: Some(step_context.inherited_environments.clone()),
environments: Some(step_context.environments.clone()),
disabled_plugin_ids: Some(turn.disabled_plugin_ids.clone()),
multi_agent_v2_usage_hints,
cyber_access_program: turn.cyber_access_program,
@@ -131,7 +131,7 @@ async fn handle_spawn_agent(
parent_turn_id: Some(turn.sub_id.clone()),
root_turn_id: turn.turn_metadata_state.root_turn_id(),
turn_trigger: turn.turn_metadata_state.current_turn_trigger(),
environments: Some(step_context.inherited_environments.clone()),
environments: Some(step_context.environments.clone()),
disabled_plugin_ids: Some(turn.disabled_plugin_ids.clone()),
multi_agent_v2_usage_hints: None,
cyber_access_program: turn.cyber_access_program,