diff --git a/.gitignore b/.gitignore index 752ceb34..1d9aebfd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,24 @@ docs/superpowers/ *.log .DS_Store Thumbs.db + +# Local development artifacts +backups/ +bin/ +.codex-merge/ +.codex-tmp-gh/ +.codex-upstream/ +psforge_debug.log +apps/codex-plus-manager/codex-plus-plus.exe + +# Local-only files (not for upstream PR) +AGENT.md +README_SYNC_FIX.md +README_AGGREGATE_KERNEL_VERIFY.md +build-to-bin.bat +build-to-bin.ps1 +reapply-local-mods.ps1 +update-and-build.bat +update-and-build.ps1 +scripts/aggregate-kernel-smoke.ps1 +scripts/installer/windows/test-update-script.ps1 diff --git a/Cargo.lock b/Cargo.lock index 73fd1fbc..919512e6 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index 46b6f1de..0b07f824 100644 Binary files a/Cargo.toml and b/Cargo.toml differ diff --git a/apps/codex-plus-launcher/src/main.rs b/apps/codex-plus-launcher/src/main.rs index 961b2c79..87795830 100644 Binary files a/apps/codex-plus-launcher/src/main.rs and b/apps/codex-plus-launcher/src/main.rs differ diff --git a/apps/codex-plus-manager/package-lock.json b/apps/codex-plus-manager/package-lock.json index 44afe3be..a4d5338e 100644 Binary files a/apps/codex-plus-manager/package-lock.json and b/apps/codex-plus-manager/package-lock.json differ diff --git a/apps/codex-plus-manager/package.json b/apps/codex-plus-manager/package.json index 39a99eb4..8d42bdea 100644 Binary files a/apps/codex-plus-manager/package.json and b/apps/codex-plus-manager/package.json differ diff --git a/apps/codex-plus-manager/src-tauri/src/commands.rs b/apps/codex-plus-manager/src-tauri/src/commands.rs index d27d334c..d0ae8eea 100644 Binary files a/apps/codex-plus-manager/src-tauri/src/commands.rs and b/apps/codex-plus-manager/src-tauri/src/commands.rs differ diff --git a/apps/codex-plus-manager/src-tauri/src/lib.rs b/apps/codex-plus-manager/src-tauri/src/lib.rs index ffd36b06..cc8f7d3b 100644 Binary files a/apps/codex-plus-manager/src-tauri/src/lib.rs and b/apps/codex-plus-manager/src-tauri/src/lib.rs differ diff --git a/apps/codex-plus-manager/src-tauri/tauri.conf.json b/apps/codex-plus-manager/src-tauri/tauri.conf.json index 49944e2e..6eea48a0 100644 Binary files a/apps/codex-plus-manager/src-tauri/tauri.conf.json and b/apps/codex-plus-manager/src-tauri/tauri.conf.json differ diff --git a/apps/codex-plus-manager/src/App.tsx b/apps/codex-plus-manager/src/App.tsx index 4b250148..933742b0 100644 Binary files a/apps/codex-plus-manager/src/App.tsx and b/apps/codex-plus-manager/src/App.tsx differ diff --git a/apps/codex-plus-manager/src/aggregateMappings.ts b/apps/codex-plus-manager/src/aggregateMappings.ts new file mode 100644 index 00000000..6d840336 Binary files /dev/null and b/apps/codex-plus-manager/src/aggregateMappings.ts differ diff --git a/apps/codex-plus-manager/src/styles.css b/apps/codex-plus-manager/src/styles.css index a40112a1..9900e474 100644 Binary files a/apps/codex-plus-manager/src/styles.css and b/apps/codex-plus-manager/src/styles.css differ diff --git a/apps/codex-plus-mobile-relay/Cargo.toml b/apps/codex-plus-mobile-relay/Cargo.toml deleted file mode 100644 index 998b3a58..00000000 --- a/apps/codex-plus-mobile-relay/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "codex-plus-mobile-relay" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true - -[[bin]] -name = "codex-plus-mobile-relay" -path = "src/main.rs" - -[dependencies] -anyhow.workspace = true -futures-util = { workspace = true, features = ["sink"] } -serde.workspace = true -serde_json.workspace = true -tokio = { workspace = true, features = ["net", "signal", "sync"] } -tokio-tungstenite.workspace = true diff --git a/apps/codex-plus-mobile-relay/src/main.rs b/apps/codex-plus-mobile-relay/src/main.rs deleted file mode 100644 index 73972c98..00000000 --- a/apps/codex-plus-mobile-relay/src/main.rs +++ /dev/null @@ -1,1283 +0,0 @@ -use std::collections::HashMap; -use std::env; -use std::net::SocketAddr; -use std::str; -use std::sync::Arc; -use std::sync::Mutex as StdMutex; -use std::time::{Duration, Instant}; - -use anyhow::{Context, bail}; -use futures_util::{SinkExt, StreamExt}; -use serde::{Deserialize, Serialize}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::{Mutex, mpsc}; -use tokio_tungstenite::accept_hdr_async; -use tokio_tungstenite::tungstenite::Message; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Role { - Host, - Client, -} - -impl Role { - fn from_str(value: &str) -> Option { - match value { - "host" => Some(Self::Host), - "client" => Some(Self::Client), - _ => None, - } - } - - fn as_str(self) -> &'static str { - match self { - Self::Host => "host", - Self::Client => "client", - } - } -} - -#[derive(Debug, Clone)] -struct Registration { - role: Role, - room: String, - token: String, -} - -#[derive(Debug, Deserialize, Serialize)] -struct RegisterMessage { - #[serde(rename = "type")] - message_type: String, - role: String, - room: String, - token: String, -} - -#[derive(Default)] -struct RelayState { - rooms: HashMap, - started_at: Option, - total_connections: u64, - active_connections: u64, - forwarded_messages: u64, - forwarded_bytes: u64, -} - -struct RoomState { - token: String, - host: Option>, - client: Option>, - connected_at: Instant, - forwarded_messages: u64, - forwarded_bytes: u64, -} - -impl RoomState { - fn new(token: String) -> Self { - Self { - token, - host: None, - client: None, - connected_at: Instant::now(), - forwarded_messages: 0, - forwarded_bytes: 0, - } - } - - fn sender_for(&self, role: Role) -> Option> { - match role { - Role::Host => self.host.clone(), - Role::Client => self.client.clone(), - } - } - - fn set_sender(&mut self, role: Role, sender: mpsc::UnboundedSender) { - let slot = match role { - Role::Host => &mut self.host, - Role::Client => &mut self.client, - }; - if let Some(previous) = slot.replace(sender) { - let _ = previous.send(Message::Close(None)); - } - } - - fn clear_sender(&mut self, role: Role) { - match role { - Role::Host => self.host = None, - Role::Client => self.client = None, - } - } - - fn is_empty(&self) -> bool { - self.host.is_none() && self.client.is_none() - } -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct RelayStatus { - status: &'static str, - service: &'static str, - version: &'static str, - uptime_seconds: u64, - rooms: usize, - active_connections: u64, - total_connections: u64, - forwarded_messages: u64, - forwarded_bytes: u64, - room_details: Vec, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct RoomStatus { - room: String, - host_online: bool, - client_online: bool, - connections: u8, - age_seconds: u64, - forwarded_messages: u64, - forwarded_bytes: u64, -} - -#[derive(Clone)] -struct RegisteredPeer { - room: String, - role: Role, - sender: mpsc::UnboundedSender, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let bind = env::var("CODEX_PLUS_MOBILE_RELAY_BIND") - .ok() - .filter(|value| !value.trim().is_empty()) - .unwrap_or_else(|| "0.0.0.0:57323".to_string()); - let listener = TcpListener::bind(&bind) - .await - .with_context(|| format!("failed to bind mobile relay server on {bind}"))?; - let local_addr = listener.local_addr()?; - println!("Codex++ mobile relay listening on ws://{local_addr}"); - println!( - "Clients must send first message: {{\"type\":\"register\",\"role\":\"host|client\",\"room\":\"...\",\"token\":\"...\"}}" - ); - - let state = Arc::new(Mutex::new(RelayState { - started_at: Some(Instant::now()), - ..RelayState::default() - })); - loop { - tokio::select! { - accepted = listener.accept() => { - let (stream, addr) = accepted?; - let state = Arc::clone(&state); - tokio::spawn(async move { - if let Err(error) = handle_tcp_connection(stream, addr, state).await { - eprintln!("relay connection {addr} closed: {error:#}"); - } - }); - } - signal = tokio::signal::ctrl_c() => { - signal.context("failed to wait for Ctrl+C")?; - break; - } - } - } - Ok(()) -} - -async fn handle_tcp_connection( - stream: TcpStream, - addr: SocketAddr, - state: Arc>, -) -> anyhow::Result<()> { - if !looks_like_websocket(&stream).await? { - return handle_http_connection(stream, state).await; - } - handle_websocket_connection(stream, addr, state).await -} - -async fn handle_websocket_connection( - stream: TcpStream, - addr: SocketAddr, - state: Arc>, -) -> anyhow::Result<()> { - let url_registration = Arc::new(StdMutex::new(None::)); - let callback_registration = Arc::clone(&url_registration); - let websocket = accept_hdr_async( - stream, - move |request: &tokio_tungstenite::tungstenite::handshake::server::Request, response| { - if let Some(registration) = - registration_from_uri(request.uri().path(), request.uri().query()) - { - if let Ok(mut slot) = callback_registration.lock() { - *slot = Some(registration); - } - } - Ok(response) - }, - ) - .await - .context("failed to accept websocket")?; - let (mut outgoing, mut incoming) = websocket.split(); - - let registration = match url_registration.lock().ok().and_then(|slot| slot.clone()) { - Some(registration) => registration, - None => { - let first = tokio::time::timeout(Duration::from_secs(10), incoming.next()) - .await - .context("registration timed out")? - .transpose() - .context("failed to read registration")? - .context("connection closed before registration")?; - parse_registration(first)? - } - }; - - let (tx, mut rx) = mpsc::unbounded_channel::(); - let peer = register_peer(&state, registration, tx).await?; - let writer = tokio::spawn(async move { - while let Some(message) = rx.recv().await { - if outgoing.send(message).await.is_err() { - break; - } - } - }); - - println!( - "relay registered {} room={} addr={}", - peer.role.as_str(), - peer.room, - addr - ); - - while let Some(message) = incoming.next().await { - let message = message.context("failed to read websocket message")?; - if message.is_close() { - break; - } - forward_message(&state, &peer, message).await; - } - - unregister_peer(&state, &peer).await; - writer.abort(); - println!( - "relay disconnected {} room={} addr={}", - peer.role.as_str(), - peer.room, - addr - ); - Ok(()) -} - -async fn looks_like_websocket(stream: &TcpStream) -> anyhow::Result { - let mut buffer = [0_u8; 2048]; - let read = stream.peek(&mut buffer).await?; - let head = String::from_utf8_lossy(&buffer[..read]).to_ascii_lowercase(); - Ok(head.contains("\r\nupgrade: websocket") || head.contains("\r\nsec-websocket-key:")) -} - -async fn handle_http_connection( - mut stream: TcpStream, - state: Arc>, -) -> anyhow::Result<()> { - let mut buffer = vec![0_u8; 8192]; - let read = stream.read(&mut buffer).await?; - let request = String::from_utf8_lossy(&buffer[..read]); - let request_line = request.lines().next().unwrap_or_default(); - let path = request_line - .split_whitespace() - .nth(1) - .unwrap_or("/") - .split('?') - .next() - .unwrap_or("/"); - let (status, content_type, body) = match path { - "/" | "/index.html" => ( - "200 OK", - "text/html; charset=utf-8", - relay_test_page().into_bytes(), - ), - "/mobile" => ( - "200 OK", - "text/html; charset=utf-8", - mobile_relay_page().into_bytes(), - ), - "/health" => ( - "200 OK", - "application/json; charset=utf-8", - serde_json::json!({ - "status": "ok", - "service": "codex-plus-mobile-relay", - "version": env!("CARGO_PKG_VERSION") - }) - .to_string() - .into_bytes(), - ), - "/status" => ( - "200 OK", - "application/json; charset=utf-8", - serde_json::to_string(&relay_status(&state).await)?.into_bytes(), - ), - _ => ( - "404 Not Found", - "application/json; charset=utf-8", - serde_json::json!({ - "status": "failed", - "message": "not found" - }) - .to_string() - .into_bytes(), - ), - }; - let response = format!( - concat!( - "HTTP/1.1 {}\r\n", - "Content-Type: {}\r\n", - "Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n", - "Pragma: no-cache\r\n", - "Expires: 0\r\n", - "Content-Length: {}\r\n", - "Connection: close\r\n\r\n" - ), - status, - content_type, - body.len() - ); - stream.write_all(response.as_bytes()).await?; - stream.write_all(&body).await?; - stream.shutdown().await?; - Ok(()) -} - -fn parse_registration(message: Message) -> anyhow::Result { - let text = match message { - Message::Text(text) => text.to_string(), - Message::Binary(bytes) => { - String::from_utf8(bytes.to_vec()).context("binary registration must be utf-8 json")? - } - _ => bail!("first message must be registration json"), - }; - let registration: RegisterMessage = - serde_json::from_str(&text).context("registration is not valid json")?; - if registration.message_type != "register" { - bail!("registration type must be register"); - } - if registration.room.trim().is_empty() { - bail!("room is required"); - } - if registration.token.trim().is_empty() { - bail!("token is required"); - } - let role = Role::from_str(®istration.role).context("role must be host or client")?; - Ok(Registration { - role, - room: registration.room, - token: registration.token, - }) -} - -fn registration_from_uri(path: &str, query: Option<&str>) -> Option { - let query = query?; - let role = match path { - "/host" => Some(Role::Host), - "/client" => Some(Role::Client), - "/ws" => query_value(query, "role").and_then(|role| Role::from_str(&role)), - _ => None, - }?; - let room = query_value(query, "room")?; - let token = query_value(query, "token")?; - if room.trim().is_empty() || token.trim().is_empty() { - return None; - } - Some(Registration { role, room, token }) -} - -fn query_value(query: &str, key: &str) -> Option { - query.split('&').find_map(|pair| { - let (name, value) = pair.split_once('=')?; - (name == key).then(|| percent_decode(value)) - }) -} - -fn percent_decode(value: &str) -> String { - let mut output = Vec::with_capacity(value.len()); - let bytes = value.as_bytes(); - let mut index = 0; - while index < bytes.len() { - match bytes[index] { - b'+' => { - output.push(b' '); - index += 1; - } - b'%' if index + 2 < bytes.len() => { - let hex = &value[index + 1..index + 3]; - if let Ok(byte) = u8::from_str_radix(hex, 16) { - output.push(byte); - index += 3; - } else { - output.push(bytes[index]); - index += 1; - } - } - byte => { - output.push(byte); - index += 1; - } - } - } - String::from_utf8_lossy(&output).to_string() -} - -async fn register_peer( - state: &Arc>, - registration: Registration, - sender: mpsc::UnboundedSender, -) -> anyhow::Result { - let mut state = state.lock().await; - state.total_connections = state.total_connections.saturating_add(1); - state.active_connections = state.active_connections.saturating_add(1); - let room = state - .rooms - .entry(registration.room.clone()) - .or_insert_with(|| RoomState::new(registration.token.clone())); - if room.token != registration.token { - bail!("room token mismatch"); - } - room.set_sender(registration.role, sender.clone()); - let _ = sender.send(Message::Text( - serde_json::json!({ - "type": "registered", - "role": registration.role.as_str(), - "room": registration.room - }) - .to_string() - .into(), - )); - Ok(RegisteredPeer { - room: registration.room, - role: registration.role, - sender, - }) -} - -async fn forward_message(state: &Arc>, peer: &RegisteredPeer, message: Message) { - let message_bytes = message_len(&message); - let target = { - let mut state = state.lock().await; - state.forwarded_messages = state.forwarded_messages.saturating_add(1); - state.forwarded_bytes = state.forwarded_bytes.saturating_add(message_bytes); - let Some(room) = state.rooms.get_mut(&peer.room) else { - return; - }; - room.forwarded_messages = room.forwarded_messages.saturating_add(1); - room.forwarded_bytes = room.forwarded_bytes.saturating_add(message_bytes); - let target_role = match peer.role { - Role::Host => Role::Client, - Role::Client => Role::Host, - }; - room.sender_for(target_role) - }; - if let Some(target) = target { - let _ = target.send(message); - } -} - -async fn unregister_peer(state: &Arc>, peer: &RegisteredPeer) { - let mut state = state.lock().await; - state.active_connections = state.active_connections.saturating_sub(1); - let Some(room) = state.rooms.get_mut(&peer.room) else { - return; - }; - let still_same_sender = room - .sender_for(peer.role) - .as_ref() - .map(|sender| sender.same_channel(&peer.sender)) - .unwrap_or(false); - if still_same_sender { - room.clear_sender(peer.role); - } - if room.is_empty() { - state.rooms.remove(&peer.room); - } -} - -fn message_len(message: &Message) -> u64 { - match message { - Message::Text(text) => text.len() as u64, - Message::Binary(bytes) => bytes.len() as u64, - Message::Ping(bytes) | Message::Pong(bytes) => bytes.len() as u64, - Message::Close(_) | Message::Frame(_) => 0, - } -} - -async fn relay_status(state: &Arc>) -> RelayStatus { - let state = state.lock().await; - let now = Instant::now(); - let mut room_details = state - .rooms - .iter() - .map(|(room, detail)| { - let host_online = detail.host.is_some(); - let client_online = detail.client.is_some(); - RoomStatus { - room: room.clone(), - host_online, - client_online, - connections: u8::from(host_online) + u8::from(client_online), - age_seconds: now.saturating_duration_since(detail.connected_at).as_secs(), - forwarded_messages: detail.forwarded_messages, - forwarded_bytes: detail.forwarded_bytes, - } - }) - .collect::>(); - room_details.sort_by(|left, right| left.room.cmp(&right.room)); - RelayStatus { - status: "ok", - service: "codex-plus-mobile-relay", - version: env!("CARGO_PKG_VERSION"), - uptime_seconds: state - .started_at - .map(|started| now.saturating_duration_since(started).as_secs()) - .unwrap_or_default(), - rooms: state.rooms.len(), - active_connections: state.active_connections, - total_connections: state.total_connections, - forwarded_messages: state.forwarded_messages, - forwarded_bytes: state.forwarded_bytes, - room_details, - } -} - -fn relay_test_page() -> String { - r#" - - - - - Codex++ Mobile Relay - - - -
-

