Add thread/settings/update app-server API (#23502)

## Why

App-server clients need a way to update a thread's next-turn settings
without starting a turn, adding transcript content, or waiting for turn
lifecycle events. This gives settings UI a direct path for durable
thread settings while clients observe the eventual effective state
through a notification.

This is a simplified rework of PR
https://github.com/openai/codex/pull/22509. In particular, it changes
the `thread/settings/update` api to return immediately rather than
waiting and returning the effective (updated) thread settings. This
makes the new api consistent with `turn/start` and greatly reduces the
complexity of the implementation relative to the earlier attempt.

## What Changed

- Adds experimental `thread/settings/update` with partial-update request
fields and an empty acknowledgment response.
- Adds experimental `thread/settings/updated`, carrying full effective
`ThreadSettings` and scoped by `threadId` to subscribed clients for the
affected thread.
- Shares durable settings validation with `turn/start`, including
`sandboxPolicy` plus `permissions` rejection and `serviceTier: null`
clearing.
- Emits the same settings notification when `turn/start` overrides
change the stored effective thread settings.
- Regenerates app-server protocol schema fixtures and updates
`app-server/README.md`.
This commit is contained in:
Eric Traut authored and GitHub committed 2026-05-20 11:03:20 -07:00
1 parent 2b4898cc47
commit 771a4e74ac
27 files changed
+2092 -159

No files matched your search

@@ -11,7 +11,9 @@ use super::TurnEnvironmentParams;
use super::TurnItemsView;
use super::shared::v2_enum_from_core;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
@@ -219,6 +221,93 @@ pub struct ThreadStartResponse {
pub reasoning_effort: Option<ReasoningEffort>,
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSettingsUpdateParams {
pub thread_id: String,
/// Override the working directory for subsequent turns.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Override the approval policy for subsequent turns.
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
/// Override where approval requests are routed for subsequent turns.
#[ts(optional = nullable)]
pub approvals_reviewer: Option<ApprovalsReviewer>,
/// Override the sandbox policy for subsequent turns.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Select a named permissions profile id for subsequent turns. Cannot be
/// combined with `sandboxPolicy`.
#[experimental("thread/settings/update.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<String>,
/// Override the model for subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
/// Override the service tier for subsequent turns. `null` clears the
/// current service tier; omission leaves it unchanged.
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub service_tier: Option<Option<String>>,
/// Override the reasoning effort for subsequent turns.
#[ts(optional = nullable)]
pub effort: Option<ReasoningEffort>,
/// Override the reasoning summary for subsequent turns.
#[ts(optional = nullable)]
pub summary: Option<ReasoningSummary>,
/// EXPERIMENTAL - Set a pre-set collaboration mode for subsequent turns.
///
/// For `collaboration_mode.settings.developer_instructions`, `null` means
/// "use the built-in instructions for the selected mode".
#[experimental("thread/settings/update.collaborationMode")]
#[ts(optional = nullable)]
pub collaboration_mode: Option<CollaborationMode>,
/// Override the personality for subsequent turns.
#[ts(optional = nullable)]
pub personality: Option<Personality>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSettingsUpdateResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSettings {
pub cwd: AbsolutePathBuf,
pub approval_policy: AskForApproval,
pub approvals_reviewer: ApprovalsReviewer,
pub sandbox_policy: SandboxPolicy,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub model: String,
pub model_provider: String,
pub service_tier: Option<String>,
pub effort: Option<ReasoningEffort>,
pub summary: Option<ReasoningSummary>,
pub collaboration_mode: CollaborationMode,
pub personality: Option<Personality>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSettingsUpdatedNotification {
pub thread_id: String,
pub thread_settings: ThreadSettings,
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]