mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Support pending environment attachment configuration (#38684)
## Why An environment connection can be available before its owner has supplied the configuration for a particular thread attachment. Threads need to start without blocking while ensuring turns do not use that attachment prematurely. ## What changed - Accept `Pending` environment configuration and resolve each attachment only after both its shared executor connection and owner configuration are ready. - Add a `Failed` configuration state and `environment_failed` callback so an owner can fail one thread's attachment without affecting other threads. - Keep pending and failed attachments out of capability-root inspection and turn environments, and allow failed attachments to recover through a ready update. - Apply owner configuration before waking a waiting turn so its permission profile, login-shell policy, capability roots, and tools are immediately consistent. ## Testing Add an integration test covering non-blocking thread startup, independent ready and failed callbacks, waiting-turn resumption, installed capability and tool configuration, and recovery from failure. GitOrigin-RevId: d587e2025d584c867d782d470b18bf5a1a27b76c
This commit is contained in:
@@ -19,12 +19,9 @@ pub(super) fn validate_environment_selections(
|
||||
) -> CodexResult<()> {
|
||||
for selection in selections {
|
||||
match &selection.config {
|
||||
EnvironmentConfigState::FromThread => {}
|
||||
EnvironmentConfigState::Pending => {
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"pending environment configuration is not supported yet".to_string(),
|
||||
));
|
||||
}
|
||||
EnvironmentConfigState::FromThread
|
||||
| EnvironmentConfigState::Pending
|
||||
| EnvironmentConfigState::Failed(_) => {}
|
||||
EnvironmentConfigState::Ready(config) => {
|
||||
validate_environment_config(selection, config)?;
|
||||
}
|
||||
@@ -74,14 +71,46 @@ impl Session {
|
||||
config: EnvironmentConfig,
|
||||
) -> CodexResult<()> {
|
||||
validate_environment_config(selection, &config)?;
|
||||
self.update_environment_configuration(selection, EnvironmentConfigState::Ready(config))
|
||||
.await
|
||||
}
|
||||
|
||||
// grab session lock so installation can't race w/ thread settings updates
|
||||
let _state = self.state.lock().await;
|
||||
self.services
|
||||
.turn_environments
|
||||
.environment_ready(selection, config)?;
|
||||
// mark mcp runtime for refresh because available capabilities could've changed
|
||||
pub(crate) async fn environment_failed(
|
||||
&self,
|
||||
selection: &TurnEnvironmentSelection,
|
||||
error: String,
|
||||
) -> CodexResult<()> {
|
||||
self.update_environment_configuration(selection, EnvironmentConfigState::Failed(error))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_environment_configuration(
|
||||
&self,
|
||||
selection: &TurnEnvironmentSelection,
|
||||
config: EnvironmentConfigState,
|
||||
) -> CodexResult<()> {
|
||||
// Serialize owner callbacks with ordinary thread settings updates.
|
||||
let state = self.state.lock().await;
|
||||
let mut environments = self.services.turn_environments.selections();
|
||||
let Some(environment) = environments.iter_mut().find(|environment| {
|
||||
environment.environment_id == selection.environment_id
|
||||
&& environment.cwd == selection.cwd
|
||||
&& environment.workspace_roots == selection.workspace_roots
|
||||
}) else {
|
||||
return Err(CodexErr::InvalidRequest(format!(
|
||||
"environment `{}` is not selected on this thread with the requested workspace",
|
||||
selection.environment_id
|
||||
)));
|
||||
};
|
||||
|
||||
environment.config = config;
|
||||
|
||||
// Invalidate MCP before installed configuration can wake a waiting turn.
|
||||
self.mark_mcp_runtime_dirty();
|
||||
self.services.turn_environments.update_selections(
|
||||
&environments,
|
||||
&state.session_configuration.turn_environment_config(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1588,6 +1588,9 @@ impl Session {
|
||||
previous_permission_profile != updated_permission_profile;
|
||||
let mcp_inputs_changed =
|
||||
self.mcp_inputs_differ(&state.session_configuration, &updated, &updates);
|
||||
if mcp_inputs_changed {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
}
|
||||
let environment_config = updated.turn_environment_config();
|
||||
if let Some(environments) = &updates.environments {
|
||||
self.services
|
||||
@@ -1601,9 +1604,6 @@ impl Session {
|
||||
state.session_configuration = updated;
|
||||
let new_config = notify_config_contributors
|
||||
.then(|| self.build_effective_session_config(&state.session_configuration));
|
||||
if mcp_inputs_changed {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
}
|
||||
(
|
||||
previous_config,
|
||||
new_config,
|
||||
|
||||
@@ -693,6 +693,9 @@ impl Session {
|
||||
Ok(next) => {
|
||||
let mcp_inputs_changed =
|
||||
self.mcp_inputs_differ(&state.session_configuration, &next, &updates);
|
||||
if mcp_inputs_changed {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
}
|
||||
let previous_permission_profile =
|
||||
state.session_configuration.permission_profile();
|
||||
let next_permission_profile = next.permission_profile();
|
||||
@@ -712,9 +715,6 @@ impl Session {
|
||||
.turn_environments
|
||||
.update_thread_config(&environment_config);
|
||||
}
|
||||
if mcp_inputs_changed {
|
||||
self.mark_mcp_runtime_dirty();
|
||||
}
|
||||
state.session_configuration = next.clone();
|
||||
let new_config = notify_config_contributors
|
||||
.then(|| self.build_effective_session_config(&state.session_configuration));
|
||||
|
||||
Reference in New Issue
Block a user