Skip to content

核心数据对象关系

区分 Codex 运行时、模型历史、工具系统、客户端协议与持久化层中的核心对象,解释它们的所有权、生命周期和标识符关系。

基于rust-v0.150.0
CodexRustProtocolArchitecture

核心数据对象关系 ​

Codex 源码中最容易造成误解的不是某个复杂算法,而是同一个业务名词在不同层代表不同对象。 Thread 既可能指 App Server 返回的可序列化快照,也可能指 Core 中持有 channel 的 CodexThread;Session 既是每个 Thread 的运行时状态所有者,session_id 又表示整个 agent 树共享的身份;Turn 在 App Server 中是一个结构体,在 Core 中却由多个运行时对象组合表达。

因此,阅读源码时不能只问“这个类型叫什么”,还要同时问四件事:它属于哪一层、谁拥有它、 活多久、是否会持久化。本文按这四个问题区分 Thread、Session、Turn、Task、Item、Op、Event、 ToolCall 和 Rollout。

本文是一张对象与 ID 地图,只回答“是什么对象、谁拥有、活多久、如何投影”。 Thread与Turn概念模型 负责解释 Thread/Turn 的业务语义, Session核心数据结构 负责逐字段解释锁和同步原语, TurnContext字段 负责 Turn 级字段来源、所有权与 持久化投影。三者不能用本篇的关系图替代。

1. 对象抽象层次 ​

先给出全局定位。表中的“权威状态”表示发生冲突时应相信哪一层,不表示其他层没有同名类型。

对象主要类型所属层生命周期是否直接持久化
Thread 运行句柄CodexThreadCoreThread 加载到关闭否
Thread 客户端快照v2::ThreadApp Server protocol单次响应或通知由 rollout/metadata 重建
Session 运行时SessionCore与一个已加载 Thread 同寿命否
Turn 客户端快照v2::TurnApp Server protocol单次投影由事件与 item 重建
Turn 运行状态TurnContext、ActiveTurn、RunningTask、TurnStateCore一次活动 Turn部分字段转为 rollout item
Task 执行策略SessionTask 实现Core一次异步工作否
模型历史 ItemResponseItemprotocol / Core跨 Turn history按策略持久化
UI ItemCore TurnItem、v2 ThreadItemprotocol / App Serveritem 生命周期或历史投影paginated 模式保存 completed item
命令与事件Submission<Op>、Event<EventMsg>Core protocolchannel 中的一次消息Op 不保存;部分 EventMsg 保存
工具调用ResponseItem → ToolCall → ToolInvocationmodel / Core tools一次 call调用与输出以 ResponseItem 保存
RolloutRolloutItem、LiveThreadprotocol / thread-storeThread 的追加历史是,但受持久化策略过滤

这个表揭示了两个关键事实:Core 没有一个包揽所有字段的 Turn struct;rollout 也不是把整个 Session 序列化到磁盘,而是追加一串经过策略选择的 RolloutItem。

图中的 CodexThread → Session 和 CodexThread → Session I/O 是同一个对象的两类所有权:前者 持有状态,后者持有 submission sender、event receiver、status watch 和 session loop 结束 future, 并不是存在两个 Session。

2. Thread 三种形态 ​

ThreadManagerState 的 threads 字段是 Core 中已加载 Thread 的权威集合:

源码位置:codex-rs/core/src/thread_manager.rs :: ThreadManagerState

rust
// loaded Thread 的权威关系是 ThreadId 到 Arc<CodexThread> 的共享 map。
pub(crate) struct ThreadManagerState {
    threads: Arc<RwLock<HashMap<ThreadId, Arc<CodexThread>>>>,
    thread_created_tx: broadcast::Sender<ThreadId>,
    // 其余字段是创建 Thread 时复用的服务与 store。
}

pub struct NewThread {
    pub thread_id: ThreadId,
    pub thread: Arc<CodexThread>,
    pub session_configured: SessionConfiguredEvent,
}

