diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 1ccf0d55d9..7950fdd7d6 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3107,6 +3107,7 @@ "type": "object" }, "ThreadTurnContextUpdateParams": { + "description": "Parameters for `thread/turnContext/update`. Each field is optional; only supplied fields override the thread's defaults for subsequent turns and thread-scoped operations (e.g. shell commands).", "properties": { "approvalPolicy": { "anyOf": [ 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 1fdb214066..339503ab5a 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 @@ -14191,6 +14191,7 @@ }, "ThreadTurnContextUpdateParams": { "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for `thread/turnContext/update`. Each field is optional; only supplied fields override the thread's defaults for subsequent turns and thread-scoped operations (e.g. shell commands).", "properties": { "approvalPolicy": { "anyOf": [ 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 f1c10868f4..d58f40ae54 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 @@ -12046,6 +12046,7 @@ }, "ThreadTurnContextUpdateParams": { "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for `thread/turnContext/update`. Each field is optional; only supplied fields override the thread's defaults for subsequent turns and thread-scoped operations (e.g. shell commands).", "properties": { "approvalPolicy": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json index 8a3347c6ac..21937d4663 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadTurnContextUpdateParams.json @@ -336,6 +336,7 @@ "type": "object" } }, + "description": "Parameters for `thread/turnContext/update`. Each field is optional; only supplied fields override the thread's defaults for subsequent turns and thread-scoped operations (e.g. shell commands).", "properties": { "approvalPolicy": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts index 24f7cc308d..d1c62a24f9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadTurnContextUpdateParams.ts @@ -10,6 +10,11 @@ import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; +/** + * Parameters for `thread/turnContext/update`. Each field is optional; only + * supplied fields override the thread's defaults for subsequent turns and + * thread-scoped operations (e.g. shell commands). + */ export type ThreadTurnContextUpdateParams = {threadId: string, /** * Override the working directory for subsequent turns and thread-scoped * operations. diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 50835ad41d..6aa9037a84 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2896,6 +2896,9 @@ pub struct ThreadSetNameResponse {} )] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] +/// Parameters for `thread/turnContext/update`. Each field is optional; only +/// supplied fields override the thread's defaults for subsequent turns and +/// thread-scoped operations (e.g. shell commands). pub struct ThreadTurnContextUpdateParams { pub thread_id: String, /// Override the working directory for subsequent turns and thread-scoped diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 33a8f0ae83..b1223b91fc 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -143,7 +143,7 @@ Example with notification opt-out: - `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success and emits `thread/archived`. - `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server shuts down and unloads the thread, then emits `thread/closed`. - `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread. -- `thread/turnContext/update` — update a loaded thread’s defaults for subsequent turns and thread-scoped operations, such as `cwd`, approval policy, approvals reviewer, sandbox policy, model, reasoning effort, service tier, collaboration mode, or personality; returns `{}` on success. +- `thread/turnContext/update` — update the live defaults on a loaded thread for subsequent turns and thread-scoped operations, such as `cwd`, approval policy, approvals reviewer, sandbox policy, model, reasoning effort, service tier, collaboration mode, or personality; returns `{}` on success. - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index b8d5546580..931088c096 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -42,6 +42,7 @@ mod thread_rollback; mod thread_shell_command; mod thread_start; mod thread_status; +mod thread_turn_context_update; mod thread_unarchive; mod thread_unsubscribe; mod turn_interrupt; diff --git a/codex-rs/app-server/tests/suite/v2/thread_turn_context_update.rs b/codex-rs/app-server/tests/suite/v2/thread_turn_context_update.rs new file mode 100644 index 0000000000..fcf7f6bd19 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/thread_turn_context_update.rs @@ -0,0 +1,98 @@ +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::to_response; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadReadParams; +use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::ThreadTurnContextUpdateParams; +use codex_app_server_protocol::ThreadTurnContextUpdateResponse; +use pretty_assertions::assert_eq; +use std::path::Path; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); + +#[tokio::test] +async fn thread_turn_context_update_changes_loaded_thread_cwd() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + let original_workspace = codex_home.path().join("original"); + let updated_workspace = codex_home.path().join("updated"); + std::fs::create_dir(&original_workspace)?; + std::fs::create_dir(&updated_workspace)?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let start_id = mcp + .send_thread_start_request(ThreadStartParams { + cwd: Some(original_workspace.display().to_string()), + ..Default::default() + }) + .await?; + let start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(start_resp)?; + + let update_id = mcp + .send_thread_turn_context_update_request(ThreadTurnContextUpdateParams { + thread_id: thread.id.clone(), + cwd: Some(updated_workspace.clone()), + ..Default::default() + }) + .await?; + let update_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(update_id)), + ) + .await??; + let _: ThreadTurnContextUpdateResponse = + to_response::(update_resp)?; + + let read_id = mcp + .send_thread_read_request(ThreadReadParams { + thread_id: thread.id, + include_turns: false, + }) + .await?; + let read_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_id)), + ) + .await??; + let ThreadReadResponse { thread: read } = to_response::(read_resp)?; + + assert_eq!(read.cwd, updated_workspace); + Ok(()) +} + +fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { + std::fs::write( + codex_home.join("config.toml"), + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" + +model_provider = "mock_provider" + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{server_uri}/v1" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 +"# + ), + ) +}