取消与错误传播
本文回答一个具体问题: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 的关键分支摘录,省略号表示源码中其余显式变体,不是可直接编译的完整函数。
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
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
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
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_finished | task 返回 Some/Err/TurnAborted | 外层生命周期、错误事件和 active state 收尾 | 不证明 provider 网络本身可恢复 |
| interrupted marker tests | V1/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
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. 取消与错误
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读者应能解释:
- 为什么
CancellationToken已取消不等于TurnAborted已发送? - 为什么
is_retryable()不能单独证明会发生重试? - 普通工具失败、stream retry 和 TurnAborted 分别由哪一层拥有终态?
- rollout flush 失败时,用户可见事件和持久化保证有什么差别?
下一步阅读 Turn主循环与退出条件 对照 retry/abort 的循环位置,再阅读 Session关闭流程 了解 Session 级 abort 如何触发 task 级清理。
