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

@@ -517,6 +517,13 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::ThreadMetadataUpdateResponse,
},
#[experimental("thread/settings/update")]
ThreadSettingsUpdate => "thread/settings/update" {
params: v2::ThreadSettingsUpdateParams,
inspect_params: true,
serialization: thread_id(params.thread_id),
response: v2::ThreadSettingsUpdateResponse,
},
#[experimental("thread/memoryMode/set")]
ThreadMemoryModeSet => "thread/memoryMode/set" {
params: v2::ThreadMemoryModeSetParams,
@@ -1470,6 +1477,8 @@ server_notification_definitions! {
ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification),
#[experimental("thread/goal/cleared")]
ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification),
#[experimental("thread/settings/updated")]
ThreadSettingsUpdated => "thread/settings/updated" (v2::ThreadSettingsUpdatedNotification),
ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification),
TurnStarted => "turn/started" (v2::TurnStartedNotification),
HookStarted => "hook/started" (v2::HookStartedNotification),
@@ -3094,6 +3103,40 @@ mod tests {
);
}
#[test]
fn thread_settings_updated_notification_is_marked_experimental() {
let notification =
ServerNotification::ThreadSettingsUpdated(v2::ThreadSettingsUpdatedNotification {
thread_id: "thr_123".to_string(),
thread_settings: v2::ThreadSettings {
cwd: absolute_path("/tmp/repo"),
approval_policy: v2::AskForApproval::Never,
approvals_reviewer: v2::ApprovalsReviewer::User,
sandbox_policy: v2::SandboxPolicy::DangerFullAccess,
active_permission_profile: None,
model: "gpt-5.4".to_string(),
model_provider: "openai".to_string(),
service_tier: None,
effort: None,
summary: None,
collaboration_mode: codex_protocol::config_types::CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: "gpt-5.4".to_string(),
reasoning_effort: None,
developer_instructions: None,
},
},
personality: None,
},
});
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&notification),
Some("thread/settings/updated")
);
}
#[test]
fn thread_realtime_started_notification_is_marked_experimental() {
let notification =