diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index d4bec1c503..8b8c2bd584 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -528,7 +528,7 @@ ] }, "reloadUserConfig": { - "description": "When true, hot-reload the updated user config into all loaded threads after writing.", + "description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.", "type": "boolean" } }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b5555ecc7c..62505923e0 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -8241,7 +8241,7 @@ ] }, "reloadUserConfig": { - "description": "When true, hot-reload the updated user config into all loaded threads after writing.", + "description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.", "type": "boolean" } }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index befa83c162..6087a29298 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4419,7 +4419,7 @@ ] }, "reloadUserConfig": { - "description": "When true, hot-reload the updated user config into all loaded threads after writing.", + "description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.", "type": "boolean" } }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigBatchWriteParams.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigBatchWriteParams.json index 03cf2edb7a..e85803e4ea 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigBatchWriteParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigBatchWriteParams.json @@ -47,7 +47,7 @@ ] }, "reloadUserConfig": { - "description": "When true, hot-reload the updated user config into all loaded threads after writing.", + "description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.", "type": "boolean" } }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigBatchWriteParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigBatchWriteParams.ts index 352eac28e3..fe82988a21 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigBatchWriteParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigBatchWriteParams.ts @@ -9,6 +9,8 @@ export type ConfigBatchWriteParams = { edits: Array, */ filePath?: string | null, expectedVersion?: string | null, /** - * When true, hot-reload the updated user config into all loaded threads after writing. + * When true, hot-reload updated runtime settings into loaded threads after writing. + * Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and + * personality defaults are not reloaded. */ reloadUserConfig?: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index a1bf83f95e..9b5604adfa 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -864,7 +864,9 @@ pub struct ConfigBatchWriteParams { pub file_path: Option, #[ts(optional = nullable)] pub expected_version: Option, - /// When true, hot-reload the updated user config into all loaded threads after writing. + /// When true, hot-reload updated runtime settings into loaded threads after writing. + /// Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and + /// personality defaults are not reloaded. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub reload_user_config: bool, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 52232dfab4..900dd16f1d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -247,7 +247,7 @@ Example with notification opt-out: - `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Pass the same optional `migrationSource` used for detection so the server reads from the matching source; omitted, `null`, or unrecognized values retain the default behavior. The optional `source` identifies the product that initiated the import, while the optional opaque `providerId` attributes analytics to the provider selected by that product without affecting migration-source selection. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately. - `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection. - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. Writes that overlap a managed requirement are rejected with `configRequirementReadonly`. -- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. +- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults do not reload existing threads. - `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`, including each command handler's optional `additionalContextLimit`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 4237d29260..1ac5efc8dd 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -136,9 +136,26 @@ impl ConfigRequestProcessor { &self, params: ConfigBatchWriteParams, ) -> Result { - self.handle_config_mutation_result(self.batch_write_inner(params).await) - .await - .map(ClientResponsePayload::ConfigBatchWrite) + let session_defaults_only = !params.edits.is_empty() + && params.edits.iter().all(|edit| { + matches!( + edit.key_path.as_str(), + "model" + | "model_reasoning_effort" + | "plan_mode_reasoning_effort" + | "service_tier" + | "personality" + ) + }); + let reload_user_config = params.reload_user_config; + let response = self.batch_write_inner(params).await?; + if !session_defaults_only { + self.handle_config_mutation().await; + if reload_user_config { + self.reload_user_config().await; + } + } + Ok(ClientResponsePayload::ConfigBatchWrite(response)) } pub(crate) async fn experimental_feature_enablement_set( @@ -149,6 +166,9 @@ impl ConfigRequestProcessor { let response = self .handle_config_mutation_result(self.set_experimental_feature_enablement(params).await) .await?; + if !response.enablement.is_empty() { + self.reload_user_config().await; + } self.outgoing .send_response_as( request_id, @@ -219,7 +239,6 @@ impl ConfigRequestProcessor { &self, params: ConfigBatchWriteParams, ) -> Result { - let reload_user_config = params.reload_user_config; let pending_changes = codex_core_plugins::toggles::collect_plugin_enabled_candidates( params .edits @@ -232,9 +251,6 @@ impl ConfigRequestProcessor { .await .map_err(map_error)?; self.emit_plugin_toggle_events(pending_changes).await; - if reload_user_config { - self.reload_user_config().await; - } Ok(response) } @@ -270,7 +286,6 @@ impl ConfigRequestProcessor { .map_err(|_| internal_error("failed to update feature enablement"))?; self.load_latest_config(/*fallback_cwd*/ None).await?; - self.reload_user_config().await; Ok(ExperimentalFeatureEnablementSetResponse { enablement }) } diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 7937cc8792..6e71ce9dc5 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -7,8 +7,12 @@ use app_test_support::MockResponsesConfig; use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::write_chatgpt_auth; +use codex_app_server_protocol::ConfigBatchWriteParams; +use codex_app_server_protocol::ConfigEdit; +use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams; use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse; +use codex_app_server_protocol::MergeStrategy; use codex_app_server_protocol::PluginListParams; use codex_app_server_protocol::PluginListResponse; use codex_app_server_protocol::SkillsChangedNotification; @@ -587,7 +591,8 @@ async fn skills_list_preserves_requested_cwd_order() -> Result<()> { } #[tokio::test] -async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> { +async fn skills_list_uses_cached_result_after_session_default_writes_until_force_reload() +-> Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; @@ -621,6 +626,47 @@ async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> { "---\nname: late-extra-skill\ndescription: late skill\n---\n\n# Body\n", )?; + for edits in [ + vec![ConfigEdit { + key_path: "plan_mode_reasoning_effort".to_string(), + value: serde_json::json!("high"), + merge_strategy: MergeStrategy::Replace, + }], + vec![ConfigEdit { + key_path: "service_tier".to_string(), + value: serde_json::json!("fast"), + merge_strategy: MergeStrategy::Replace, + }], + vec![ConfigEdit { + key_path: "personality".to_string(), + value: serde_json::json!("friendly"), + merge_strategy: MergeStrategy::Replace, + }], + vec![ + ConfigEdit { + key_path: "model".to_string(), + value: serde_json::json!("gpt-5.4"), + merge_strategy: MergeStrategy::Replace, + }, + ConfigEdit { + key_path: "model_reasoning_effort".to_string(), + value: serde_json::json!("high"), + merge_strategy: MergeStrategy::Replace, + }, + ], + ] { + let write_id = mcp + .send_config_batch_write_request(ConfigBatchWriteParams { + edits, + file_path: None, + expected_version: None, + reload_user_config: true, + }) + .await?; + let _: ConfigWriteResponse = + timeout(DEFAULT_TIMEOUT, mcp.read_response(write_id)).await??; + } + let second_request_id = mcp .send_skills_list_request(SkillsListParams { cwds: vec![cwd.path().to_path_buf()], diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 392246dade..5a42655e09 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -1626,8 +1626,6 @@ impl Session { (previous_config, new_config, config) }; self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref()); - self.services.skills_service.clear_cache(); - self.services.plugins_manager.clear_cache(); let environments = self.services.turn_environments.snapshot().await; let hooks = build_hooks_for_config( config.as_ref(), @@ -1791,6 +1789,8 @@ impl Session { resolve_tool_suggest_config_from_layer_stack(&config.config_layer_stack); config }; + self.services.skills_service.clear_cache(); + self.services.plugins_manager.clear_cache(); self.refresh_runtime_config(next_config).await; }