mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
2b4898cc47
commit
771a4e74ac
27 files changed
+2092
-159
No files matched your search
@@ -3,6 +3,7 @@ use crate::outgoing_message::ConnectionRequestId;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadGoal;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::ThreadSettings;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
use codex_core::CodexThread;
|
||||
@@ -76,6 +77,7 @@ pub(crate) struct ThreadState {
|
||||
pub(crate) cancel_tx: Option<oneshot::Sender<()>>,
|
||||
pub(crate) experimental_raw_events: bool,
|
||||
pub(crate) listener_generation: u64,
|
||||
last_thread_settings: Option<ThreadSettings>,
|
||||
listener_command_tx: Option<mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||
current_turn_history: ThreadHistoryBuilder,
|
||||
listener_thread: Option<Weak<CodexThread>>,
|
||||
@@ -95,11 +97,13 @@ impl ThreadState {
|
||||
cancel_tx: oneshot::Sender<()>,
|
||||
conversation: &Arc<CodexThread>,
|
||||
watch_registration: WatchRegistration,
|
||||
thread_settings_baseline: ThreadSettings,
|
||||
) -> (mpsc::UnboundedReceiver<ThreadListenerCommand>, u64) {
|
||||
if let Some(previous) = self.cancel_tx.replace(cancel_tx) {
|
||||
let _ = previous.send(());
|
||||
}
|
||||
self.listener_generation = self.listener_generation.wrapping_add(1);
|
||||
self.last_thread_settings = Some(thread_settings_baseline);
|
||||
let (listener_command_tx, listener_command_rx) = mpsc::unbounded_channel();
|
||||
self.listener_command_tx = Some(listener_command_tx);
|
||||
self.listener_thread = Some(Arc::downgrade(conversation));
|
||||
@@ -143,6 +147,12 @@ impl ThreadState {
|
||||
self.current_turn_history.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn note_thread_settings(&mut self, thread_settings: ThreadSettings) -> bool {
|
||||
let changed = self.last_thread_settings.as_ref() != Some(&thread_settings);
|
||||
self.last_thread_settings = Some(thread_settings);
|
||||
changed
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_server_request_on_thread_listener(
|
||||
@@ -177,6 +187,60 @@ pub(crate) async fn resolve_server_request_on_thread_listener(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ApprovalsReviewer;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn note_thread_settings_reports_only_effective_changes() {
|
||||
let mut state = ThreadState::default();
|
||||
let initial = thread_settings("mock-model");
|
||||
let updated = thread_settings("mock-model-2");
|
||||
|
||||
let results = vec![
|
||||
state.note_thread_settings(initial.clone()),
|
||||
state.note_thread_settings(initial),
|
||||
state.note_thread_settings(updated.clone()),
|
||||
state.note_thread_settings(updated),
|
||||
];
|
||||
|
||||
assert_eq!(results, vec![true, false, true, false]);
|
||||
}
|
||||
|
||||
fn thread_settings(model: &str) -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
cwd: AbsolutePathBuf::from_absolute_path("/tmp").expect("absolute path"),
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
sandbox_policy: SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
},
|
||||
active_permission_profile: None,
|
||||
model: model.to_string(),
|
||||
model_provider: "mock_provider".to_string(),
|
||||
service_tier: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
collaboration_mode: CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model: model.to_string(),
|
||||
reasoning_effort: None,
|
||||
developer_instructions: None,
|
||||
},
|
||||
},
|
||||
personality: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThreadEntry {
|
||||
state: Arc<Mutex<ThreadState>>,
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
|
||||
Reference in new issue
Block a user