CodexThread 是对外操作句柄。它把运行状态和消息端点组合在一起:

源码位置:codex-rs/core/src/codex_thread.rs :: CodexThread

rust
// 运行状态与 I/O 端点组合成 handle,但仍保持不同所有权对象。
pub struct CodexThread {
    pub(crate) session: Arc<Session>,
    pub(crate) io: SessionIo,
    pub(crate) session_source: SessionSource,
    session_configured: SessionConfiguredEvent,
    rollout_path: Option<PathBuf>,
    out_of_band_elicitations: Mutex<OutOfBandElicitations>,
    _diagnostics_guard: GaugeGuard,
}

pub async fn submit(&self, op: Op) -> CodexResult<String> {
    self.io.submit(op).await
}

SessionIo 不保存 conversation history。它只保存连接长期 submission_loop 的 I/O:有界 tx_sub、无界 rx_event、最新 AgentStatus 的 watch receiver,以及可复用的 loop 结束 future。 真正的 history、配置、服务和活动 Turn 都在 Session 中。

源码位置:codex-rs/core/src/session/mod.rs :: SessionIo

rust
pub(crate) struct SessionIo {
    // submission 与 event 是方向相反的端点,容量策略也不同。
    pub(crate) tx_sub: Sender<Submission>,
    pub(crate) rx_event: Receiver<Event>,
    // status 用 watch 只保留最新值,不承担完整事件历史。
    pub(crate) agent_status: watch::Receiver<AgentStatus>,
    // completion 是可 clone 的 shared future,让多个 handle 等同一个 loop。
    pub(crate) session_loop_termination: SessionLoopTermination,
}

pub(crate) type SessionLoopTermination = Shared<BoxFuture<'static, ()>>;

impl SessionIo {
    // submit 统一生成 ID,调用方不直接构造 Submission 破坏关联规则。
    pub(crate) async fn submit(&self, op: Op) -> CodexResult<String> {
        self.submit_with_trace(op, /*trace*/ None, /*parent_turn_id*/ None)
            .await
    }

    pub(crate) async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()> {
        if sub.trace.is_none() {
            sub.trace = current_span_w3c_trace_context();
        }
        self.tx_sub
            .send(sub)
            .await
            .map_err(|_| CodexErr::InternalAgentDied)?;
        Ok(())
    }
}

四个字段分别回答“如何提交”“如何接收”“当前状态是什么”“何时彻底结束”,没有一个字段可以替代 SessionState 或 rollout。

App Server protocol 的 Thread 则完全不同。它是一份可序列化快照,包含 id、session_id、 fork/parent 关系、preview、时间戳、路径、cwd、来源、状态和可选 turns。未加载 Thread 也能从 ThreadStore 读取为这类快照,所以不能把 v2::Thread 当成 Arc<CodexThread> 的网络版。

第三个对象 LiveThread 负责持久化生命周期。它只知道 thread_id、history mode、 ThreadStore、metadata sync 和持久化遥测;本地 store 可以写 rollout 文件,远端 store 可以采用 其他实现。Core Session 通过 SessionServices.live_thread: Option<LiveThread> 使用它,ephemeral 或禁用持久化时该字段可以是 None。

3. Session与Thread ​

Session 的源码注释写着“initialized model agent context”,并保证同一时刻最多运行一个 task。 它可分成三类成员:

  • SessionState:受同一个 mutex 保护的配置、ContextManager history、token/rate limit、 compaction window 和跨 Turn 状态;
  • SessionServices:模型客户端、MCP runtime、执行进程管理、认证、tools、extensions、 LiveThread 等长寿命服务;
  • 并发控制:active_turn、input queue、pending admission、realtime conversation 和 shutdown 状态。