Codex++ Mobile Relay

-
-
- - - -
-
- - -
-
-
- -
- - - -
-
-
-

-  
-
- - -"# - .to_string() -} - -fn mobile_relay_page() -> String { - r#" - - - - - Codex++ 手机控制 - - - -
-
Codex++待连接
-
-
-
-
- - - -
-
- -
连接后读取会话
-
- -
-
- - -"# - .to_string() -} diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 49c5a98c..48f3c396 100644 Binary files a/assets/inject/renderer-inject.js and b/assets/inject/renderer-inject.js differ diff --git a/crates/codex-plus-core/Cargo.toml b/crates/codex-plus-core/Cargo.toml index 1fe7bc53..64b92cfa 100644 Binary files a/crates/codex-plus-core/Cargo.toml and b/crates/codex-plus-core/Cargo.toml differ diff --git a/crates/codex-plus-core/src/aggregate_model_alias.rs b/crates/codex-plus-core/src/aggregate_model_alias.rs new file mode 100644 index 00000000..1d5a1852 Binary files /dev/null and b/crates/codex-plus-core/src/aggregate_model_alias.rs differ diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index a87ab5a6..e0b8d9f9 100644 Binary files a/crates/codex-plus-core/src/assets.rs and b/crates/codex-plus-core/src/assets.rs differ diff --git a/crates/codex-plus-core/src/ccs_import.rs b/crates/codex-plus-core/src/ccs_import.rs index dedca227..74630ea1 100644 Binary files a/crates/codex-plus-core/src/ccs_import.rs and b/crates/codex-plus-core/src/ccs_import.rs differ diff --git a/crates/codex-plus-core/src/install/macos.rs b/crates/codex-plus-core/src/install/macos.rs index 65dff410..b072f112 100644 Binary files a/crates/codex-plus-core/src/install/macos.rs and b/crates/codex-plus-core/src/install/macos.rs differ diff --git a/crates/codex-plus-core/src/install/mod.rs b/crates/codex-plus-core/src/install/mod.rs index 2f105edd..98a493c4 100644 Binary files a/crates/codex-plus-core/src/install/mod.rs and b/crates/codex-plus-core/src/install/mod.rs differ diff --git a/crates/codex-plus-core/src/launcher.rs b/crates/codex-plus-core/src/launcher.rs index e4618115..f4a8c3e4 100644 Binary files a/crates/codex-plus-core/src/launcher.rs and b/crates/codex-plus-core/src/launcher.rs differ diff --git a/crates/codex-plus-core/src/lib.rs b/crates/codex-plus-core/src/lib.rs index f0341819..c537e5b6 100644 Binary files a/crates/codex-plus-core/src/lib.rs and b/crates/codex-plus-core/src/lib.rs differ diff --git a/crates/codex-plus-core/src/model_catalog.rs b/crates/codex-plus-core/src/model_catalog.rs index b0723a06..3655af1d 100644 Binary files a/crates/codex-plus-core/src/model_catalog.rs and b/crates/codex-plus-core/src/model_catalog.rs differ diff --git a/crates/codex-plus-core/src/protocol_proxy.rs b/crates/codex-plus-core/src/protocol_proxy.rs index 94178394..7de9cb99 100644 Binary files a/crates/codex-plus-core/src/protocol_proxy.rs and b/crates/codex-plus-core/src/protocol_proxy.rs differ diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs index 0c0b606a..6147c24d 100644 Binary files a/crates/codex-plus-core/src/relay_config.rs and b/crates/codex-plus-core/src/relay_config.rs differ diff --git a/crates/codex-plus-core/src/relay_rotation.rs b/crates/codex-plus-core/src/relay_rotation.rs index 3dbc98d4..4518267d 100644 Binary files a/crates/codex-plus-core/src/relay_rotation.rs and b/crates/codex-plus-core/src/relay_rotation.rs differ diff --git a/crates/codex-plus-core/src/settings.rs b/crates/codex-plus-core/src/settings.rs index feddeb9d..734bd6df 100644 Binary files a/crates/codex-plus-core/src/settings.rs and b/crates/codex-plus-core/src/settings.rs differ diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index cae9f900..0733ad8f 100644 Binary files a/crates/codex-plus-core/tests/cdp_bridge.rs and b/crates/codex-plus-core/tests/cdp_bridge.rs differ diff --git a/crates/codex-plus-core/tests/installers.rs b/crates/codex-plus-core/tests/installers.rs index 7cd4e79a..a5fa9833 100644 Binary files a/crates/codex-plus-core/tests/installers.rs and b/crates/codex-plus-core/tests/installers.rs differ diff --git a/crates/codex-plus-core/tests/launcher.rs b/crates/codex-plus-core/tests/launcher.rs index ac253966..72da9cf3 100644 Binary files a/crates/codex-plus-core/tests/launcher.rs and b/crates/codex-plus-core/tests/launcher.rs differ diff --git a/crates/codex-plus-core/tests/model_catalog.rs b/crates/codex-plus-core/tests/model_catalog.rs index cf5b1589..d222a26e 100644 Binary files a/crates/codex-plus-core/tests/model_catalog.rs and b/crates/codex-plus-core/tests/model_catalog.rs differ diff --git a/crates/codex-plus-core/tests/protocol_proxy.rs b/crates/codex-plus-core/tests/protocol_proxy.rs index 795796ca..52dac5a6 100644 Binary files a/crates/codex-plus-core/tests/protocol_proxy.rs and b/crates/codex-plus-core/tests/protocol_proxy.rs differ diff --git a/crates/codex-plus-core/tests/relay_rotation.rs b/crates/codex-plus-core/tests/relay_rotation.rs index 1c3904db..e09dd5f9 100644 Binary files a/crates/codex-plus-core/tests/relay_rotation.rs and b/crates/codex-plus-core/tests/relay_rotation.rs differ diff --git a/crates/codex-plus-core/tests/relay_switch.rs b/crates/codex-plus-core/tests/relay_switch.rs index 4ec2f7d6..fef56f1f 100644 Binary files a/crates/codex-plus-core/tests/relay_switch.rs and b/crates/codex-plus-core/tests/relay_switch.rs differ