Skip to content

V2实时与远程审查协议

从实时会话、远程控制到账户审查,理解 V2 长连接能力的状态、身份和失败边界。

基于rust-v0.150.0
CodexRustAppServerRealtimeReview

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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

text
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 和最终历史分开阅读。