在 rust-v0.150.0 中,Session 还直接持有 managed network proxy refresh 的 Semaphore、 MCP refresh/prewarm 的取消 token 与 task、OnceLock<MultiAgentVersion>、异步 hook 结果接收端和 GuardianReviewSessionManager。这些字段说明“一个 Session 至多一个活动 Turn”并不等于“Session 只有一个异步任务”:MCP 预热、网络代理刷新和 hook 消费都拥有独立生命周期,活动 Turn 只是其中 受 active_turn 约束的一条执行槽位。

SessionState.history 保存的是 ResponseItem,而不是 App Server ThreadItem。这是因为下一次模型 采样需要 Responses API 语义的消息、reasoning、tool call 和 tool output,而不是 UI 已经格式化的 命令行、状态或展示文本。

这里必须区别 Rust 类型 Session 和协议身份 SessionId:

  • 每个已加载 CodexThread 都有自己的 Arc<Session> 和唯一 thread_id;
  • root Thread 创建时,session_id 默认由自己的 ThreadId 转换而来;
  • non-root agent Thread 继承 AgentControl 的 session_id;
  • resume 时优先读取 rollout SessionMeta 中保存的 session ID;
  • 因而同一 agent tree 中可以有多个 Core Session 实例,却共享一个 session_id。

SessionId 与 ThreadId 都包装 UUID,并提供双向转换,但类型系统仍把“整个树的身份”和“具体 Thread 的身份”分开。对 root 而言值通常相同,不代表语义等价。

这张图表达身份归属,不表示 SessionId 对象在内存中拥有 Thread。真正的内存所有权仍由 ThreadManager 和 AgentControl 管理。

4. Core Turn ​

App Server 的 v2::Turn 只有客户端需要的投影字段:id、items、items view、status、error、 开始/完成时间和 duration。Core 没有与它同形的 struct,原因是运行时需要把不同同步策略的状态 拆开。

4.1 TurnContext ​

Session::new_turn_with_sub_id 先把本 Turn 的 settings update 应用到 Session 配置,再调用 new_turn_context_from_configuration。TurnContext 保存 turn ID、模型、provider、环境快照、cwd、 权限配置、reasoning 设置、动态工具、扩展存储、timing 和 terminal error。

大部分字段创建后不再替换,因此 task、工具和 step 可以通过 Arc<TurnContext> 共享同一份 Turn 语义。源码中的注释还明确标出若干迁移中的 legacy 字段:step-scoped 执行应读取 StepContext 的模型、reasoning effort、summary、approval policy 和 telemetry,而不是继续从 TurnContext 的 同名字段读取。因而 TurnContext 是 Turn-wide 快照与兼容桥,不是每个模型 step 的最终状态容器。 需要在执行中更新的元数据又被拆成内部同步对象,如 TurnMetadataState、 TurnTimingState 和 Mutex<Option<ErrorEvent>>。

4.2 ActiveTurn ​

Session.active_turn 的类型是 Mutex<Option<ActiveTurn>>。ActiveTurn 内部有可选 RunningTask 和一个可共享的 Arc<Mutex<TurnState>>。Option 为 None 表示 Session 当前空闲; 存在 ActiveTurn 但 task 暂时为空,是生命周期切换中的合法中间状态。

4.3 RunningTask ​

RunningTask 保存类型擦除后的 AnySessionTask、Tokio handle、CancellationToken、完成 Notify、TaskKind、Arc<TurnContext>、agent execution guard 和计时器。它回答的是“当前异步 工作如何停止和回收”,不是“历史 Turn 有哪些内容”。

4.4 TurnState ​

TurnState 保存审批 oneshot、request_user_input 回复、MCP elicitation、动态工具回复、pending input、已授予权限、tool call 计数和 token baseline。这些数据只在活动 Turn 内有意义,结束后不会 整体序列化到 rollout。

这也解释了为什么 App Server 能展示多个历史 Turn,而 Core 只有一个 active_turn:历史 Turn 不是多个 RunningTask 留在内存中,而是由 rollout 和 history 重新分组得到。

