Files
codex/codex-rs/core/src/session/review.rs
T
Matthew Zeng b5544d5732 Persist disabled plugin IDs in thread settings (#44332)
## What changed

- Add `disabled_plugin_ids` to thread startup options, settings overrides, snapshots, and persisted turn contexts. Omitted overrides preserve the current list; an empty list clears it.
- Restore the selection on resume and capture it before truncating history for a fork. Prefer the owning thread's latest settings snapshot, falling back only to the latest turn context so stale history cannot resurrect an older selection.
- Freeze the selection at turn admission and retain compatibility with histories that lack the new fields.

## Testing

Add coverage for owned settings, explicit clears, latest-turn fallback, and legacy snapshots. Extend settings tests to cover plugin selection during runtime restoration and compaction, and verify initial turn persistence without extra settings checkpoints in both history modes.

GitOrigin-RevId: 75ffbff5763cd93cc5ac69d73b08b237f59bc009
2026-09-09 23:23:59 +00:00

211 lines
9.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::step_settings::ResolvedStepSettings;
use super::*;
use arc_swap::ArcSwap;
use std::sync::atomic::AtomicBool;
/// Spawn a review thread using the given prompt.
pub(super) async fn spawn_review_thread(
sess: Arc<Session>,
config: Arc<Config>,
parent_turn_context: Arc<TurnContext>,
sub_id: String,
resolved: crate::review_prompts::ResolvedReviewRequest,
) {
let model = config
.review_model
.clone()
.unwrap_or_else(|| parent_turn_context.model_info().slug.clone());
let available_models = sess
.services
.models_manager
.list_models(
RefreshStrategy::OnlineIfUncached,
config.http_client_factory(),
)
.await;
let review_model_info = sess
.services
.models_manager
.get_model_info(&model, &config.to_models_manager_config())
.await;
// For reviews, disable web_search and view_image regardless of global settings.
let mut review_features = sess.features.clone();
let _ = review_features.disable(Feature::WebSearchRequest);
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
review_features.get(),
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
sess.services.shell_zsh_path.as_ref(),
sess.services.main_execve_wrapper_exe.as_ref(),
);
let review_prompt = resolved.prompt.clone();
let provider = parent_turn_context.provider.clone();
let auth_manager = parent_turn_context.auth_manager.clone();
let model_info = review_model_info.clone();
let mut selected = parent_turn_context.initial_settings.selected().clone();
let mut reasoning_effort = selected.collaboration_mode.reasoning_effort();
// Build per‑turn client with the requested model/family.
let mut per_turn_config = (*parent_turn_context.config).clone();
// Preserve configured overrides without carrying over the parent model's defaults.
per_turn_config.token_budget = config.token_budget.clone();
per_turn_config.features = review_features.clone();
if let Some(current_effort) = reasoning_effort.as_ref()
&& review_model_info.slug != parent_turn_context.model_info().slug
&& !review_model_info.used_fallback_model_metadata
&& !review_model_info
.supported_reasoning_levels
.iter()
.any(|preset| &preset.effort == current_effort)
{
let supported_reasoning_levels = &review_model_info.supported_reasoning_levels;
reasoning_effort = supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.map(|preset| preset.effort.clone())
.or_else(|| review_model_info.default_reasoning_level.clone());
}
if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) {
let fallback_value = per_turn_config.web_search_mode.value();
tracing::warn!(
error = %err,
?review_web_search_mode,
?fallback_value,
"review web_search_mode is disallowed by requirements; keeping constrained value"
);
}
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = provider.clone();
let session_source = parent_turn_context.session_source.clone();
let (forked_from_thread_id, thread_source, service_tier) = {
let state = sess.state.lock().await;
(
state.session_configuration.forked_from_thread_id,
state.session_configuration.thread_source.clone(),
state
.session_configuration
.step_settings
.service_tier
.clone()
.or_else(|| config.service_tier.clone()),
)
};
let auto_review_enabled = crate::guardian::routes_approval_policy_to_guardian(
per_turn_config.permissions.approval_policy.value(),
per_turn_config.approvals_reviewer,
);
// The original config may retain a requested tier unsupported by the parent
// model. Resolve it against the review model and the shared feature gate.
selected.collaboration_mode.settings.model = model.clone();
selected.collaboration_mode.settings.reasoning_effort = reasoning_effort.clone();
selected.service_tier = service_tier;
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
Arc::new(model_info.clone()),
review_features.enabled(Feature::FastMode),
));
per_turn_config.model = Some(model);
per_turn_config.model_reasoning_effort = reasoning_effort;
per_turn_config.service_tier = step_settings.service_tier.clone();
let session_telemetry_for_context =
step_settings.telemetry(&parent_turn_context.session_telemetry);
let per_turn_config = Arc::new(per_turn_config);
let review_turn_id = sub_id.to_string();
let turn_metadata_state = Arc::new(TurnMetadataState::new(
sess.session_id().to_string(),
sess.thread_id().to_string(),
forked_from_thread_id,
parent_turn_context.parent_thread_id,
&session_source,
thread_source,
review_turn_id.clone(),
#[allow(deprecated)]
parent_turn_context.cwd.clone(),
&parent_turn_context.permission_profile(),
parent_turn_context.windows_sandbox_level,
parent_turn_context.network.is_some(),
auto_review_enabled,
&model_info,
));
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(
review_turn_id.clone(),
));
extension_data.insert(parent_turn_context.skills_snapshot().as_ref().clone());
let review_turn_context = TurnContext {
sub_id: review_turn_id.clone(),
trace_id: current_span_trace_id(),
realtime_active: parent_turn_context.realtime_active,
code_mode_available: parent_turn_context.code_mode_available,
configured_token_budget: per_turn_config.token_budget.clone(),
use_model_token_budget_defaults: per_turn_config.features.enabled(Feature::TokenBudget)
&& !super::token_budget::has_explicit_settings(&per_turn_config),
config: per_turn_config,
auth_manager: auth_manager_for_context,
initial_settings: Arc::clone(&step_settings),
disabled_plugin_ids: parent_turn_context.disabled_plugin_ids.clone(),
current_settings: ArcSwap::from(step_settings),
session_telemetry: session_telemetry_for_context,
provider: provider_for_context,
session_source,
history_mode: parent_turn_context.history_mode,
parent_thread_id: parent_turn_context.parent_thread_id,
originator: parent_turn_context.originator.clone(),
environments: parent_turn_context.environments.clone(),
available_models,
unified_exec_shell_mode,
current_date: parent_turn_context.current_date.clone(),
timezone: parent_turn_context.timezone.clone(),
app_server_client_name: parent_turn_context.app_server_client_name.clone(),
developer_instructions: None,
multi_agent_version: MultiAgentVersion::Disabled,
network: parent_turn_context.network.clone(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
#[allow(deprecated)]
cwd: parent_turn_context.cwd.clone(),
final_output_json_schema: None,
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
turn_metadata_state,
extension_data,
turn_timing_state: Arc::new(TurnTimingState::default()),
terminal_error: Arc::new(Mutex::new(None)),
server_model_warning_emitted: AtomicBool::new(false),
model_verification_emitted: AtomicBool::new(false),
cyber_access_program: None,
};
// Seed the child task with the review prompt as the initial user message.
let input = vec![TurnInput::UserInput {
acceptance_order: None,
content: vec![UserInput::Text {
text: review_prompt,
// Review prompt is synthesized; no UI element ranges to preserve.
text_elements: Vec::new(),
}],
client_id: None,
}];
let tc = Arc::new(review_turn_context);
if tc.environments.single_local_environment_cwd().is_some() {
tc.turn_metadata_state
.spawn_git_enrichment_task(Arc::clone(&sess.services.git_root_discovery));
}
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
// emit a parent TurnStarted. Consider giving review a full parent turn lifecycle
// (TurnStarted + TurnComplete) for consistency with other standalone tasks.
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
.await;
// Announce entering review mode so UIs can switch modes.
let item = TurnItem::EnteredReviewMode(EnteredReviewModeItem {
id: uuid::Uuid::now_v7().to_string(),
target: resolved.target,
user_facing_hint: resolved.user_facing_hint,
});
sess.emit_turn_item_started(&tc, &item).await;
sess.emit_turn_item_completed(&tc, item).await;
}