核心数据对象关系
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 运行句柄 | CodexThread | Core | Thread 加载到关闭 | 否 |
| Thread 客户端快照 | v2::Thread | App Server protocol | 单次响应或通知 | 由 rollout/metadata 重建 |
| Session 运行时 | Session | Core | 与一个已加载 Thread 同寿命 | 否 |
| Turn 客户端快照 | v2::Turn | App Server protocol | 单次投影 | 由事件与 item 重建 |
| Turn 运行状态 | TurnContext、ActiveTurn、RunningTask、TurnState | Core | 一次活动 Turn | 部分字段转为 rollout item |
| Task 执行策略 | SessionTask 实现 | Core | 一次异步工作 | 否 |
| 模型历史 Item | ResponseItem | protocol / Core | 跨 Turn history | 按策略持久化 |
| UI Item | Core TurnItem、v2 ThreadItem | protocol / App Server | item 生命周期或历史投影 | paginated 模式保存 completed item |
| 命令与事件 | Submission<Op>、Event<EventMsg> | Core protocol | channel 中的一次消息 | Op 不保存;部分 EventMsg 保存 |
| 工具调用 | ResponseItem → ToolCall → ToolInvocation | model / Core tools | 一次 call | 调用与输出以 ResponseItem 保存 |
| Rollout | RolloutItem、LiveThread | protocol / thread-store | Thread 的追加历史 | 是,但受持久化策略过滤 |
这个表揭示了两个关键事实: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
// 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
// 运行状态与 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
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 保护的配置、ContextManagerhistory、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
// 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
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
// 两个方向都保留关联 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
// 路由后的调用把工具名、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 最终出现在文件里。
持久化策略有意区分“实时可见”和“恢复必需”:
| 数据 | Legacy | Paginated | 原因 |
|---|---|---|---|
常规 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 | 生成或来源 | 作用范围 | 主要关联 |
|---|---|---|---|
ThreadId | Core UUIDv7 | 一个具体 Thread | ThreadManager、store、事件 thread_id |
SessionId | root Thread 转换、继承或 resume | agent tree | root 与所有 descendant Thread |
| submission / turn ID | new_submission_id UUIDv7 | 一次创建 Turn 的 submission | TurnContext.sub_id、Event.id、v2 Turn.id |
| JSON-RPC request ID | App Server client | 一次请求/响应 | 与 Turn ID 独立 |
ResponseItemId | provider 或 Core 按类型补齐 | 一个模型历史 item | raw response、TurnItem 转换 |
| Core/v2 item ID | provider ID、call ID或本地生成 | 一个展示 item | ItemStarted/Completed upsert |
call_id | 模型工具调用 | 一次 tool call | call 与 output 配对 |
model response_id | Responses API | 一次采样 | RawResponseCompleted、连续性与遥测 |
client_user_message_id | 客户端可选提供 | 一条用户消息 | UI 去重与 admission |
| process ID | exec 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 仍显示 running | TurnComplete/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 ServerThreadItem是否继续保持两层 schema;- extension
ToolCall是否继续由 Core routerToolCall/ToolInvocation转换; RolloutItem的过滤策略和ThreadHistoryBuilderreducer 是否新增或移除权威数据源。
只要这些边界不变,就可以用一句话概括整个模型: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
// :: 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
// :: 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
// :: 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
// :: 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 对象关系验证
# 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字段。
