V2实时与远程审查协议
本文承接V2 Turn与Item协议和V2 FSCommandProcess协议。实时会话、远程控制和代码审查都依赖长生命周期连接,但它们的 owner 不同:realtime 负责音频、文本、转写和会话事件,remote control 负责服务开关、配对和客户端管理,review 则把目标规范化为一个独立的 review turn。本文沿协议类型、processor 和测试追踪三条链,解释“连接已建立”“请求已接受”“审查已完成”为什么是三个不同状态。
1. 实时会话类型
源码位置:codex-rs/app-server-protocol/src/protocol/v2/realtime.rs :: ThreadRealtimeStartParams、ThreadRealtimeItem、ThreadRealtimeSessionOutcome
pub struct ThreadRealtimeStartParams {
pub thread_id: String,
pub client_managed_handoffs: Option<bool>,
pub delegation_ack_filler: Option<bool>,
pub flush_transcript_tail_on_session_end: Option<bool>,
pub codex_responses_as_items: Option<bool>,
pub codex_response_item_prefix: Option<String>,
pub codex_response_handoff_mode: Option<CodexResponseHandoffMode>,
pub model: Option<String>,
pub output_modality: RealtimeOutputModality,
pub include_startup_context: Option<bool>,
pub initial_items: Option<Vec<ThreadRealtimeInitialItem>>,
pub realtime_start_instructions: Option<String>,
pub realtime_end_instructions: Option<String>,
pub prompt: Option<Option<String>>,
pub realtime_session_id: Option<String>,
pub transport: Option<ThreadRealtimeStartTransport>,
pub version: Option<RealtimeConversationVersion>,
pub voice: Option<RealtimeVoice>,
}
pub struct ThreadRealtimeItem {
pub id: String,
pub content: ThreadRealtimeItemContent,
pub presentation: Option<ThreadRealtimeBemItemPresentation>,
}
pub enum ThreadRealtimeSessionOutcome {
Completed,
Interrupted,
Failed,
}实时入口可以携带模型、指令、音频格式、转写、turn detection、输出模态和初始 item;这些是会话启动参数,不是普通 turn/start 的 UserInput。会话内部的 item 还带 presentation,客户端应以 notification 的 item id 维护增量,而不是拼接所有文本事件。
源码位置:codex-rs/app-server-protocol/src/protocol/v2/realtime.rs :: ThreadRealtimeStartedNotification、ThreadRealtimeItemTranscriptDeltaNotification、ThreadRealtimeClosedNotification
pub struct ThreadRealtimeStartedNotification {
pub thread_id: String,
pub realtime_session_id: Option<String>,
pub version: RealtimeConversationVersion,
}
pub struct ThreadRealtimeItemTranscriptDeltaNotification {
pub thread_id: String,
pub item_id: String,
pub delta: String,
}
pub struct ThreadRealtimeClosedNotification {
pub thread_id: String,
pub reason: Option<String>,
}started、item transcript delta 和 closed 构成会话生命周期;音频 delta、transcript done、SDP 和 error 是并行事件,不保证与普通 Turn item 一一对应。
2. realtime请求处理
源码位置:codex-rs/app-server/src/request_processors/turn_processor.rs :: thread_realtime_start、thread_realtime_start_inner、thread_realtime_append_audio_inner、thread_realtime_stop_inner
pub(crate) async fn thread_realtime_start(
&self,
request_id: &ConnectionRequestId,
params: ThreadRealtimeStartParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_realtime_start_inner(request_id, params)
.await
.map(|response| response.map(Into::into))
}
pub(crate) async fn thread_realtime_append_audio(
&self,
request_id: &ConnectionRequestId,
params: ThreadRealtimeAppendAudioParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_realtime_append_audio_inner(request_id, params)
.await
.map(|response| response.map(Into::into))
}processor 只负责请求校验、线程加载和会话 owner 调用;音频、文本、speech append 的 response 可以为空,实际结果通过 realtime notification 返回。thread/realtime/stop 结束的是实时会话,不等于中断历史 Turn;进入和退出 realtime 时,processor 还会处理 session 指令和历史 transcript 的封存。
3. 远程控制类型
源码位置:codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs :: RemoteControlEnableParams、RemoteControlStatusReadResponse、RemoteControlPairingStartResponse、RemoteControlClient
pub struct RemoteControlEnableParams {
pub ephemeral: bool,
}
pub struct RemoteControlStatusReadResponse {
pub status: RemoteControlConnectionStatus,
pub server_name: String,
pub installation_id: String,
pub environment_id: Option<String>,
}
pub struct RemoteControlPairingStartResponse {
pub pairing_code: String,
pub manual_pairing_code: Option<String>,
pub environment_id: String,
pub expires_at: i64,
}
pub struct RemoteControlClient {
pub client_id: String,
pub display_name: Option<String>,
pub device_type: Option<String>,
pub platform: Option<String>,
pub os_version: Option<String>,
pub device_model: Option<String>,
pub app_version: Option<String>,
pub last_seen_at: Option<i64>,
}远程控制协议把 enable/status、pairing 和 client management 分开。配对码有过期时间,客户端列表是已登记身份,不能据此推断当前是否有活动 websocket;连接状态由 RemoteControlConnectionStatus 单独表达。
源码位置:codex-rs/app-server/src/request_processors/remote_control_processor.rs :: enable、disable、status_read、pairing_start、clients_list、clients_revoke
pub(crate) async fn enable(
&self,
ephemeral: bool,
) -> Result<RemoteControlEnableResponse, JSONRPCErrorError> {
let handle = self.handle()?;
let status = if ephemeral {
handle.enable_ephemeral().map_err(map_enable_error)?
} else {
handle.enable(None).await.map_err(map_update_error)?
};
Ok(RemoteControlEnableResponse::from(status))
}
pub(crate) fn status_read(
&self,
) -> Result<RemoteControlStatusReadResponse, JSONRPCErrorError> {
let status = self.handle()?.status();
Ok(RemoteControlStatusReadResponse {
status: status.status,
server_name: status.server_name,
installation_id: status.installation_id,
environment_id: status.environment_id,
})
}processor 通过可选的 RemoteControlHandle 访问 transport owner;handle 不存在时返回 unavailable,而不是伪造 disabled 状态。enable 的 requirements 拒绝、网络不可用和已启用冲突由 map_enable_error 映射为不同的 JSON-RPC 错误。
4. 配对与客户端管理
源码位置:codex-rs/app-server/src/request_processors/remote_control_processor.rs :: map_enable_error、pairing_status、clients_revoke
fn map_enable_error(err: RemoteControlEnableError) -> JSONRPCErrorError {
match err {
RemoteControlEnableError::Unavailable(err) => map_unavailable(err),
RemoteControlEnableError::DisabledByRequirements(err) => {
invalid_request(err.to_string())
}
}
}
pub(crate) async fn clients_revoke(
&self,
params: RemoteControlClientsRevokeParams,
) -> Result<RemoteControlClientsRevokeResponse, JSONRPCErrorError> {
self.handle()?.revoke_client(params.client_id).await?;
Ok(RemoteControlClientsRevokeResponse {})
}撤销是对已登记客户端身份的删除,未必立即终止已经建立的连接;连接 owner 还要在 transport 层处理断开。配对状态查询同样只描述当前配对流程,不能替代 clients list。
5. Review目标与启动
源码位置:codex-rs/app-server-protocol/src/protocol/v2/review.rs :: ReviewStartParams、ReviewTarget、ReviewStartResponse
pub struct ReviewStartParams {
pub thread_id: String,
pub target: ReviewTarget,
pub delivery: Option<ReviewDelivery>,
}
pub enum ReviewTarget {
UncommittedChanges,
BaseBranch { branch: String },
Commit { sha: String, title: Option<String> },
Custom { instructions: String },
}
pub struct ReviewStartResponse {
pub turn: Turn,
pub review_thread_id: String,
}review target 是结构化联合体,不是任意字符串。delivery 决定结果留在原线程还是创建 detached thread;response 返回 review turn 和 item 身份,最终结论仍沿 Turn/item 通知发布。
源码位置:codex-rs/app-server/src/request_processors/turn_processor.rs :: review_request_from_target、review_start、start_inline_review
fn review_request_from_target(
target: ApiReviewTarget,
) -> Result<(ReviewRequest, String, String), JSONRPCErrorError> {
let cleaned_target = match target {
ApiReviewTarget::UncommittedChanges => ApiReviewTarget::UncommittedChanges,
ApiReviewTarget::BaseBranch { branch } => {
let branch = branch.trim().to_string();
if branch.is_empty() {
return Err(invalid_request("branch must not be empty".to_string()));
}
ApiReviewTarget::BaseBranch { branch }
}
ApiReviewTarget::Commit { sha, title } => {
let sha = sha.trim().to_string();
if sha.is_empty() {
return Err(invalid_request("sha must not be empty".to_string()));
}
ApiReviewTarget::Commit { sha, title }
}
ApiReviewTarget::Custom { instructions } => {
let trimmed = instructions.trim().to_string();
if trimmed.is_empty() {
return Err(invalid_request(
"instructions must not be empty".to_string(),
));
}
ApiReviewTarget::Custom {
instructions: trimmed,
}
}
};
// target is converted to the core ReviewRequest after normalization
todo!()
}启动前会 trim 并拒绝空 branch、sha 和 custom instructions,再映射到 Core ReviewRequest。这一步决定审查 prompt 和用户提示;它不是执行阶段的模型输出校验。
6. Review执行边界
源码位置:codex-rs/app-server/src/request_processors/turn_processor.rs :: start_inline_review
async fn start_inline_review(
&self,
request_id: &ConnectionRequestId,
parent_thread: Arc<CodexThread>,
review_request: ReviewRequest,
display_text: &str,
parent_thread_id: String,
) -> std::result::Result<(), JSONRPCErrorError> {
let turn_id = self
.submit_core_op(request_id, parent_thread.as_ref(), Op::Review { review_request })
.await?;
let turn = Self::build_review_turn(turn_id, display_text);
self.emit_review_started(request_id, turn, parent_thread_id)
.await;
Ok(())
}review 使用 Core Op::Review,因此会进入普通 Turn 的审批、sandbox、事件和历史投影;它不是在 App Server 中直接执行 git 命令。detached delivery 还会改变 thread id 和历史 owner,分页 parent、空目标和活动 review 的拒绝由 request processor 负责。
7. 测试与边界
源码位置:codex-rs/app-server/tests/suite/v2/realtime_conversation.rs :: realtime_conversation_streams_v2_notifications、realtime_conversation_stop_emits_closed_notification、realtime_mode_uses_client_instructions_on_entry_and_exit
源码位置:codex-rs/app-server/tests/suite/v2/remote_control.rs :: remote_control_enable_and_disable、remote_control_pairing_start_returns_pairing_artifacts、remote_control_client_management_works_while_disabled
源码位置:codex-rs/app-server/tests/suite/v2/review.rs :: review_start_runs_review_turn_and_emits_code_review_item、review_start_rejects_empty_base_branch、review_start_with_detached_delivery_returns_new_thread_id
cd codex-rs
cargo test -p codex-app-server realtime_conversation_streams_v2_notifications -- --nocapture --test-threads=1
cargo test -p codex-app-server realtime_conversation_stop_emits_closed_notification -- --nocapture --test-threads=1
cargo test -p codex-app-server remote_control_pairing_start_returns_pairing_artifacts -- --nocapture --test-threads=1
cargo test -p codex-app-server remote_control_client_management_works_while_disabled -- --nocapture --test-threads=1
cargo test -p codex-app-server review_start_runs_review_turn_and_emits_code_review_item -- --nocapture --test-threads=1
cargo test -p codex-app-server review_start_rejects_empty_base_branch -- --nocapture --test-threads=1
cargo test -p codex-app-server review_start_with_detached_delivery_returns_new_thread_id -- --nocapture --test-threads=1这些测试分别断言 realtime notification 与关闭事件、远程控制配对和撤销、review 目标校验与 detached thread 行为。它们不能证明真实音频 provider、远程控制公网 transport、所有审查 delivery 组合或跨平台网络故障恢复。
8. 源码定位练习
遇到 realtime 已 started 但没有 transcript,检查 item delta、transcript done 和 session error 是否分开到达;遇到 remote control status 是 disabled,先确认 handle 是否存在,再判断 requirements 是否拒绝 enable;遇到 review response 已返回但没有结论,沿 review turn 的普通 item/turn 事件继续追踪。三条链路都必须把 response、notification 和最终历史分开阅读。
