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
This commit is contained in:
Matthew Zeng authored and copyberry committed 2026-09-09 23:23:59 +00:00
1 parent e722303e38
commit b5544d5732
29 files changed
+374 -12

No files matched your search

@@ -727,6 +727,7 @@ mod tests {
item: RolloutItem::TurnContext(TurnContextItem { item: RolloutItem::TurnContext(TurnContextItem {
turn_id: Some((*turn_id).to_string()), turn_id: Some((*turn_id).to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: AbsolutePathBuf::from_absolute_path(tempdir.path()) cwd: AbsolutePathBuf::from_absolute_path(tempdir.path())
.expect("absolute feedback rollout directory"), .expect("absolute feedback rollout directory"),
workspace_roots: None, workspace_roots: None,
@@ -30,6 +30,7 @@ fn settings_item(
ThreadSettingsAppliedEvent { ThreadSettingsAppliedEvent {
thread_id: None, thread_id: None,
thread_settings: ThreadSettingsSnapshot { thread_settings: ThreadSettingsSnapshot {
disabled_plugin_ids: Vec::new(),
model: "gpt-5".to_string(), model: "gpt-5".to_string(),
model_provider_id: "openai".to_string(), model_provider_id: "openai".to_string(),
service_tier: None, service_tier: None,
@@ -64,6 +65,7 @@ fn turn_context_item(
RolloutItem::TurnContext(TurnContextItem { RolloutItem::TurnContext(TurnContextItem {
turn_id: Some(turn_id.to_string()), turn_id: Some(turn_id.to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: cwd(), cwd: cwd(),
workspace_roots: Some(vec![cwd()]), workspace_roots: Some(vec![cwd()]),
current_date: None, current_date: None,
@@ -874,6 +874,7 @@ impl TurnRequestProcessor {
if has_any_overrides { if has_any_overrides {
thread thread
.preview_thread_settings_overrides(CodexThreadSettingsOverrides { .preview_thread_settings_overrides(CodexThreadSettingsOverrides {
disabled_plugin_ids: None,
environments: environments.clone(), environments: environments.clone(),
runtime_workspace_roots: runtime_workspace_roots.clone(), runtime_workspace_roots: runtime_workspace_roots.clone(),
approval_policy, approval_policy,
@@ -897,6 +898,7 @@ impl TurnRequestProcessor {
} }
Ok(codex_protocol::protocol::ThreadSettingsOverrides { Ok(codex_protocol::protocol::ThreadSettingsOverrides {
disabled_plugin_ids: None,
environments, environments,
runtime_workspace_roots, runtime_workspace_roots,
profile_workspace_roots, profile_workspace_roots,
+1
View File
@@ -1204,6 +1204,7 @@ async fn spawn_agent_fork_from_paginated_parent_uses_model_context_prefix() {
ThreadSettingsAppliedEvent { ThreadSettingsAppliedEvent {
thread_id: Some(parent_thread_id), thread_id: Some(parent_thread_id),
thread_settings: ThreadSettingsSnapshot { thread_settings: ThreadSettingsSnapshot {
disabled_plugin_ids: Vec::new(),
model: "parent-only-model".to_string(), model: "parent-only-model".to_string(),
model_provider_id: "parent-only-provider".to_string(), model_provider_id: "parent-only-provider".to_string(),
service_tier: None, service_tier: None,
+1
View File
@@ -106,6 +106,7 @@ pub(crate) async fn run_codex_thread_interactive(
code_mode_session_provider: parent_session.services.code_mode_service.session_provider(), code_mode_session_provider: parent_session.services.code_mode_service.session_provider(),
extensions, extensions,
conversation_history, conversation_history,
disabled_plugin_ids: None,
requested_history_mode: None, requested_history_mode: None,
fork_persistence: ForkPersistence::Copied, fork_persistence: ForkPersistence::Copied,
session_source, session_source,
+3
View File
@@ -152,6 +152,7 @@ pub struct CodexThreadSettingsOverrides {
pub service_tier: Option<Option<String>>, pub service_tier: Option<Option<String>>,
pub collaboration_mode: Option<CollaborationMode>, pub collaboration_mode: Option<CollaborationMode>,
pub personality: Option<Personality>, pub personality: Option<Personality>,
pub disabled_plugin_ids: Option<Vec<String>>,
} }
pub use codex_guardian_context::GuardianRootMessage; pub use codex_guardian_context::GuardianRootMessage;
@@ -560,6 +561,7 @@ impl CodexThread {
service_tier, service_tier,
collaboration_mode, collaboration_mode,
personality, personality,
disabled_plugin_ids,
} = overrides; } = overrides;
SessionSettingsUpdate { SessionSettingsUpdate {
step_settings: StepSettingsUpdate { step_settings: StepSettingsUpdate {
@@ -579,6 +581,7 @@ impl CodexThread {
permission_profile, permission_profile,
active_permission_profile, active_permission_profile,
windows_sandbox_level, windows_sandbox_level,
disabled_plugin_ids,
..Default::default() ..Default::default()
} }
} }
@@ -359,6 +359,7 @@ fn reference_context_item() -> TurnContextItem {
TurnContextItem { TurnContextItem {
turn_id: Some("reference-turn".to_string()), turn_id: Some("reference-turn".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: AbsolutePathBuf::try_from( cwd: AbsolutePathBuf::try_from(
std::env::current_dir() std::env::current_dir()
.expect("current directory") .expect("current directory")
+19
View File
@@ -436,6 +436,7 @@ pub(crate) struct SessionSpawnArgs {
pub(crate) code_mode_session_provider: Arc<dyn codex_code_mode::CodeModeSessionProvider>, pub(crate) code_mode_session_provider: Arc<dyn codex_code_mode::CodeModeSessionProvider>,
pub(crate) extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>, pub(crate) extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
pub(crate) conversation_history: InitialHistory, pub(crate) conversation_history: InitialHistory,
pub(crate) disabled_plugin_ids: Option<Vec<String>>,
pub(crate) requested_history_mode: Option<ThreadHistoryMode>, pub(crate) requested_history_mode: Option<ThreadHistoryMode>,
pub(crate) fork_persistence: ForkPersistence, pub(crate) fork_persistence: ForkPersistence,
pub(crate) session_source: SessionSource, pub(crate) session_source: SessionSource,
@@ -540,6 +541,7 @@ impl Session {
code_mode_session_provider, code_mode_session_provider,
extensions, extensions,
conversation_history, conversation_history,
disabled_plugin_ids,
requested_history_mode, requested_history_mode,
fork_persistence, fork_persistence,
session_source, session_source,
@@ -727,6 +729,22 @@ impl Session {
} else { } else {
dynamic_tools dynamic_tools
}; };
let disabled_plugin_ids = disabled_plugin_ids.unwrap_or_else(|| {
let settings_owner = match &conversation_history {
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
InitialHistory::Forked(_) => forked_from_thread_id,
InitialHistory::New | InitialHistory::Cleared => None,
};
settings_owner
.and_then(|thread_id| {
codex_history::latest_disabled_plugin_ids(
conversation_history.get_rollout_items(),
thread_id,
)
})
.map(<[String]>::to_vec)
.unwrap_or_default()
});
// TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode // TODO (aibrahim): Consolidate config.model and config.model_reasoning_effort into config.collaboration_mode
// to avoid extracting these fields separately and constructing CollaborationMode here. // to avoid extracting these fields separately and constructing CollaborationMode here.
let collaboration_mode = CollaborationMode { let collaboration_mode = CollaborationMode {
@@ -771,6 +789,7 @@ impl Session {
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids,
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name, metrics_service_name,
app_server_client_name: None, app_server_client_name: None,
+1
View File
@@ -147,6 +147,7 @@ pub(super) async fn spawn_review_thread(
config: per_turn_config, config: per_turn_config,
auth_manager: auth_manager_for_context, auth_manager: auth_manager_for_context,
initial_settings: Arc::clone(&step_settings), initial_settings: Arc::clone(&step_settings),
disabled_plugin_ids: parent_turn_context.disabled_plugin_ids.clone(),
current_settings: ArcSwap::from(step_settings), current_settings: ArcSwap::from(step_settings),
session_telemetry: session_telemetry_for_context, session_telemetry: session_telemetry_for_context,
provider: provider_for_context, provider: provider_for_context,
@@ -266,6 +266,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
let previous_context_item = TurnContextItem { let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -316,6 +317,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
let mut previous_context_item = TurnContextItem { let mut previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -1598,6 +1600,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
let previous_context_item = TurnContextItem { let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: Some("root-turn".to_string()), root_turn_id: Some("root-turn".to_string()),
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -1695,6 +1698,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
serde_json::to_value(Some(TurnContextItem { serde_json::to_value(Some(TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: Some("root-turn".to_string()), root_turn_id: Some("root-turn".to_string()),
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -1730,6 +1734,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
let previous_context_item = TurnContextItem { let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -1868,6 +1873,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
let current_context_item = TurnContextItem { let current_context_item = TurnContextItem {
turn_id: Some(current_turn_id.clone()), turn_id: Some(current_turn_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -1998,6 +2004,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
let previous_context_item = TurnContextItem { let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -2176,6 +2183,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
let previous_context_item = TurnContextItem { let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
+8
View File
@@ -116,6 +116,8 @@ pub(crate) struct SessionConfiguration {
pub(super) codex_home: AbsolutePathBuf, pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session. /// Optional user-facing name for the thread, updated during the session.
pub(super) thread_name: Option<String>, pub(super) thread_name: Option<String>,
/// Thread-owned plugin selection inherited by future turns.
pub(super) disabled_plugin_ids: Vec<String>,
// TODO(pakrym): Remove config from here // TODO(pakrym): Remove config from here
pub(super) original_config_do_not_use: Arc<Config>, pub(super) original_config_do_not_use: Arc<Config>,
@@ -302,6 +304,7 @@ impl SessionConfiguration {
reasoning_summary: self.step_settings.reasoning_summary, reasoning_summary: self.step_settings.reasoning_summary,
personality: self.step_settings.personality, personality: self.step_settings.personality,
collaboration_mode: self.step_settings.collaboration_mode.clone(), collaboration_mode: self.step_settings.collaboration_mode.clone(),
disabled_plugin_ids: self.disabled_plugin_ids.clone(),
} }
} }
@@ -330,6 +333,7 @@ impl SessionConfiguration {
service_tier: Some(self.step_settings.service_tier.clone()), service_tier: Some(self.step_settings.service_tier.clone()),
collaboration_mode: Some(self.step_settings.collaboration_mode.clone()), collaboration_mode: Some(self.step_settings.collaboration_mode.clone()),
personality: self.step_settings.personality, personality: self.step_settings.personality,
disabled_plugin_ids: Some(self.disabled_plugin_ids.clone()),
..Default::default() ..Default::default()
} }
} }
@@ -370,6 +374,9 @@ impl SessionConfiguration {
current_environments: &[TurnEnvironmentSelection], current_environments: &[TurnEnvironmentSelection],
) -> ConstraintResult<Self> { ) -> ConstraintResult<Self> {
let mut next_configuration = self.clone(); let mut next_configuration = self.clone();
if let Some(disabled_plugin_ids) = &updates.disabled_plugin_ids {
next_configuration.disabled_plugin_ids = disabled_plugin_ids.clone();
}
let current_file_system_sandbox_policy = let current_file_system_sandbox_policy =
self.file_system_sandbox_policy(current_environments); self.file_system_sandbox_policy(current_environments);
let file_system_policy_has_rebindable_project_root_write = let file_system_policy_has_rebindable_project_root_write =
@@ -573,6 +580,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) service_tier_for_turn: Option<String>, pub(crate) service_tier_for_turn: Option<String>,
pub(crate) app_server_client_name: Option<String>, pub(crate) app_server_client_name: Option<String>,
pub(crate) app_server_client_version: Option<String>, pub(crate) app_server_client_version: Option<String>,
pub(crate) disabled_plugin_ids: Option<Vec<String>>,
} }
pub(crate) struct AppServerClientMetadata { pub(crate) struct AppServerClientMetadata {
+9
View File
@@ -3807,6 +3807,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
let previous_context_item = TurnContextItem { let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()), turn_id: Some(turn_context.sub_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
#[allow(deprecated)] #[allow(deprecated)]
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
workspace_roots: None, workspace_roots: None,
@@ -4527,6 +4528,7 @@ async fn set_rate_limits_retains_previous_credits() {
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -4647,6 +4649,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -5293,6 +5296,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -6376,6 +6380,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -6535,6 +6540,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -6835,6 +6841,7 @@ async fn make_session_with_config_and_rx(
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -6963,6 +6970,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -8701,6 +8709,7 @@ where
runtime_workspace_roots: config.workspace_roots.clone(), runtime_workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(), codex_home: config.codex_home.clone(),
thread_name: None, thread_name: None,
disabled_plugin_ids: Vec::new(),
original_config_do_not_use: Arc::clone(&config), original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None, metrics_service_name: None,
app_server_client_name: None, app_server_client_name: None,
@@ -1272,6 +1272,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
code_mode_session_provider: Arc::new(codex_code_mode::DisabledCodeModeSessionProvider), code_mode_session_provider: Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
extensions: codex_extension_api::empty_extension_registry(), extensions: codex_extension_api::empty_extension_registry(),
conversation_history: InitialHistory::New, conversation_history: InitialHistory::New,
disabled_plugin_ids: None,
requested_history_mode: None, requested_history_mode: None,
fork_persistence: ForkPersistence::Copied, fork_persistence: ForkPersistence::Copied,
session_source: SessionSource::SubAgent(SubAgentSource::Other( session_source: SessionSource::SubAgent(SubAgentSource::Other(
@@ -71,6 +71,7 @@ pub(super) fn prepare_update(overrides: ThreadSettingsOverrides) -> SessionSetti
service_tier, service_tier,
collaboration_mode, collaboration_mode,
personality, personality,
disabled_plugin_ids,
} = overrides; } = overrides;
SessionSettingsUpdate { SessionSettingsUpdate {
step_settings: StepSettingsUpdate { step_settings: StepSettingsUpdate {
@@ -90,6 +91,7 @@ pub(super) fn prepare_update(overrides: ThreadSettingsOverrides) -> SessionSetti
permission_profile, permission_profile,
active_permission_profile, active_permission_profile,
windows_sandbox_level, windows_sandbox_level,
disabled_plugin_ids,
..Default::default() ..Default::default()
} }
} }
@@ -295,6 +295,8 @@ pub struct TurnContext {
/// Frozen settings used to construct this context. Legacy turn consumers /// Frozen settings used to construct this context. Legacy turn consumers
/// keep this view even when later steps use different settings. /// keep this view even when later steps use different settings.
pub(crate) initial_settings: Arc<ResolvedStepSettings>, pub(crate) initial_settings: Arc<ResolvedStepSettings>,
/// Thread-owned plugin selection captured when this turn was admitted.
pub(crate) disabled_plugin_ids: Vec<String>,
/// Snapshot for the next step; request consumers use their captured StepContext. /// Snapshot for the next step; request consumers use their captured StepContext.
pub(super) current_settings: ArcSwap<ResolvedStepSettings>, pub(super) current_settings: ArcSwap<ResolvedStepSettings>,
/// Turn-wide telemetry; model-attributed step work should use `StepContext::session_telemetry`. /// Turn-wide telemetry; model-attributed step work should use `StepContext::session_telemetry`.
@@ -594,6 +596,7 @@ impl TurnContext {
use_model_token_budget_defaults: self.use_model_token_budget_defaults, use_model_token_budget_defaults: self.use_model_token_budget_defaults,
auth_manager: self.auth_manager.clone(), auth_manager: self.auth_manager.clone(),
initial_settings: Arc::clone(&step_settings), initial_settings: Arc::clone(&step_settings),
disabled_plugin_ids: self.disabled_plugin_ids.clone(),
current_settings: ArcSwap::from(step_settings), current_settings: ArcSwap::from(step_settings),
session_telemetry, session_telemetry,
provider: self.provider.clone(), provider: self.provider.clone(),
@@ -655,6 +658,7 @@ impl TurnContext {
TurnContextItem { TurnContextItem {
turn_id: Some(self.sub_id.clone()), turn_id: Some(self.sub_id.clone()),
root_turn_id: self.turn_metadata_state.root_turn_id(), root_turn_id: self.turn_metadata_state.root_turn_id(),
disabled_plugin_ids: Some(self.disabled_plugin_ids.clone()),
cwd, cwd,
workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots), workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
current_date: self.current_date.clone(), current_date: self.current_date.clone(),
@@ -866,6 +870,7 @@ impl Session {
use_model_token_budget_defaults, use_model_token_budget_defaults,
auth_manager, auth_manager,
initial_settings: Arc::clone(&step_settings), initial_settings: Arc::clone(&step_settings),
disabled_plugin_ids: session_configuration.disabled_plugin_ids.clone(),
current_settings: ArcSwap::from(step_settings), current_settings: ArcSwap::from(step_settings),
session_telemetry: session_telemetry_for_context, session_telemetry: session_telemetry_for_context,
provider, provider,
+17
View File
@@ -247,6 +247,8 @@ pub struct StartThreadOptions {
pub client_mcp_extensions: ClientMcpExtensions, pub client_mcp_extensions: ClientMcpExtensions,
/// Thread ID reserved before startup so the caller can associate host-owned state with it. /// Thread ID reserved before startup so the caller can associate host-owned state with it.
pub reserved_thread_id: Option<ThreadId>, pub reserved_thread_id: Option<ThreadId>,
/// Initial thread-owned plugin selection; omission restores persisted settings.
pub disabled_plugin_ids: Option<Vec<String>>,
} }
impl StartThreadOptions { impl StartThreadOptions {
@@ -267,6 +269,7 @@ impl StartThreadOptions {
thread_extension_init: ExtensionDataInit::default(), thread_extension_init: ExtensionDataInit::default(),
client_mcp_extensions: ClientMcpExtensions::default(), client_mcp_extensions: ClientMcpExtensions::default(),
reserved_thread_id: None, reserved_thread_id: None,
disabled_plugin_ids: None,
} }
} }
} }
@@ -1410,6 +1413,18 @@ impl ThreadManager {
InitialHistory::Forked(_) => history.forked_from_id(), InitialHistory::Forked(_) => history.forked_from_id(),
InitialHistory::New | InitialHistory::Cleared => None, InitialHistory::New | InitialHistory::Cleared => None,
}; };
// Capture the source's settings before truncating its model history.
options.disabled_plugin_ids = Some(options.disabled_plugin_ids.unwrap_or_else(|| {
source_thread_id
.and_then(|thread_id| {
codex_history::latest_disabled_plugin_ids(
history.get_rollout_items(),
thread_id,
)
})
.map(<[String]>::to_vec)
.unwrap_or_default()
}));
let multi_agent_version = self let multi_agent_version = self
.state .state
.effective_multi_agent_version_for_spawn( .effective_multi_agent_version_for_spawn(
@@ -1914,6 +1929,7 @@ impl ThreadManagerState {
thread_extension_init, thread_extension_init,
client_mcp_extensions, client_mcp_extensions,
reserved_thread_id, reserved_thread_id,
disabled_plugin_ids,
} = options; } = options;
let inherited_environments = captured_environments.or(inherited_environments); let inherited_environments = captured_environments.or(inherited_environments);
let session_source = session_source.unwrap_or_else(|| self.session_source.clone()); let session_source = session_source.unwrap_or_else(|| self.session_source.clone());
@@ -2041,6 +2057,7 @@ impl ThreadManagerState {
code_mode_session_provider: Arc::clone(&self.code_mode_session_provider), code_mode_session_provider: Arc::clone(&self.code_mode_session_provider),
extensions, extensions,
conversation_history: initial_history, conversation_history: initial_history,
disabled_plugin_ids,
requested_history_mode: history_mode, requested_history_mode: history_mode,
fork_persistence, fork_persistence,
// Keep only the manager registration internal. The session and its saved // Keep only the manager registration internal. The session and its saved
+6 -11
View File
@@ -3768,23 +3768,19 @@ async fn code_mode_wait_uses_its_own_max_tokens_budget() -> Result<()> {
let _ = config.features.enable(Feature::CodeMode); let _ = config.features.enable(Feature::CodeMode);
}); });
let test = builder.build(&server).await?; let test = builder.build(&server).await?;
let completion_gate = test.workspace_path("code-mode-max-tokens.ready");
let completion_wait = wait_for_file_source(&completion_gate)?;
let code = format!( // An explicit yield separates the output batches without a nested shell call.
r#"// @exec: {{"max_output_tokens": 100}} let code = r#"// @exec: {"max_output_tokens": 100}
text("phase 1"); text("phase 1");
yield_control(); yield_control();
{completion_wait}
text("token one token two token three token four token five token six token seven"); text("token one token two token three token four token five token six token seven");
"# "#;
);
responses::mount_sse_once( responses::mount_sse_once(
&server, &server,
sse(vec![ sse(vec![
ev_response_created("resp-1"), ev_response_created("resp-1"),
ev_custom_tool_call("call-1", "exec", &code), ev_custom_tool_call("call-1", "exec", code),
ev_completed("resp-1"), ev_completed("resp-1"),
]), ]),
) )
@@ -3802,11 +3798,10 @@ text("token one token two token three token four token five token six token seve
let first_request = first_completion.single_request(); let first_request = first_completion.single_request();
let first_items = custom_tool_output_items(&first_request, "call-1"); let first_items = custom_tool_output_items(&first_request, "call-1");
assert_eq!(first_items.len(), 2); assert_eq!(first_items.len(), 2, "exec output: {first_items:?}");
assert_eq!(text_item(&first_items, /*index*/ 1), "phase 1"); assert_eq!(text_item(&first_items, /*index*/ 1), "phase 1");
let cell_id = extract_running_cell_id(text_item(&first_items, /*index*/ 0)); let cell_id = extract_running_cell_id(text_item(&first_items, /*index*/ 0));
fs::write(&completion_gate, "ready")?;
responses::mount_sse_once( responses::mount_sse_once(
&server, &server,
sse(vec![ sse(vec![
@@ -3837,7 +3832,7 @@ text("token one token two token three token four token five token six token seve
let second_request = second_completion.single_request(); let second_request = second_completion.single_request();
let second_items = function_tool_output_items(&second_request, "call-2"); let second_items = function_tool_output_items(&second_request, "call-2");
assert_eq!(second_items.len(), 2); assert_eq!(second_items.len(), 2, "wait output: {second_items:?}");
assert_regex_match( assert_regex_match(
concat!( concat!(
r"(?s)\A", r"(?s)\A",
@@ -31,6 +31,7 @@ fn resume_history(
let turn_ctx = TurnContextItem { let turn_ctx = TurnContextItem {
turn_id: Some(turn_id.clone()), turn_id: Some(turn_id.clone()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: config.cwd.clone(), cwd: config.cwd.clone(),
workspace_roots: None, workspace_roots: None,
current_date: None, current_date: None,
+111 -1
View File
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use codex_core::StartIfIdleSubmission;
use codex_core::TurnInputRequest; use codex_core::TurnInputRequest;
use codex_core::TurnInputSubmission; use codex_core::TurnInputSubmission;
use codex_core::config::Config; use codex_core::config::Config;
@@ -9,7 +10,9 @@ use codex_history::RolloutItem;
use codex_history::RolloutLine; use codex_history::RolloutLine;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op; use codex_protocol::protocol::Op;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::ThreadSettingsSnapshot;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::responses; use core_test_support::responses;
@@ -17,6 +20,7 @@ use core_test_support::skip_if_no_network;
use core_test_support::streaming_sse::StreamingSseChunk; use core_test_support::streaming_sse::StreamingSseChunk;
use core_test_support::streaming_sse::start_streaming_sse_server; use core_test_support::streaming_sse::start_streaming_sse_server;
use core_test_support::submit_thread_settings; use core_test_support::submit_thread_settings;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event; use core_test_support::wait_for_event;
@@ -34,6 +38,106 @@ const INITIAL_MODEL: &str = "gpt-5.4";
const COMMITTED_MODEL: &str = "gpt-5.2"; const COMMITTED_MODEL: &str = "gpt-5.2";
const TIMEOUT: Duration = Duration::from_secs(10); const TIMEOUT: Duration = Duration::from_secs(10);
fn assert_checkpoints(test: &TestCodex, expected: &[ThreadSettingsSnapshot]) -> Result<()> {
let rollout_path = test.codex.rollout_path().expect("rollout path");
let rollout: Vec<RolloutLine> = std::fs::read_to_string(rollout_path)?
.lines()
.map(codex_rollout::parse_rollout_line)
.collect::<std::result::Result<_, _>>()?;
let snapshots: Vec<_> = rollout
.into_iter()
.filter_map(|line| match line.item {
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(applied))
if applied.thread_id == Some(test.session_configured.thread_id) =>
{
Some(applied.thread_settings)
}
_ => None,
})
.collect();
assert_eq!(snapshots, expected);
Ok(())
}
#[test_case(ThreadHistoryMode::Legacy; "legacy")]
#[test_case(ThreadHistoryMode::Paginated; "paginated")]
#[tokio::test]
async fn initial_plugin_ids_use_turn_context_without_extra_settings_checkpoints(
history_mode: ThreadHistoryMode,
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let test = test_codex()
.with_history_mode(history_mode)
.build_with_auto_env(&server)
.await?;
let selected = vec!["slack@openai".to_string()];
submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
disabled_plugin_ids: Some(selected.clone()),
..Default::default()
},
)
.await?;
let response = responses::mount_sse_once(&server, responses::sse_completed("first turn")).await;
let submission = test
.codex
.start_turn_if_idle(TurnInputRequest::user_input(Vec::new()))
.await?;
let StartIfIdleSubmission::Started { turn_id } = submission else {
panic!("expected an accepted first turn, got {submission:?}");
};
wait_for_event(
&test.codex,
|event| matches!(event, EventMsg::TurnComplete(completed) if completed.turn_id == turn_id),
)
.await;
test.codex.flush_rollout().await?;
assert_checkpoints(&test, &[])?;
let rollout_path = test.codex.rollout_path().expect("rollout path");
let (items, _, parse_errors) =
codex_rollout::RolloutRecorder::load_rollout_items(&rollout_path).await?;
assert_eq!(parse_errors, 0);
let context = items.iter().find_map(|item| match item {
RolloutItem::TurnContext(context)
if context.turn_id.as_deref() == Some(turn_id.as_str()) =>
{
Some(context)
}
_ => None,
});
assert_eq!(
context
.expect("inputless first turn context")
.disabled_plugin_ids,
Some(selected)
);
assert_eq!(response.requests().len(), 1);
submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
disabled_plugin_ids: Some(vec![]),
..Default::default()
},
)
.await?;
let expected = vec![test.codex.thread_settings_snapshot().await];
assert!(expected[0].disabled_plugin_ids.is_empty());
test.codex.flush_rollout().await?;
assert_checkpoints(&test, &expected)?;
let response =
responses::mount_sse_once(&server, responses::sse_completed("second turn")).await;
test.submit_text_turn("second turn").await?;
test.codex.flush_rollout().await?;
assert_eq!(response.requests().len(), 1);
assert_checkpoints(&test, &expected)?;
test.codex.shutdown_and_wait().await?;
Ok(())
}
struct PauseAfterCommit { struct PauseAfterCommit {
gate: Mutex<Option<(oneshot::Sender<()>, mpsc::Receiver<()>)>>, gate: Mutex<Option<(oneshot::Sender<()>, mpsc::Receiver<()>)>>,
} }
@@ -94,9 +198,12 @@ async fn settings_notifications_keep_their_commit_across_postcommit_work(
.with_extensions(Arc::new(extensions.build())) .with_extensions(Arc::new(extensions.build()))
.build_with_auto_env(&server) .build_with_auto_env(&server)
.await?; .await?;
let initial = test.codex.restorable_thread_settings().await; let mut initial = test.codex.restorable_thread_settings().await;
// Restore only runtime model settings, without overwriting the committed plugin selection.
initial.disabled_plugin_ids = None;
let thread_settings = ThreadSettingsOverrides { let thread_settings = ThreadSettingsOverrides {
model: Some(COMMITTED_MODEL.to_string()), model: Some(COMMITTED_MODEL.to_string()),
disabled_plugin_ids: Some(vec!["slack@openai".to_string()]),
..Default::default() ..Default::default()
}; };
let submission = tokio::spawn({ let submission = tokio::spawn({
@@ -128,11 +235,13 @@ async fn settings_notifications_keep_their_commit_across_postcommit_work(
timeout(TIMEOUT, entered_rx).await??; timeout(TIMEOUT, entered_rx).await??;
let expected = test.codex.thread_settings_snapshot().await; let expected = test.codex.thread_settings_snapshot().await;
assert_eq!(expected.model, COMMITTED_MODEL); assert_eq!(expected.model, COMMITTED_MODEL);
assert_eq!(expected.disabled_plugin_ids, vec!["slack@openai"]);
// Submitted operations are serialized. Runtime restoration is an existing // Submitted operations are serialized. Runtime restoration is an existing
// direct writer, so it can overlap the first operation's post-commit work. // direct writer, so it can overlap the first operation's post-commit work.
timeout(TIMEOUT, test.codex.restore_thread_settings(initial)).await??; timeout(TIMEOUT, test.codex.restore_thread_settings(initial)).await??;
let restored = test.codex.thread_settings_snapshot().await; let restored = test.codex.thread_settings_snapshot().await;
assert_eq!(restored.model, INITIAL_MODEL); assert_eq!(restored.model, INITIAL_MODEL);
assert_eq!(restored.disabled_plugin_ids, vec!["slack@openai"]);
release_tx.send(())?; release_tx.send(())?;
let submission_id = timeout(TIMEOUT, submission).await???; let submission_id = timeout(TIMEOUT, submission).await???;
@@ -222,6 +331,7 @@ async fn compaction_checkpoints_settings_changed_during_its_model_request() -> R
ThreadSettingsOverrides { ThreadSettingsOverrides {
environments: Some(local_selections(updated_cwd_path.clone())), environments: Some(local_selections(updated_cwd_path.clone())),
model: Some(COMMITTED_MODEL.to_string()), model: Some(COMMITTED_MODEL.to_string()),
disabled_plugin_ids: Some(vec!["slack@openai".to_string()]),
..Default::default() ..Default::default()
}, },
) )
+28
View File
@@ -453,6 +453,34 @@ fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {
}) })
} }
/// Returns a thread's latest plugin selection, with a turn-context fallback.
///
/// Forked history may contain ancestor snapshots, and compaction may append a
/// frozen turn context after an update. Neither can replace thread-owned settings.
/// Without an owned snapshot, only the latest turn context supplies the initial
/// selection; a missing field must not resurrect a selection from an older turn.
pub fn latest_disabled_plugin_ids(items: &[RolloutItem], thread_id: ThreadId) -> Option<&[String]> {
if let Some(ids) = items.iter().rev().find_map(|item| {
if let RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(event)) = item
&& event.thread_id == Some(thread_id)
{
Some(event.thread_settings.disabled_plugin_ids.as_slice())
} else {
None
}
}) {
return Some(ids);
}
items
.iter()
.rev()
.find_map(|item| match item {
RolloutItem::TurnContext(context) => Some(context),
_ => None,
})
.and_then(|context| context.disabled_plugin_ids.as_deref())
}
fn multi_agent_version_from_items( fn multi_agent_version_from_items(
items: &[RolloutItem], items: &[RolloutItem],
thread_id: Option<ThreadId>, thread_id: Option<ThreadId>,
+125
View File
@@ -1,11 +1,136 @@
use anyhow::Result; use anyhow::Result;
use codex_protocol::models::ConfigurationReasoning; use codex_protocol::models::ConfigurationReasoning;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::ThreadSettingsSnapshot;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
use super::*; use super::*;
fn thread_settings_snapshot(disabled_plugin_ids: Vec<String>) -> Result<ThreadSettingsSnapshot> {
Ok(serde_json::from_value(json!({
"model": "gpt-5",
"model_provider_id": "openai",
"approval_policy": "never",
"approvals_reviewer": "user",
"permission_profile": codex_protocol::models::PermissionProfile::read_only(),
"cwd": std::env::current_dir()?,
"collaboration_mode": {
"mode": "default",
"settings": {
"model": "gpt-5",
"reasoning_effort": null,
"developer_instructions": null
}
},
"disabled_plugin_ids": disabled_plugin_ids
}))?)
}
#[test]
fn latest_disabled_plugin_ids_preserves_owned_updates_and_clears() -> Result<()> {
let thread_id = ThreadId::new();
let ancestor_id = ThreadId::new();
let selected = thread_settings_snapshot(vec!["example@marketplace".to_string()])?;
let cleared = thread_settings_snapshot(Vec::new())?;
let item = |thread_id, thread_settings| {
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(
codex_protocol::protocol::ThreadSettingsAppliedEvent {
thread_id,
thread_settings,
},
))
};
let foreign_items = vec![
item(Some(ancestor_id), selected.clone()),
item(/*thread_id*/ None, selected.clone()),
];
assert_eq!(latest_disabled_plugin_ids(&foreign_items, thread_id), None);
let mut history = vec![item(Some(thread_id), selected.clone())];
history.extend(foreign_items.clone());
assert_eq!(
latest_disabled_plugin_ids(&history, thread_id),
Some(selected.disabled_plugin_ids.as_slice())
);
history.push(item(Some(thread_id), cleared.clone()));
history.extend(foreign_items);
history.push(turn_context_with_disabled_plugins(Some(
selected.disabled_plugin_ids,
))?);
assert_eq!(
latest_disabled_plugin_ids(&history, thread_id),
Some(cleared.disabled_plugin_ids.as_slice())
);
Ok(())
}
fn turn_context_with_disabled_plugins(
disabled_plugin_ids: Option<Vec<String>>,
) -> Result<RolloutItem> {
let mut context: TurnContextItem = serde_json::from_value(json!({
"cwd": std::env::current_dir()?,
"approval_policy": "never",
"sandbox_policy": { "type": "danger-full-access" },
"model": "gpt-5",
"summary": "auto"
}))?;
context.disabled_plugin_ids = disabled_plugin_ids;
Ok(RolloutItem::TurnContext(context))
}
#[test]
fn latest_disabled_plugin_ids_falls_back_only_to_latest_turn_context() -> Result<()> {
let thread_id = ThreadId::new();
let selected = vec!["example@marketplace".to_string()];
let mut history = vec![turn_context_with_disabled_plugins(Some(selected.clone()))?];
assert_eq!(
latest_disabled_plugin_ids(&history, thread_id),
Some(selected.as_slice())
);
history.push(RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(
codex_protocol::protocol::ThreadSettingsAppliedEvent {
thread_id: Some(ThreadId::new()),
thread_settings: thread_settings_snapshot(Vec::new())?,
},
)));
assert_eq!(
latest_disabled_plugin_ids(&history, thread_id),
Some(selected.as_slice())
);
let cleared = Vec::new();
history.push(turn_context_with_disabled_plugins(Some(cleared.clone()))?);
assert_eq!(
latest_disabled_plugin_ids(&history, thread_id),
Some(cleared.as_slice())
);
history.push(turn_context_with_disabled_plugins(
/*disabled_plugin_ids*/ None,
)?);
assert_eq!(latest_disabled_plugin_ids(&history, thread_id), None);
Ok(())
}
#[test]
fn older_thread_settings_snapshot_defaults_disabled_plugins_to_empty() -> Result<()> {
let expected = thread_settings_snapshot(Vec::new())?;
let mut legacy = serde_json::to_value(&expected)?;
legacy
.as_object_mut()
.unwrap()
.remove("disabled_plugin_ids");
assert_eq!(
serde_json::from_value::<ThreadSettingsSnapshot>(legacy)?,
expected
);
Ok(())
}
#[test] #[test]
fn response_item_envelope_accessors_preserve_item() { fn response_item_envelope_accessors_preserve_item() {
let expected_item = ResponseItem::Message { let expected_item = ResponseItem::Message {
+12
View File
@@ -573,6 +573,10 @@ pub struct ThreadSettingsOverrides {
/// Updated personality preference. /// Updated personality preference.
pub personality: Option<Personality>, pub personality: Option<Personality>,
/// Replace the thread's disabled plugin IDs. Omission preserves the current
/// selection, and an empty list clears it.
pub disabled_plugin_ids: Option<Vec<String>>,
} }
/// Source classification for client-supplied context. /// Source classification for client-supplied context.
@@ -2220,6 +2224,9 @@ pub struct ThreadSettingsSnapshot {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub personality: Option<Personality>, pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode, pub collaboration_mode: CollaborationMode,
/// Thread-owned plugin selection, retained even when a plugin is unavailable.
#[serde(default)]
pub disabled_plugin_ids: Vec<String>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
@@ -3224,6 +3231,10 @@ pub struct TurnContextItem {
/// Only set for subagent turns; persisted so resume keeps the scope frozen at turn start. /// Only set for subagent turns; persisted so resume keeps the scope frozen at turn start.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub root_turn_id: Option<String>, pub root_turn_id: Option<String>,
/// Plugin selection captured for this turn. Absent in older histories.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub disabled_plugin_ids: Option<Vec<String>>,
pub cwd: AbsolutePathBuf, pub cwd: AbsolutePathBuf,
/// Effective workspace roots used to materialize symbolic /// Effective workspace roots used to materialize symbolic
/// `:workspace_roots` filesystem permissions in `permission_profile`. /// `:workspace_roots` filesystem permissions in `permission_profile`.
@@ -6061,6 +6072,7 @@ mod tests {
let item = TurnContextItem { let item = TurnContextItem {
turn_id: None, turn_id: None,
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: test_path_buf("/tmp").abs(), cwd: test_path_buf("/tmp").abs(),
workspace_roots: None, workspace_roots: None,
current_date: None, current_date: None,
+1
View File
@@ -1798,6 +1798,7 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re
item: RolloutItem::TurnContext(TurnContextItem { item: RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()), turn_id: Some("turn-1".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!(&latest_cwd)) cwd: serde_json::from_value(serde_json::json!(&latest_cwd))
.expect("absolute latest cwd"), .expect("absolute latest cwd"),
workspace_roots: None, workspace_roots: None,
+5
View File
@@ -432,6 +432,7 @@ mod tests {
&RolloutItem::TurnContext(TurnContextItem { &RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()), turn_id: Some("turn-1".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!( cwd: serde_json::from_value(serde_json::json!(
std::env::current_dir() std::env::current_dir()
.expect("current directory") .expect("current directory")
@@ -481,6 +482,7 @@ mod tests {
&RolloutItem::TurnContext(TurnContextItem { &RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()), turn_id: Some("turn-1".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!( cwd: serde_json::from_value(serde_json::json!(
std::env::current_dir() std::env::current_dir()
.expect("current directory") .expect("current directory")
@@ -530,6 +532,7 @@ mod tests {
&RolloutItem::TurnContext(TurnContextItem { &RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()), turn_id: Some("turn-1".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!(&fallback_cwd)) cwd: serde_json::from_value(serde_json::json!(&fallback_cwd))
.expect("absolute fallback cwd"), .expect("absolute fallback cwd"),
workspace_roots: None, workspace_roots: None,
@@ -568,6 +571,7 @@ mod tests {
&RolloutItem::TurnContext(TurnContextItem { &RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()), turn_id: Some("turn-1".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!( cwd: serde_json::from_value(serde_json::json!(
std::env::current_dir() std::env::current_dir()
.expect("current directory") .expect("current directory")
@@ -613,6 +617,7 @@ mod tests {
ThreadSettingsAppliedEvent { ThreadSettingsAppliedEvent {
thread_id: None, thread_id: None,
thread_settings: ThreadSettingsSnapshot { thread_settings: ThreadSettingsSnapshot {
disabled_plugin_ids: Vec::new(),
model: "gpt-5.2-codex".to_string(), model: "gpt-5.2-codex".to_string(),
model_provider_id: "updated-provider".to_string(), model_provider_id: "updated-provider".to_string(),
service_tier: None, service_tier: None,
+1
View File
@@ -957,6 +957,7 @@ mod tests {
RolloutItem::TurnContext(TurnContextItem { RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()), turn_id: Some("turn-1".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!(cwd)).expect("absolute cwd"), cwd: serde_json::from_value(serde_json::json!(cwd)).expect("absolute cwd"),
workspace_roots: None, workspace_roots: None,
current_date: None, current_date: None,
@@ -851,6 +851,7 @@ fn turn_context(root: &Path, turn_id: &str) -> RolloutItem {
RolloutItem::TurnContext(TurnContextItem { RolloutItem::TurnContext(TurnContextItem {
turn_id: Some(turn_id.to_string()), turn_id: Some(turn_id.to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(serde_json::json!(root)).expect("absolute cwd"), cwd: serde_json::from_value(serde_json::json!(root)).expect("absolute cwd"),
workspace_roots: None, workspace_roots: None,
current_date: None, current_date: None,
@@ -1734,6 +1734,7 @@ async fn migration_compacts_subagent_prefix_and_does_not_project_it() {
RolloutItem::TurnContext(TurnContextItem { RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("child-turn".to_string()), turn_id: Some("child-turn".to_string()),
root_turn_id: None, root_turn_id: None,
disabled_plugin_ids: None,
cwd: serde_json::from_value(json!(home.path())).expect("absolute cwd"), cwd: serde_json::from_value(json!(home.path())).expect("absolute cwd"),
workspace_roots: None, workspace_roots: None,
current_date: None, current_date: None,
@@ -656,6 +656,7 @@ mod tests {
ThreadSettingsAppliedEvent { ThreadSettingsAppliedEvent {
thread_id: None, thread_id: None,
thread_settings: ThreadSettingsSnapshot { thread_settings: ThreadSettingsSnapshot {
disabled_plugin_ids: Vec::new(),
model: "gpt-5.2-codex".to_string(), model: "gpt-5.2-codex".to_string(),
model_provider_id: "updated-provider".to_string(), model_provider_id: "updated-provider".to_string(),
service_tier: None, service_tier: None,