5. Task 执行策略 ​

SessionTask trait 只有四项职责:报告 TaskKind、提供 tracing span 名、执行 run,以及可选的 abort 清理。Session::start_task 统一创建 Tokio task、取消 token、Turn timing 和终态处理。

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

rust
// Task 是可取消的执行策略,不是可持久化的 Turn 数据对象。
pub(crate) trait SessionTask: Send + Sync + 'static {
    fn kind(&self) -> TaskKind;
    fn span_name(&self) -> &'static str;

    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;
}

生产实现包括 RegularTask、CompactTask、ReviewTask 和 UserShellCommandTask。前三者分别 报告 Regular、Compact、Review;独立 user shell task 复用 Regular kind,但使用不同 span。 所以 TaskKind 是粗粒度遥测/UI 分类,不是每个 task 实现的一一枚举。

活动 Turn 再把 task owner 与当前交互状态分开:

源码位置:codex-rs/core/src/state/turn.rs :: ActiveTurn, RunningTask

rust
pub(crate) struct ActiveTurn {
    // task 可在清理阶段暂时为空,TurnState 仍需完成 waiter 清理。
    pub(crate) task: Option<RunningTask>,
    pub(crate) turn_state: Arc<Mutex<TurnState>>,
}

pub(crate) struct RunningTask {
    pub(crate) done: Arc<Notify>,
    pub(crate) kind: TaskKind,
    pub(crate) task: Arc<dyn AnySessionTask>,
    // 协作取消与 abort-on-drop 共同保证 task 最终可终止。
    pub(crate) cancellation_token: CancellationToken,
    pub(crate) handle: AbortOnDropHandle<()>,
    pub(crate) turn_context: Arc<TurnContext>,
    pub(crate) _agent_execution_guard: Option<AgentExecutionGuard>,
    pub(crate) _timer: Option<codex_otel::Timer>,
}

ActiveTurn 是槽位,RunningTask 是当前 occupant,TurnState 是等待表;三者的组合才构成一次活动 Turn 的运行时切片。

同一个 Turn 通常由一个 task 驱动,但二者仍不能互换:

  • Turn ID 来自 submission,task 没有独立业务 ID;
  • TurnContext 描述本次执行环境,task 描述采用哪种算法;
  • task 返回后由 Session::on_task_finished 产生 Turn 终态;
  • task 被替换或中断时,Turn 通过 TurnAborted 结束;
  • 历史重放只重建 Turn 和 Item,不会重建原来的 Rust task 对象。

6. Submission事件 ​

Op 是“请求 Session 做什么”的命令枚举,EventMsg 是“Core 已观察到什么”的事实枚举。 它们通过两个信封与 submission ID 相关联:

源码位置:codex-rs/protocol/src/protocol.rs :: Submission, Event

rust
// 两个方向都保留关联 ID,使控制提交与返回事件可以归并。
pub struct Submission {
    pub id: String,
    pub op: Op,
    pub client_user_message_id: Option<String>,
    pub trace: Option<W3cTraceContext>,
    pub parent_turn_id: Option<String>,
}

pub struct Event {
    /// 与 Submission id 或当前 Turn id 关联。
    pub id: String,
    pub msg: EventMsg,
}

Op 包括用户输入、interrupt、审批回复、MCP elicitation 回复、动态工具回复、compact、review、 rollback、thread settings 和 shutdown 等。不是每个 Op 都创建 Turn:例如审批回复只是解除 TurnState 中的 oneshot,thread settings 可以只修改 Session,shutdown 结束 loop。

EventMsg 既包含 Turn/Item 生命周期,也包含 delta、审批请求、stream error、token usage、MCP 启动状态和 shutdown complete。Event 是 Core 内部协议事实,App Server 还会把它映射成另一层 ServerNotification 或 ServerRequest;两者不能直接按 enum variant 名逐项等同。

