Skip to content

取消与错误传播

从 CancellationToken 和 CodexErr 追踪取消、可重试错误、普通失败到 TurnAborted 与用户可见事件的传播边界。

基于rust-v0.150.0
CodexRustRuntime

取消与错误传播 ​

本文回答一个具体问题:Core 中的取消、可重试错误和普通错误,如何经过 SessionTask、Turn loop 和完成 处理,最终变成 TurnAborted、retry、warning 或普通失败事件?重点是区分触发源、错误分类、owner 和 用户可见结果;本文不把所有 Err 都解释成 Turn 终止。

1. 四个处理层次 ​

层次真实对象负责什么
触发CancellationToken、abort_all_tasks通知运行中的 task 停止
分类CodexErr/CodexErrorDetails描述 TurnAborted、stream、context、usage 等原因
执行SessionTask::run/abort、Turn loop观察取消、清理资源、决定 retry 或返回
投影on_task_finished、EventMsg发送完成、错误、warning 或 TurnAborted

2. SessionTask ​

真实 trait 位于 codex-rs/core/src/tasks/mod.rs。Session 为每个 task 提供 token;实现必须观察它并尽快 终止,abort 是额外的清理机会,不是 run 返回成功的替代品。

源码位置:codex-rs/core/src/tasks/mod.rs :: SessionTask 下面是该 match 的关键分支摘录,省略号表示源码中其余显式变体,不是可直接编译的完整函数。

rust
fn run(
    self: Arc<Self>,
    session: Arc<Session>,
    ctx: Arc<TurnContext>,
    input: Vec<TurnInput>,
    cancellation_token: CancellationToken,
) -> impl Future<Output = SessionTaskResult> + Send;

fn abort(
    &self,
    session: Arc<Session>,
    ctx: Arc<TurnContext>,
) -> impl Future<Output = ()> + Send { /* 默认无操作 */ }

取消 token 被触发后,task 仍可能需要 flush rollout、写入 interrupted marker 或结束工具/审批等待;所以 “token 已 cancelled”与“用户已收到终态事件”不是同一时间点。

3. CodexErr边界 ​

CodexErr::is_retryable() 位于 codex-rs/protocol/src/error.rs。它是错误分类函数,不是自动重试本身: Turn loop 仍要决定何时重新建立 request、何时递增 retry 计数、何时把错误交给外层。

源码位置:codex-rs/protocol/src/error.rs :: CodexErr::is_retryable

rust
pub fn is_retryable(&self) -> bool {
    match self.details() {
        CodexErrorDetails::TurnAborted
        | CodexErrorDetails::SessionBudgetExceeded
        | CodexErrorDetails::Interrupted
        | CodexErrorDetails::ContextWindowExceeded
        | CodexErrorDetails::UsageLimitReached(_)
        | CodexErrorDetails::ServerOverloaded
        | CodexErrorDetails::RetryLimit(_) => false,
        CodexErrorDetails::Stream(..)
        | CodexErrorDetails::Timeout
        | CodexErrorDetails::RequestTimeout
        | CodexErrorDetails::ResponseStreamFailed(_)
        | CodexErrorDetails::ConnectionFailed(_)
        | CodexErrorDetails::InternalServerError => true,
        // ...其余显式变体也在同一 match 中按当前协议定义归类。
    }
}

不能把 retryable=true 写成“用户看不到错误”:重试可能耗尽,后续仍会产生最终错误或 warning;也不能把 TurnAborted 当作普通 stream retry,它进入专门的 abort 生命周期。

源码位置:codex-rs/protocol/src/error.rs :: CodexErrorDetails

rust
pub enum CodexErrorDetails {
    TurnAborted,
    ContextWindowExceeded,
    Stream(String),
    UsageLimitReached(UsageLimitError),
    // 其他协议和基础设施错误变体
}

// SessionTask的取消参数由Session创建并传入每个具体任务。
let cancellation_token = CancellationToken::new();
let task_result = task.run(session.clone(), turn_context.clone(), input, cancellation_token.clone()).await;
// task_result随后交给Session完成处理,而不是由任务自行发送最终TurnComplete。
let _ = session.on_task_finished(turn_context, task_result).await;

Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => {
    break Err(err);
}
Err(err) if err.is_retryable() => {
    handle_retryable_response_stream_error(/* request state */).await?;
    turn_context.turn_timing_state.record_sampling_retry();
}

4. Turn Loop错误 ​

codex-rs/core/src/session/turn.rs 在请求错误处先处理 context window、usage limit 等专门分支,再检查 is_retryable();只有当前实现列为 retryable 的 stream、timeout、连接或内部错误才进入 handle_retryable_response_stream_error,并记录 sampling retry。ContextWindowExceeded 虽然是独立错误分支, 当前实现不会把它当作普通 retryable stream error。

