App-server 架构
app-server 是 codex 的 JSON-RPC 服务端,把 codex-core 的 ThreadManager、AuthManager、ConfigManager 包成一个可被 IDE、Web、TUI 共用的进程。它对外暴露 stdio / unix socket / websocket 三种 transport,对内用 MessageProcessor 做 request 路由,所有 RPC 走同一套 ClientRequest / ServerNotification 协议。
职责
- 入口
run_main_with_transport_options装载 config、auth、otel、state_db,启动 transport acceptor 与 outbound router,最后 spawnMessageProcessor进入主 select 循环 (codex-rs/app-server/src/lib.rs:449-460)。 - Transport 层把
stdio:///unix:///ws://IP:PORT/off统一成TransportEvent(ConnectionOpened / ConnectionClosed / IncomingMessage),所有连接共享一个 event channel (codex-rs/app-server-transport/src/transport/mod.rs:73-78)。 MessageProcessor::process_request反序列化JSONRPCRequest成ClientRequest,按Initialize/ 已初始化分支派发,已初始化的请求经dispatch_initialized_client_request入对应 processor (codex-rs/app-server/src/message_processor.rs:518-570)。- 每条已初始化请求按
serialization_scope排队或 spawn:RequestSerializationQueues保证同 scope 请求串行(比如同 thread 的 turn 操作),跨 scope 才并发 (codex-rs/app-server/src/message_processor.rs:846-861)。 - 出站走
OutgoingEnvelope::ToConnection/Broadcast,广播只发已initialized的连接;experimental 通知只发开了experimental_api的连接 (codex-rs/app-server/src/transport.rs:198-237)。
设计动机
app-server 之所以独立成 crate,是因为 IDE(VSCode、JetBrains)、TUI、远程配对 app 共用同一套协议但运行环境不同:IDE 跑 stdio,远程配对走 websocket,daemon 模式需要 unix socket + pid 文件。把 transport 抽到 app-server-transport crate,协议抽到 app-server-protocol crate,server crate 只关心"如何把 JSON-RPC 路由到 ThreadManager",这样加新 transport 不用动主逻辑。
ConnectionSessionState 用 OnceLock<InitializedConnectionSessionState> 而不是 Mutex,因为 initialize 是一次性操作——一旦设置不可改,后续请求只读。这避免了"已初始化连接"用锁的开销,experimental_api_enabled / opted_out_notification_methods 都直接 atomic / RwLock 读。
请求路由用 processor struct 集合而不是单个 match:每个 processor(ThreadRequestProcessor、TurnRequestProcessor、ConfigRequestProcessor 等)持有自己的 state,handle_initialized_client_request 只做 match 派发。这样加新方法不用动中心代码,processor 内部测试也好写。
serialization_scope 是个有意思的设计:同 thread 的 turn 操作必须串行(否则两个 Op::UserInput 会乱序),但不同 thread 的请求可以并发。RequestSerializationQueues 按 (connection_id, scope) 分 key,同 key 请求排队,跨 key 直接 tokio::spawn。
outbound 路由的 try_send + 队列满就 disconnect 是防止慢客户端拖死 server:websocket 慢连接队列满后直接 kick,stdio/stdin 没有断流保护才用 send().await。
关键文件
codex-rs/app-server/src/main.rs:19-60 — AppServerArgs 命令行,--listen 解析 transport,--session-source 区分 vscode / cli / mcp。codex-rs/app-server/src/lib.rs:449-540 — run_main_with_transport_options,装载 config、auth、otel 的入口。codex-rs/app-server/src/lib.rs:849-990 — MessageProcessor spawn 与主 select 循环,处理 transport event 与 outbound envelope。codex-rs/app-server/src/message_processor.rs:224-302 — MessageProcessor::new,创建 ThreadManager、ThreadStateManager、SkillsWatcher、各 processor。codex-rs/app-server/src/message_processor.rs:799-862 — dispatch_initialized_client_request,serialization scope 派发逻辑。codex-rs/app-server/src/message_processor.rs:864-1000 — handle_initialized_client_request 大 match,把 ClientRequest 路由到各 processor。codex-rs/app-server/src/transport.rs:134-172 — send_message_to_connection,慢连接队列满后 disconnect。codex-rs/app-server-transport/src/transport/mod.rs:73-158 — AppServerTransport 枚举与 from_listen_url 解析。codex-rs/app-server-protocol/src/rpc.rs:34-88 — JSONRPCMessage / JSONRPCRequest / JSONRPCError,协议基础类型(注释明确说不做真 JSON-RPC 2.0,不要求 jsonrpc 字段)。codex-rs/app-server/src/request_processors/initialize_processor.rs:44-101 — InitializeRequestProcessor::initialize,设置 connection 的 experimental_api、attestation、originator 等全局身份。主 select 是 transport event 与 outbound envelope 二选一,加 shutdown 信号守卫。transport 分支把 IncomingMessage 交给 processor.process_request,outbound 分支调 route_outgoing_envelope 路由:
// app-server/src/lib.rs:877-892 — 主循环 shutdown 判定
let exit_reason = loop {
let running_turn_count = { *running_turn_count_rx.borrow() };
if matches!(
shutdown_state.update(running_turn_count, connections.len()),
ShutdownAction::Finish
) {
transport_shutdown_token.cancel();
let _ = outbound_control_tx
.send(OutboundControlEvent::DisconnectAll)
.await;
break "shutdown_requested";
}
tokio::select! { /* ... */ }
};dispatch_initialized_client_request 把请求转成 QueuedInitializedRequest 再决定排队或 spawn。同 scope 必须串行,跨 scope 才并发——比如两个 connection 各自发 turn 不会互相阻塞:
// message_processor.rs:851-860 — scope 排队 vs 直接 spawn
if let Some(scope) = serialization_scope {
let (key, access) = RequestSerializationQueueKey::from_scope(connection_id, scope);
self.request_serialization_queues
.enqueue(key, access, request)
.await;
} else {
tokio::spawn(async move { request.run().await; });
}route_outgoing_envelope 区分定向发送与广播:广播时遍历所有已 initialized 连接,但 experimental notification 只发开了 experimental_api 的连接,opted_out_notification_methods 让客户端可以按方法名退订:
// app-server/src/transport.rs:212-236 — Broadcast 路由
OutgoingEnvelope::Broadcast { message } => {
let target_connections: Vec<ConnectionId> = connections
.iter()
.filter_map(|(connection_id, connection_state)| {
if connection_state.initialized.load(Ordering::Acquire)
&& !should_skip_notification_for_connection(connection_state, &message)
{
Some(*connection_id)
} else {
None
}
})
.collect();
// ...
}数据流
边界与失败
- 慢连接被踢:websocket 连接 outbound 队列满后
try_send返回Full,直接disconnect_connection,防止 server 被慢客户端拖死;stdio 没有 disconnect_sender 时才send().await阻塞 (codex-rs/app-server/src/transport.rs:154-172)。 - 未初始化连接只接 Initialize:
dispatch_initialized_client_request第一行检查session.initialized(),未初始化直接invalid_request("Not initialized")(codex-rs/app-server/src/message_processor.rs:806-808)。 - experimental 请求被 gate:带
experimental_reason()的请求在未开 experimental_api 的连接上被拒,返回experimental_required_message(codex-rs/app-server/src/message_processor.rs:810-814)。 - shutdown 等 turn 收尾:
ShutdownState::update看running_turn_count与connections.len(),只有两者都为 0 才Finish;带超时避免死等 (codex-rs/app-server/src/lib.rs:877-892)。 - transport
off也合法:不启动任何 acceptor,只跑 remote control,这是 headless 场景下用 daemon 控制远程 codex 的路径 (codex-rs/app-server/src/lib.rs:712-740)。
小结
app-server 是 codex 的协议层:transport 多形态、processor 各管一摊、serialization scope 保证同 thread 串行。ConnectionSessionState 用 OnceLock 表达"初始化一次即不可变"的语义。要继续往下看,远程配对、daemon 生命周期在 app-server-daemon crate,客户端封装在 app-server-client;MCP 工具调用如何经此进 core 见 MCP 集成。