Submission.id 在 Op::UserInput 创建新 Turn 时被公开为 turn ID;其他 Op 也有 submission ID, 却不因此自动成为一个客户端 Turn。ID 的语义取决于 Op 和事件生命周期,不能只看它是 UUIDv7。

7. Item的三种投影 ​

源码中至少有三套不能混用的 item:

7.1 ResponseItem ​

ResponseItem 对齐 Responses API 的 item 语义,包含 message、agent message、reasoning、function call、custom tool call、tool search、各种 tool output、web search、image generation 和 compaction。 SessionState.history 保存它,下一次 Prompt 也从它构造。

每个可携带 ID 的 variant 使用 ResponseItemId。Core 在记录历史前通过 assign_missing_response_item_id 按类型补 msg、fc、fco、ctc 等前缀,并通过 InternalChatMessageMetadataPassthrough 补充 turn_id。item ID 和 turn ID 是两个维度:前者标识 具体 item,后者把 item 归属到某次 Turn。

7.2 Core TurnItem ​

Core TurnItem 表示 user/agent message、reasoning、command execution、file change、MCP、dynamic tool、multi-agent、plan、review、compaction 与 extension item。它不负责构造下一次模型 prompt, 而是放进 ItemStartedEvent 和 ItemCompletedEvent,给客户端稳定的生命周期。

普通 assistant message、reasoning 和 hosted web search 可由 event_mapping::parse_turn_item 从 ResponseItem 转换;命令、MCP、文件修改和 extension 工具通常由 handler 直接构造并调用 emit_turn_item_started/completed。因此不存在一个覆盖所有 variant 的通用 ResponseItem -> TurnItem 强制转换。

7.3 v2 ThreadItem ​

App Server protocol 再通过 impl From<CoreTurnItem> for ThreadItem 转换字段。例如 Core agent message 保存分段 content,v2 ThreadItem::AgentMessage 对外合并为 text;命令、MCP 和 dynamic tool 也转换为面向客户端的 status、duration 和结果结构。

图中 rollout 两条箭头都只是候选写入,最终是否保留由 history mode 和 persistence policy 决定。 尤其 ItemStarted 是 transient event,不会成为 durable rollout;paginated 模式主要保留 ItemCompleted(TurnItem)。

8. ToolCall 转换链 ​

工具调用从模型输出开始。ToolRouter::build_tool_call 只接受能由客户端执行的 ResponseItem::FunctionCall、client tool search 或 custom tool call,并转成 Core router 的 ToolCall:

源码位置:codex-rs/core/src/tools/router.rs :: ToolCall

rust
// 路由后的调用把工具名、call ID 与已解析 payload 绑定为一次执行输入。
#[derive(Clone, Debug, PartialEq)]
pub struct ToolCall {
    pub tool_name: ToolName,
    pub call_id: String,
    pub payload: ToolPayload,
    pub encrypted_function_args: Option<Vec<String>>,
}

pub struct ToolRouter {
    registry: ToolRegistry,
    model_visible_specs: Vec<ToolSpec>,
}

这个 ToolCall 是路由 DTO:tool_name 找 handler,call_id 把结果与模型请求配对,payload 保存 function JSON、custom text 或 tool-search 参数。ToolRouter 分派时再构造 ToolInvocation, 补上 Arc<Session>、Arc<TurnContext>、精确的 StepContext、取消 token、diff tracker 和来源。

codex-tools crate 也有一个公开 ToolCall,但它只在 extension tool 边界由 to_extension_call 构造。该类型携带 conversation history、model、环境、sandbox context 和 TurnItemEmitter,不能与 router 的轻量 ToolCall 混为一谈。

call_id 是整条链最稳定的关联键:模型发出的 call 和回灌的 output 共用它。tool item 自己还 可能有 item ID,底层进程可能有 process ID;这些 ID 分属不同资源,不应相互替代。