5. 两条终态路径 ​

主动取消和 task 自然返回错误不是同一条调用链。abort_all_tasks/abort_turn_if_active 先取出 RunningTask,再进入 handle_task_abort;该函数取消 token、等待宽限期、调用 SessionTask::abort、 写入可选 interrupted marker、flush rollout、计算 profile,并直接发送 TurnAborted。此时终态事件不经过 on_task_finished。

另一条路径是 task 自己返回结果,随后进入 on_task_finished:Err(TurnAborted) 被归约为 TurnAbortReason::Interrupted,普通 Err 先触发 emit_turn_error_lifecycle 与 EventMsg::Error,再继续 统一完成处理;没有 abort reason 时最终事件是 TurnComplete,其中可携带 terminal_error。

源码位置:codex-rs/core/src/tasks/mod.rs :: Session::on_task_finished

源码位置:codex-rs/core/src/tasks/mod.rs :: Session::on_task_finished

rust
let (last_agent_message, abort_reason) = match task_result {
    Ok(last_agent_message) => (last_agent_message, None),
    Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => {
        (None, Some(TurnAbortReason::Interrupted))
    }
    Err(err) => {
        self.emit_turn_error_lifecycle(turn_context.as_ref(), err.to_codex_protocol_error()).await;
        self.send_event(turn_context.as_ref(), EventMsg::Error(err.to_error_event(None))).await;
        (None, None)
    }
};

这段归约说明 TurnAborted 与普通错误的投影不同:前者交给中断生命周期,后者先发送错误事件;两者都在 完成处理阶段释放活动 task,而不是由具体 SessionTask 自行决定客户端终态。

6. 取消历史清理 ​

当前实现的中断路径还可能写入 model-visible interrupted marker,并在发送 TurnAborted 前尝试 flush; flush 失败只记录 warning。发送终态事件后还会再次 flush rollout,因此“事件已发出”与“rollout 已成功落盘” 仍是两个可观察结果。handle_task_abort 同时取消 Git enrichment,并在中断场景运行 interrupt hooks; pending approvals 会等待 task 观察取消后才清理,避免把审批拒绝顺序倒置。

7. 重试与取消边界 ​

测试/场景输入断言未覆盖什么
retryability_preserves_error_details_distinctions各 CodexErrorDetails 变体retryable 分类符合表不证明 Turn loop 一定执行成功重试
run_turn abort 分支cancellation token + running task返回 TurnAborted 并执行输入记录/收尾不证明任意第三方 tool 都及时观察 token
on_task_finishedtask 返回 Some/Err/TurnAborted外层生命周期、错误事件和 active state 收尾不证明 provider 网络本身可恢复
interrupted marker testsV1/V2 marker 配置marker 形状与版本分支正确不证明 flush 永远成功

run_turn 在响应流错误处先处理专门分支,再调用 is_retryable();只有返回 true 才进入 handle_retryable_response_stream_error 并递增 sampling retry。因而 is_retryable() 是分类器, 退避、最大次数、请求重建和最终失败由 Turn loop/response retry handler 共同决定。

源码位置:codex-rs/core/src/session/turn.rs :: run_turn;codex-rs/core/src/responses_retry.rs :: handle_retryable_response_stream_error

rust
if !err.is_retryable() {
    return Err(err);
}
handle_retryable_response_stream_error(
    &mut retry_state,
    max_retries,
    err,
    client_session,
    &sess,
    &turn_context,
    ResponsesStreamRequest::Sampling,
).await?;
turn_context.turn_timing_state.record_sampling_retry();

8. 取消与错误 ​

bash
rg -n "trait SessionTask|abort_all_tasks|TurnAborted|is_retryable|handle_retryable_response_stream_error" \
  codex-rs/core/src/tasks codex-rs/core/src/session/turn.rs codex-rs/protocol/src/error.rs
cargo test -p codex-core turn_aborted
cargo test -p codex-protocol is_retryable

读者应能解释:

  1. 为什么 CancellationToken 已取消不等于 TurnAborted 已发送?
  2. 为什么 is_retryable() 不能单独证明会发生重试?
  3. 普通工具失败、stream retry 和 TurnAborted 分别由哪一层拥有终态?
  4. rollout flush 失败时,用户可见事件和持久化保证有什么差别?

下一步阅读 Turn主循环与退出条件 对照 retry/abort 的循环位置,再阅读 Session关闭流程 了解 Session 级 abort 如何触发 task 级清理。