mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Keep session defaults static during config batch writes (#34940)
## What changed - Treat batch writes containing only `model`, `model_reasoning_effort`, `plan_mode_reasoning_effort`, `service_tier`, or `personality` as defaults for future sessions, even when `reloadUserConfig` is requested. - Keep runtime config reloads and skill/plugin cache invalidation for other config changes, and document which settings remain session-static. - Preserve cached skill listings after session-default writes until a forced reload, with test coverage for each supported default. GitOrigin-RevId: 6d988014cf4758ceb9422a11f7c2bd5fc6b9f90a
This commit is contained in:
+1
-1
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+3
-1
@@ -9,6 +9,8 @@ export type ConfigBatchWriteParams = { edits: Array<ConfigEdit>,
|
||||
*/
|
||||
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, };
|
||||
|
||||
@@ -864,7 +864,9 @@ pub struct ConfigBatchWriteParams {
|
||||
pub file_path: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub expected_version: Option<String>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -136,9 +136,26 @@ impl ConfigRequestProcessor {
|
||||
&self,
|
||||
params: ConfigBatchWriteParams,
|
||||
) -> Result<ClientResponsePayload, JSONRPCErrorError> {
|
||||
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<ConfigWriteResponse, JSONRPCErrorError> {
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user