9. Rollout 追加日志 ​

RolloutItem 枚举允许保存以下内容:

  • SessionMeta:Thread 与 session 的创建元数据;
  • ResponseItem:模型可见 conversation history;
  • inter-agent communication 及其 metadata;
  • Compacted:压缩结果与 context window 链;
  • TurnContext、WorldState:恢复模型可见运行环境所需的基线;
  • EventMsg:被持久化策略允许的生命周期或兼容事件。

Session::persist_rollout_items 把候选项交给 LiveThread::append_items。LiveThread 再委托具体 ThreadStore,同步 metadata,并应用 codex-rollout 的 history-mode 策略。因而调用 send_event_raw_with_persistence 不保证每个 EventMsg 最终出现在文件里。

持久化策略有意区分“实时可见”和“恢复必需”:

数据LegacyPaginated原因
常规 ResponseItem保存保存重建模型 history
TurnStarted、TurnComplete、TurnAborted保存保存重建 Turn 边界
ItemCompleted(TurnItem)仅 Plan/Sleep 等特殊项保存paginated 使用 canonical item
legacy user/agent/reasoning 终态事件保存不保存避免与 canonical item 重复
ItemStarted、文本 delta、approval request、stream error不保存不保存仅实时交互,不用于恢复
SessionMeta、TurnContext、WorldState、Compacted保存保存恢复配置、环境与 context window

App Server 的 ThreadHistoryBuilder 同时能消费 live EventMsg 和历史 RolloutItem。它用 TurnStarted 打开 PendingTurn,用 item/response 归并内容,再用 TurnComplete 或 TurnAborted 关闭 Turn。也就是说,客户端看到的 Vec<Turn> 是 reducer 的输出,不是 rollout 文件中嵌套保存的一组 Turn struct。

10. ID 关系决定对象 ​

最后把常见 ID 放在一起。它们看起来都是字符串,却服务于不同关联范围。

ID生成或来源作用范围主要关联
ThreadIdCore UUIDv7一个具体 ThreadThreadManager、store、事件 thread_id
SessionIdroot Thread 转换、继承或 resumeagent treeroot 与所有 descendant Thread
submission / turn IDnew_submission_id UUIDv7一次创建 Turn 的 submissionTurnContext.sub_id、Event.id、v2 Turn.id
JSON-RPC request IDApp Server client一次请求/响应与 Turn ID 独立
ResponseItemIdprovider 或 Core 按类型补齐一个模型历史 itemraw response、TurnItem 转换
Core/v2 item IDprovider ID、call ID或本地生成一个展示 itemItemStarted/Completed upsert
call_id模型工具调用一次 tool callcall 与 output 配对
model response_idResponses API一次采样RawResponseCompleted、连续性与遥测
client_user_message_id客户端可选提供一条用户消息UI 去重与 admission
process IDexec runtime一个执行进程stdin、输出和终止

图中的“包含”是逻辑归属,不表示 SessionId 内部存有 Thread 列表;“拥有多个”也由 rollout 和 App Server reducer 重建,不表示 Core Session 同时保留多个 ActiveTurn。

11. 对象关系定位 ​

遇到状态不一致时,先判断问题属于哪一层,通常比全文搜索 Turn 更快:

现象优先检查的对象
模型下一次采样看不到工具结果ResponseItem history、call_id、record_conversation_items
UI 中命令一直显示运行中Core TurnItem 生命周期、v2 ThreadItem 转换、item ID
Turn 已结束但 Thread 仍显示 runningTurnComplete/TurnAborted、ThreadHistoryBuilder、App Server ThreadState
中断后仍有审批悬挂ActiveTurn、TurnState waiters、RunningTask.cancellation_token
resume 后模型或权限配置错误SessionMeta、TurnContextItem、WorldStateItem、Session 配置恢复
子 agent 被归到错误会话SessionId、ThreadId、parent/fork ID、AgentControl
rollout 中缺少某条 delta先查 should_persist_event_msg;delta 本来就可能只实时投递

