Session与Turn状态
Codex 把运行时状态拆成两个不同的可变性范围:SessionState 由 Session.state 持有,跨 Turn 保存配置、历史、token/rate-limit 和会话级授权;TurnState 由 ActiveTurn.turn_state 持有,服务一个当前 Turn,保存 pending waiter、pending input、Turn 级授权和本轮统计。ActiveTurn 是两者之间的运行时桥梁:它同时记录可选的 RunningTask 和共享的 Arc<Mutex<TurnState>>。
本文只讨论这些状态对象在当前 Turn 生命周期中的真实职责,不把 SessionConfiguration、TurnContext 或 rollout snapshot 误写成 SessionState 字段。
阅读前可先看 Session核心数据结构 和 TurnContext字段。本文范围限定为运行时状态的所有权、更新和可见性, 不展开模型请求字段;读完后应能把一个状态变化追到写入者、读取者和生效时机。
1. 所有权与锁边界
Session.state 和 Session.active_turn 是两把不同的锁。读取历史或 session-level permissions 时锁住前者;检查当前 task、保存 Turn waiter 或读取 Turn-level permission 时先锁住后者,再锁 turn_state。代码中的 await_holding_invalid_type 例外说明并非粗心地跨锁等待,而是为了让 active-turn 检查和对应 TurnState 更新保持原子关系。
源码位置:codex-rs/core/src/session/session.rs :: Session 字段。
pub(crate) struct Session {
pub(crate) thread_id: ThreadId,
pub(crate) installation_id: String,
pub(super) tx_event: Sender<Event>,
pub(super) agent_status: watch::Sender<AgentStatus>,
pub(super) state: Mutex<SessionState>,
pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
pub(crate) input_queue: InputQueue,
// Other session services are omitted here.
}初始化时 state 从 SessionState::new_with_auto_compact_window_ids 创建,active_turn 则明确为 None;因此“Session 已建立”和“当前存在活动 Turn”是两个事实。
App Server 还有一层独立的 ThreadWatchManager:它把 Core 的 TurnStarted/Completed/Interrupted、pending permission/user-input waiter 和 system error 归约为协议 ThreadStatus(NotLoaded、Idle、Active 及 active flags、SystemError)。因此 Session 内部 AgentStatus、ActiveTurn 与客户端 ThreadStatus 不是同一个状态对象,不能用其中任意一个直接替代另外两个。
2. SessionState字段
源码位置:codex-rs/core/src/state/session.rs :: SessionState。
pub(crate) struct SessionState {
pub(crate) session_configuration: SessionConfiguration,
pub(crate) history: ContextManager,
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
pub(crate) server_reasoning_included: bool,
pub(crate) mcp_dependency_prompted: HashSet<String>,
pub(crate) additional_context: AdditionalContextStore,
previous_turn_settings: Option<PreviousTurnSettings>,
auto_compact_window: AutoCompactWindow,
pub(crate) startup_prewarm: Option<SessionStartupPrewarmHandle>,
pub(crate) current_time_reminder: CurrentTimeReminderState,
pub(crate) active_connector_selection: HashSet<String>,
pub(crate) pending_session_start_sources: VecDeque<SessionStartSource>,
granted_permissions_by_environment_id: HashMap<String, AdditionalPermissionProfile>,
next_turn_is_first: bool,
}这些字段不是一个平面“全局变量袋”:
history通过record_items、replace_history、set_reference_context_item等方法维护模型上下文;替换 history 时还会清掉 auto-compaction 的 prefill。previous_turn_settings和next_turn_is_first跨越 Turn 边界,服务下一次 regular Turn 的上下文处理。auto_compact_window保存 token 预算窗口和 prefill/fallback 标记,不属于单个TurnState。latest_rate_limits使用合并逻辑保留新快照缺失的 credits、plan 等元数据;缺失limit_id时归入默认codexbucket。granted_permissions_by_environment_id是会话范围授权,并按 environment ID 隔离。
3. TurnState 字段
源码位置:codex-rs/core/src/state/turn.rs :: ActiveTurn、TurnState。
pub(crate) struct ActiveTurn {
pub(crate) task: Option<RunningTask>,
pub(crate) turn_state: Arc<Mutex<TurnState>>,
}
#[derive(Default)]
pub(crate) struct TurnState {
pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
pending_request_permissions: HashMap<String, PendingRequestPermissions>,
pending_user_input: HashMap<String, oneshot::Sender<RequestUserInputResponse>>,
pending_elicitations: HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>,
mcp_tool_approval_metadata: HashMap<String, (Option<McpInvocation>, McpToolApprovalMetadata)>,
pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
pub(crate) pending_input: TurnInputQueue,
mailbox_delivery_phase: MailboxDeliveryPhase,
granted_permissions_by_environment_id: HashMap<String, AdditionalPermissionProfile>,
strict_auto_review_enabled: bool,
pub(crate) tool_calls: u64,
pub(crate) has_memory_citation: bool,
pub(crate) token_usage_at_turn_start: TokenUsage,
}TurnState 的 waiter map 都是请求-响应配对:approval、request permissions、user input、MCP elicitation 和 dynamic tool 各有自己的 key 与 sender。它们不能搬到 SessionState,否则旧 Turn 的响应可能错误地唤醒新 Turn。pending_input 和 mailbox phase 也必须与同一个 TurnState 一起读取,才能判断消息是否仍允许并入当前 Turn。
4. ActiveTurn 状态
start_task 先从 InputQueue::get_pending_input 取得可消费输入,再在 active_turn 锁内取得或创建 ActiveTurn,为 TurnState.token_usage_at_turn_start 写入起始 token,最后才把 RunningTask 放入 active_turn.task。因此 ActiveTurn { task: None, ... } 是一个有意存在的 idle reservation,不等于 task 已经开始。
源码位置:codex-rs/core/src/tasks/mod.rs :: start_task。
let (pending_items, parent_turn_id) =
self.input_queue.get_pending_input(&self.active_turn).await;
let turn_state = {
let mut active = self.active_turn.lock().await;
let turn = active.get_or_insert_with(ActiveTurn::default);
debug_assert!(turn.task.is_none());
Arc::clone(&turn.turn_state)
};
turn_state.lock().await.token_usage_at_turn_start = token_usage_at_turn_start.clone();
self.input_queue
.extend_pending_input_for_turn_state(turn_state.as_ref(), pending_items)
.await;
self.emit_turn_start_lifecycle(turn_context.as_ref(), &token_usage_at_turn_start)
.await;
// The spawned task is installed into active_turn.task below.完成路径会先 take 掉 active_turn.task,读取 TurnState 中的 has_memory_citation、tool_calls 和 token baseline,处理 leftover pending input,然后在确认 reservation 仍指向该 TurnState 时清除整个 active turn。这个 identity 检查避免旧 task 完成时误删已经被新 reservation 替换的 Turn。
5. pending
源码位置:codex-rs/core/src/state/turn.rs :: clear_pending_waiters
pub(crate) fn clear_pending_waiters(&mut self) {
self.pending_approvals.clear();
self.pending_request_permissions.clear();
self.pending_user_input.clear();
self.pending_elicitations.clear();
self.mcp_tool_approval_metadata.clear();
self.pending_dynamic_tools.clear();
}中断或 task 清理调用 InputQueue::clear_pending,它在同一把 TurnState 锁下清理 waiter map 和 pending_input.items。这解释了为什么取消时不能只丢掉 RunningTask:仍悬挂的 oneshot sender 会让审批、用户输入或 elicitation 等待者留下错误的生命周期。
权限范围则明确分为 Turn 和 Session 两层:
源码位置:codex-rs/core/src/session/mod.rs :: record_granted_request_permissions_for_turn
match response.scope {
PermissionGrantScope::Turn => {
if let Some(turn_state) = originating_turn_state {
let mut ts = turn_state.lock().await;
let permissions: AdditionalPermissionProfile = response.permissions.clone().into();
ts.record_granted_permissions(environment_id, permissions);
if response.strict_auto_review {
ts.enable_strict_auto_review();
}
}
}
PermissionGrantScope::Session => {
let mut state = self.state.lock().await;
state.record_granted_permissions(
environment_id,
response.permissions.clone().into(),
);
}
}响应可能在 active Turn 已经切换后到达,所以 Turn scope 使用保存下来的 originating_turn_state,而不是盲目读取当前 active Turn。测试 record_granted_request_permissions_for_turn_uses_originating_turn 专门验证了这一点;同一组测试还验证权限按 environment ID 隔离,Session scope 不会写入 Turn scope。
6. mailbox
源码位置:codex-rs/core/src/state/turn.rs :: MailboxDeliveryPhase 和 TurnState::accepts_mailbox_delivery_for_current_turn。
InputQueue::get_pending_input 在 active-turn 锁和 TurnState 锁下先决定 phase:CurrentTurn 才取出 TurnInput 并 drain session mailbox;NextTurn 返回空的当前 Turn mailbox 输入。于是“mailbox 中有消息”不等于“当前模型请求会看到消息”。
源码位置:codex-rs/core/src/session/input_queue.rs :: get_pending_input
let (pending_input, accepts_mailbox_delivery) = {
let mut active = active_turn.lock().await;
match active.as_mut() {
Some(active_turn) => {
let mut turn_state = active_turn.turn_state.lock().await;
let accepts_mailbox_delivery =
turn_state.accepts_mailbox_delivery_for_current_turn();
let pending_input = if accepts_mailbox_delivery {
turn_state.pending_input.items.split_off(0)
} else {
Vec::new()
};
(pending_input, accepts_mailbox_delivery)
}
None => (Vec::new(), true),
}
};显式 steer 或工具调用会调用 accept_mailbox_delivery_for_current_turn;答案边界后的 queue-only child mail 和 trigger-turn mail 则通过 defer_mailbox_delivery_to_next_turn 留给下一 Turn。phase 是 TurnState 的字段,而不是 InputQueue 的全局开关,因此多个 Turn 不会互相覆盖投递状态。
7. SessionState更新
例如 token 使用量的“累计事实”存入 SessionState.history,而本 Turn 的起始 token baseline 存入 TurnState,完成时用二者计算 Turn 增量。tool_calls、has_memory_citation 也只在 Turn 完成时读取并汇总,不应写入 SessionState 作为跨 Turn 的当前值。
8. 失败路径测试
- Session 创建后
active_turn仍为None;空闲 gate 可以先放入 reservation,再由后续逻辑决定是否清除。 - task 被替换或中断时,
abort_all_tasks先取出 task、等待取消和 abort hook,再发TurnAborted,最后清理 TurnState waiter/input。 - 空的 active reservation 被清理时,
abort_empty_active_turn_preserves_pending_input验证 pending input 不会因为没有 RunningTask 而被误删。 - Turn permission response 使用 originating TurnState,避免响应抵达时写入错误的新 Turn。
replace_history清除 auto-compaction prefill;rate-limit 测试验证缺失 credits、plan 和 limit ID 时的合并规则。
建议读者按以下顺序反向验证:state/session_tests.rs 中的 connector、history、auto-compact 和 rate-limit 测试;session/tests.rs 中的 record_granted_request_permissions_for_turn_uses_originating_turn、request_permission_grants_are_environment_keyed、abort_empty_active_turn_preserves_pending_input、task_finish_emits_thread_idle_lifecycle_after_active_turn_clears 和 turn_start_lifecycle_exposes_turn_metadata_and_token_baseline。
9. 状态更新验证
- 说明为什么
Session.state、Session.active_turn和ActiveTurn.turn_state必须使用不同锁,而不能把所有字段合并到一个结构体。 - 遇到“权限响应写入了错误 Turn”时,检查响应保存的
originating_turn_state、PermissionGrantScope和当前 active Turn 是否发生切换。
可以用下面的只读搜索把本文的 session and turn state 主线落回源码:
rg -n "SessionState|TurnState|ActiveTurn|RunningTask|MailboxDeliveryPhase" codex-rs/core/src