源码阅读顺序也应按对象层次展开:先从 ThreadManager → CodexThread → Session 确定所有权,再从 TurnContext → ActiveTurn → RunningTask → TurnState 确定当前执行,随后检查 Submission/Op → Event/EventMsg 的消息方向,最后才进入 ResponseItem → ToolCall → TurnItem → ThreadItem → RolloutItem 的转换链。

12. 版本结构变化 ​

这些对象关系比单个函数名稳定,但仍有几个明确的演进点:

  • Session 与 session_id 的语义是否继续分离,agent tree 是否仍共享 session identity;
  • Core 是否引入真正的 Turn 聚合类型,替代当前四对象组合;
  • TaskKind 是否扩展,user shell 是否仍复用 Regular;
  • paginated history 是否完全替代 legacy event 投影;
  • TurnItem 与 App Server ThreadItem 是否继续保持两层 schema;
  • extension ToolCall 是否继续由 Core router ToolCall / ToolInvocation 转换;
  • RolloutItem 的过滤策略和 ThreadHistoryBuilder reducer 是否新增或移除权威数据源。

只要这些边界不变,就可以用一句话概括整个模型:ThreadManager 持有运行中的 CodexThread/Session,一次活动 Turn 由 context、slot、task 和 mutable state 协作完成,模型历史用 ResponseItem 延续,客户端状态用 TurnItem/ThreadItem 投影,最终由筛选后的 RolloutItem 在 下一次加载时重建。

13. 身份与历史投影 ​

对象关系最容易在“ID 相同”时被误读。下面三组测试分别验证:同一个 ThreadId 是否复用同一 Arc, 活动槽位何时真正消失,以及历史 Turn 如何在没有 RunningTask 的情况下重建。

13.1 Active复用 ​

resume_active_thread_from_rollout_returns_running_thread 在 manager 仍持有活动 Thread 时从同一 rollout resume。结果不仅 ThreadId 相等,Arc::ptr_eq 也必须为 true。

源码位置:codex-rs/core/src/thread_manager_tests.rs

rust
// :: resume_active_thread_from_rollout_returns_running_thread(结果路径)
let resumed = manager
    .resume_thread_from_rollout(
        config,
        rollout_path,
        auth_manager,
        /*parent_trace*/ None,
        ClientMcpExtensions::default(),
    )
    .await
    .expect("resume active source thread");

assert_eq!(resumed.thread_id, source.thread_id);
// manager命中活动registry,返回完全相同的CodexThread allocation。
assert!(Arc::ptr_eq(&resumed.thread, &source.thread));

相邻测试先关闭 source,再用相同 rollout resume。ThreadId 仍保持,但新 Session/CodexThread 必须是另一 allocation;否则已停止的 I/O、task 和 service owner 会被错误复活。

源码位置:codex-rs/core/src/thread_manager_tests.rs

rust
// :: resume_stopped_thread_from_rollout_spawns_new_thread(关键路径)
source
    .thread
    .shutdown_and_wait()
    .await
    .expect("shutdown source thread");

let resumed = manager
    .resume_thread_from_rollout(
        config,
        rollout_path,
        auth_manager,
        /*parent_trace*/ None,
        ClientMcpExtensions::default(),
    )
    .await
    .expect("resume stopped source thread");

assert_eq!(resumed.thread_id, source.thread_id);
// durable identity复用,不代表内存owner复用。
assert!(!Arc::ptr_eq(&resumed.thread, &source.thread));

恢复事务的 writer、history 和 registry 细节见 ThreadManager恢复。

13.2 Idle通知时序 ​

task_finish_emits_thread_idle_lifecycle_after_active_turn_clears 安装一个 thread idle contributor,再运行 会立即完成的 task。收到 idle callback 后,测试要求 callback 只调用一次,且 active_turn 已为 None。

源码位置:codex-rs/core/src/session/tests.rs

rust
// :: task_finish_emits_thread_idle_lifecycle_after_active_turn_clears(结果路径)
let (mut session, turn_context) = make_session_and_context().await;
let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let (idle_tx, idle_rx) = async_channel::bounded(1);
let mut builder = codex_extension_api::ExtensionRegistryBuilder::<crate::config::Config>::new();
builder.thread_lifecycle_contributor(Arc::new(ThreadIdleRecorder {
    calls: Arc::clone(&calls),
    idle_tx,
    expected_thread_id: session.thread_id,
}));
session.services.extensions = Arc::new(builder.build());

let session = Arc::new(session);
session
    .spawn_task(Arc::new(turn_context), Vec::new(), CompletingTask)
    .await;

timeout(StdDuration::from_secs(2), idle_rx.recv())
    .await
    .expect("thread idle lifecycle")
    .expect("idle receiver open");

assert_eq!(1, calls.load(std::sync::atomic::Ordering::SeqCst));
// idle是清槽后的事实,不是“task即将完成”的提前通知。
assert!(session.active_turn.lock().await.is_none());

这组测试把 RunningTask结束 → ActiveTurn清空 → Thread idle 固定为可观察顺序。字段与 pointer identity 保护的详细解释留给 Session核心数据结构。

13.3 历史Turn投影 ​

projects_identified_turn_aborts 只给 ThreadHistoryBuilder 一个带 TurnId 的 durable abort event,就能 得到 Interrupted Turn change;输入中没有 Session、ActiveTurn 或 RunningTask。

源码位置:codex-rs/app-server-protocol/src/protocol/thread_history_projection_tests.rs

rust
// :: projects_identified_turn_aborts(完整测试)
#[test]
fn projects_identified_turn_aborts() {
    let changes = project(RolloutItem::EventMsg(EventMsg::TurnAborted(
        TurnAbortedEvent {
            // reducer依赖TurnId关联历史终态;没有ID的legacy abort会被忽略。
            turn_id: Some("turn-1".to_string()),
            reason: TurnAbortReason::Interrupted,
            started_at: Some(10),
            completed_at: Some(20),
            duration_ms: Some(10_000),
        },
    )));

    assert_eq!(
        changes,
        ThreadHistoryChangeSet {
            changed_turns: vec![ThreadHistoryTurnChange {
                turn_id: "turn-1".to_string(),
                status: TurnStatus::Interrupted,
                error: None,
                started_at: Some(10),
                completed_at: Some(20),
                duration_ms: Some(10_000),
            }],
            ..Default::default()
        }
    );
}

同文件还验证没有 TurnId 的 legacy abort 被忽略。这说明 ID 不只是展示字段,而是 reducer 能否把终态归并 到历史 Turn 的必要关联键。

13.4 对象关系验证 ​

bash
# 1. 哪些对象由ThreadId查找,哪些只在当前active slot中存在?
rg -n "HashMap<ThreadId|active_turn|Arc<Mutex<TurnState" codex-rs/core/src

# 2. 哪些测试用值相等,哪些测试必须用指针身份?
rg -n "resume_(active|stopped)_thread|Arc::ptr_eq" codex-rs/core/src/thread_manager_tests.rs

# 3. 历史Turn由哪些Rollout/Event终态重建?
rg -n "TurnStarted|TurnComplete|TurnAborted|PendingTurn" \
  codex-rs/app-server-protocol/src/protocol/thread_history.rs

读者应能解释:ThreadId 相同为什么不保证 Arc<CodexThread> 相同;Session idle 为什么要求 active_turn=None;客户端历史 Turn 为什么不对应仍存活的 Rust task;以及何时应该离开本地图进入 Thread与Turn概念模型、 Session核心数据结构 或 TurnContext字段。